# Get an API key
Source: https://docs.superserve.ai/api-key
Create an API key and configure the SDK to authenticate with Superserve.
Every SDK call is authenticated with an API key. Keys are prefixed with `ss_live_` and scoped to a single team.
## 1. Create a key in the console
Go to [console.superserve.ai](https://console.superserve.ai?utm_source=docs\&utm_medium=link), open **Settings → API keys**, and click **Create key**. Copy the value - it's only shown once.
Treat API keys like passwords. Never commit them to source control or paste them into client-side code.
## 2. Configure the SDK
By default, the SDK reads `SUPERSERVE_API_KEY` from the environment.
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
export SUPERSERVE_API_KEY=ss_live_...
```
You can also pass the key explicitly - useful in serverless environments where env vars are awkward.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({
name: "explicit-key",
apiKey: process.env.MY_KEY,
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import os
from superserve import Sandbox
sandbox = Sandbox.create(
name="explicit-key",
api_key=os.environ["MY_KEY"],
)
```
# List team audit-log activity
Source: https://docs.superserve.ai/api-reference/activity/list-team-audit-log-activity
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /activity
Returns audit-log activity for the authenticated team, ordered by
creation time (newest first) by default. Backs the console Audit Logs
page.
Pass `limit` (and `offset`) to fetch one page at a time; the
`X-Total-Count` response header reports the total across all pages.
The activity log grows without bound, so omitting `limit` returns only
the most recent page (up to the 200-row maximum) — page through older
history with `limit` + `offset`. Filter by exact `category` or
`status`, restrict to a window with `start`/`end`, and search with `q`
(case-insensitive substring across sandbox name, secret name, action,
and category).
# Get billing pricing
Source: https://docs.superserve.ai/api-reference/billing/get-billing-pricing
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /billing/pricing
Returns the authenticated team's active pricing plan and current
resource rates for billing UI display. The team is derived from
authentication; callers cannot select a team by query parameter.
Access requires `billing:read` and the `tenant_usage_dashboard`
rollout gate to be enabled for the team.
# Get billing summary
Source: https://docs.superserve.ai/api-reference/billing/get-billing-summary
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /billing/summary
Returns team-level financial information for the authenticated team's
current billing period. All charge fields are monetary USD values, not
raw usage metrics. The team is derived from authentication; callers
cannot select a team by query parameter.
Access requires `billing:read` and the `tenant_usage_dashboard`
rollout gate to be enabled for the team.
Phase 1 assumes no pricing tier or rate changes within the current
billing period. The summary applies the team's currently active pricing
rates to all current-period usage.
# Get public PAYG billing pricing
Source: https://docs.superserve.ai/api-reference/billing/get-public-payg-billing-pricing
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /billing/pricing/public
Returns the active public pay-as-you-go pricing plan and current
resource rates. This endpoint is unauthenticated so public pricing
pages can render from the same pricing data used by billing.
# Run a command and stream output over SSE
Source: https://docs.superserve.ai/api-reference/exec/run-a-command-and-stream-output-over-sse
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /exec/stream
Runs a command and streams its output as Server-Sent Events while it
runs. Each `data:` line is a JSON object: an output chunk
(`{"stdout":"..."}` or `{"stderr":"..."}`), the terminal event
(`{"exit_code":N,"finished":true}`), or an error
(`{"error":"...","finished":true}`). The stream closes when the command
exits. The sandbox must be `running`; a paused sandbox returns `503`.
Activate it first with `POST /sandboxes/{sandbox_id}/activate` (the SDKs
do this automatically).
`timeout_s` is a hard runtime limit: the command is killed after that
many seconds regardless of output, exiting with code 124.
# Run a command and wait for it to finish
Source: https://docs.superserve.ai/api-reference/exec/run-a-command-and-wait-for-it-to-finish
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /exec
Runs a command to completion and returns its full output in a single
response. For live output use `POST /exec/stream` (Server-Sent Events)
or `GET /exec/connect` (WebSocket). A non-zero exit code is returned in
the body, not as an HTTP error. The sandbox must be `running`; a paused
sandbox returns `503`. Activate it first with
`POST /sandboxes/{sandbox_id}/activate` (the SDKs do this automatically).
# Run a command over a WebSocket
Source: https://docs.superserve.ai/api-reference/exec/run-a-command-over-a-websocket
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /exec/connect
Runs a command over a WebSocket, streaming output back and accepting
stdin over one connection.
Connect with two `Sec-WebSocket-Protocol` values: `superserve.exec.v1`
and `token.`.
I/O rides binary frames prefixed with a one-byte channel, so output is
byte-exact; lifecycle and control ride text JSON frames.
- First frame (text): an `ExecRequest` JSON object. Omitting `timeout_s`
runs with no per-command timeout.
- Client frames: a binary frame is stdin, prefixed with channel `0x00`
(`[0x00][bytes]`); a text frame is JSON control —
`{"type":"stdin_close"}` or `{"type":"signal","name":"SIGINT"}`.
Frames are capped at 64 KiB, so chunk larger stdin.
- Server frames: a binary frame is output, prefixed with its channel
(`0x01` stdout, `0x02` stderr); a text frame is JSON lifecycle —
`{"exit_code":N,"finished":true}`, or `{"error":"...","code":"..."}`.
Closes on exit.
# List a directory inside a sandbox
Source: https://docs.superserve.ai/api-reference/files/list-a-directory-inside-a-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /sandboxes/{sandbox_id}/files
Returns a one-level listing of a directory inside the sandbox. The
listing is served through the control plane, so it works on every
sandbox regardless of when it was created. A paused sandbox is resumed
automatically before the listing and stays active afterward.
`modified_unix` is 0 for entries on older sandboxes that predate that
field.
# Read a file from a sandbox
Source: https://docs.superserve.ai/api-reference/files/read-a-file-from-a-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /files
Returns the file at `path` as raw bytes. To download a directory, set `format=zip` to receive its contents as a zip archive.
# Write a file to a sandbox
Source: https://docs.superserve.ai/api-reference/files/write-a-file-to-a-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /files
Creates parent directories as needed and overwrites any existing file.
# Add or invite a team member
Source: https://docs.superserve.ai/api-reference/rbac/add-or-invite-a-team-member
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /teams/{team_id}/members
# Assign a team role
Source: https://docs.superserve.ai/api-reference/rbac/assign-a-team-role
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /teams/{team_id}/roles
# Deactivate a team member
Source: https://docs.superserve.ai/api-reference/rbac/deactivate-a-team-member
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml delete /teams/{team_id}/members/{user_id}
# Get team management state
Source: https://docs.superserve.ai/api-reference/rbac/get-team-management-state
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /teams/{team_id}/management
Returns the caller-visible team members, role assignments, and
capability flags used by customer-facing member management UI. The
authenticated API key determines the actor; customer clients must not
send or rely on `X-Actor-User-Id`.
# List team members
Source: https://docs.superserve.ai/api-reference/rbac/list-team-members
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /teams/{team_id}/members
# List team role assignments
Source: https://docs.superserve.ai/api-reference/rbac/list-team-role-assignments
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /teams/{team_id}/roles
# Revoke a team role
Source: https://docs.superserve.ai/api-reference/rbac/revoke-a-team-role
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml delete /teams/{team_id}/roles/{assignment_id}
# Activate a sandbox
Source: https://docs.superserve.ai/api-reference/sandboxes/activate-a-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /sandboxes/{sandbox_id}/activate
Returns the sandbox with a fresh access token. If the sandbox is paused, it is resumed first. Idempotent — calling it on an active sandbox just returns a new token.
# Attach a secret to a sandbox
Source: https://docs.superserve.ai/api-reference/sandboxes/attach-a-secret-to-a-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /sandboxes/{sandbox_id}/secrets
Binds a stored secret to an existing sandbox under an env var. The
sandbox sees a stand-in token; the credential is swapped in for outbound
requests to its allowed hosts. Takes effect for processes started after
this call; a paused sandbox applies it on resume.
# Create a new sandbox
Source: https://docs.superserve.ai/api-reference/sandboxes/create-a-new-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /sandboxes
Creates a sandbox from a template (defaults to `superserve/base` when
`from_template` is omitted). When the request returns successfully,
the sandbox is ready to use — you can run commands against it
immediately.
# Delete a sandbox
Source: https://docs.superserve.ai/api-reference/sandboxes/delete-a-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml delete /sandboxes/{sandbox_id}
# Detach a secret from a sandbox
Source: https://docs.superserve.ai/api-reference/sandboxes/detach-a-secret-from-a-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml delete /sandboxes/{sandbox_id}/secrets/{env_key}
Removes a secret binding from an existing sandbox and revokes its
stand-in token, so requests that use it are refused — for an
already-running process, within about a minute. A paused sandbox applies
the change on resume.
# Get a sandbox by ID
Source: https://docs.superserve.ai/api-reference/sandboxes/get-a-sandbox-by-id
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /sandboxes/{sandbox_id}
# List a sandbox's egress activity
Source: https://docs.superserve.ai/api-reference/sandboxes/list-a-sandboxs-egress-activity
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /sandboxes/{sandbox_id}/network
The unified per-sandbox network log: every outbound connection the
sandbox made, merged into one time-ordered stream, most recent first.
Each row has a `kind` — `connection` (host, bytes, allow/deny verdict)
or `request` (HTTP method, path, status, and the secret used, when a
credential was injected). Fields not relevant to a row's kind are
omitted.
Filter by time window (`since`/`before`) and `verdict`. Paginate by
passing the response's `next_cursor` as `before` while `has_more` is
true. A `verdict` filter returns only `connection` rows, since request
rows carry no verdict.
# List all sandboxes
Source: https://docs.superserve.ai/api-reference/sandboxes/list-all-sandboxes
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /sandboxes
Returns sandboxes belonging to the authenticated team, ordered by
creation time (newest first) by default.
## Pagination, sorting, and search
Pass `limit` (and `offset`) to fetch one page at a time; the
`X-Total-Count` response header reports the total across all pages.
Omitting `limit` returns the full list, so existing unpaginated
callers are unaffected. Sort with `sort` + `order`, filter by exact
`status`, and search names with `q` (case-insensitive substring).
## Filtering by metadata
Any query parameter prefixed `metadata.` is treated as a filter
clause: `?metadata.env=prod&metadata.owner=agent-7`. Multiple
filters AND together — a sandbox matches only if every key/value
pair is present in its metadata. Values are compared as exact
strings; there is no type coercion or substring matching.
# Partially update a running sandbox
Source: https://docs.superserve.ai/api-reference/sandboxes/partially-update-a-running-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml patch /sandboxes/{sandbox_id}
Applies a partial update to a running sandbox. Each top-level field
in the request body is optional; only fields that are present are
applied. Omitted top-level fields are left unchanged. Nested objects
are full replacements when present — to clear a list, send it as an
empty array.
At least one top-level field must be present, otherwise the request
is rejected with `400`. Unknown top-level fields are also rejected
with `400` so typos surface as errors instead of silent no-ops.
## Currently patchable fields
- `network` — replaces the egress allow/deny rules. The sandbox
must be in the `active` state; patching a paused sandbox
returns `409`. Rules take effect immediately and are persisted so
they survive a future pause/resume cycle.
- `metadata` — replaces the sandbox's metadata tags. Can be updated
regardless of sandbox state (active, paused).
- `auto_delete_seconds` — sets or clears (`null`) the
garbage-collection window for the paused state. Can be updated
regardless of sandbox state. When applied to an already-paused
sandbox, the deletion deadline counts from the moment of this
request — never retroactively from when the sandbox paused — so
you always get the full window.
- `timeout_seconds` — sets or clears (`null`) the auto-pause
timeout. Can be updated regardless of sandbox state; on a paused
sandbox it applies to the next active session. The timeout is
evaluated against the current active session, so lowering it
below already-elapsed time pauses the sandbox promptly.
# Pause a running sandbox
Source: https://docs.superserve.ai/api-reference/sandboxes/pause-a-running-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /sandboxes/{sandbox_id}/pause
Snapshots the sandbox's full state (memory + disk), suspends the VM,
and transitions to `paused`. Resume it later to continue exactly where
it left off — same memory, same running processes, same files.
# Resume a paused sandbox
Source: https://docs.superserve.ai/api-reference/sandboxes/resume-a-paused-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /sandboxes/{sandbox_id}/resume
Restores the sandbox from its paused snapshot. Transitions back to
`active` with all state intact — same memory, same processes, same files.
# Create a secret
Source: https://docs.superserve.ai/api-reference/secrets/create-a-secret
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /secrets
Stores a credential under the caller's team. The plaintext is
envelope-encrypted at rest and is never returned by any API.
At sandbox-create time, bind the secret to an environment-variable
name via the `secrets` map on `POST /sandboxes`; the agent sees a
proxy token in env and the in-host enforcement daemon swaps it for
the real value at egress.
Use `provider` for built-in shortcuts (e.g. `anthropic`, `openai`,
`github`, `stripe`) which auto-fill the auth scheme and allowed
upstream hosts. Use `auth` + `hosts` for a custom integration.
# Get a secret's metadata
Source: https://docs.superserve.ai/api-reference/secrets/get-a-secrets-metadata
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /secrets/{name}
Returns metadata only — the cleartext value is never returned.
# List proxy egress events that used this credential
Source: https://docs.superserve.ai/api-reference/secrets/list-proxy-egress-events-that-used-this-credential
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /secrets/{name}/audit
Returns proxy_audit events for every outbound request that the
in-host enforcement daemon swapped with this credential, across
every sandbox it was bound to. Each row includes the originating
sandbox name (null when that sandbox has since been deleted).
# List sandboxes currently bound to this credential
Source: https://docs.superserve.ai/api-reference/secrets/list-sandboxes-currently-bound-to-this-credential
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /secrets/{name}/sandboxes
Returns the active (non-destroyed) sandboxes that have this credential bound, with the env-var name each binding uses. Useful before rotation or deletion ("which sandboxes will be affected?").
# List secrets for the calling team
Source: https://docs.superserve.ai/api-reference/secrets/list-secrets-for-the-calling-team
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /secrets
Returns metadata for all secrets owned by the team. Cleartext values
are never returned.
# List the built-in provider shortcut catalog
Source: https://docs.superserve.ai/api-reference/secrets/list-the-built-in-provider-shortcut-catalog
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /providers
Returns the providers customers can pass as `provider` on `POST /secrets`. Backend-of-record so the console picker stays in sync as the catalog grows.
# Revoke a secret
Source: https://docs.superserve.ai/api-reference/secrets/revoke-a-secret
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml delete /secrets/{name}
Soft-deletes the secret: the proxy daemon refuses to serve it and
new sandbox bindings fail. Existing audit-history queries still
resolve the row. The same name can be re-used by creating a new secret.
# Rotate a secret's value
Source: https://docs.superserve.ai/api-reference/secrets/rotate-a-secrets-value
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml patch /secrets/{name}
Replaces the stored ciphertext with a freshly-encrypted copy of the
new value. All sandboxes bound to this secret continue to work; in-host
caches are invalidated so the next egress uses the new value.
# Cancel an in-flight build
Source: https://docs.superserve.ai/api-reference/templates/cancel-an-in-flight-build
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml delete /templates/{template_id}/builds/{build_id}
Cancels an in-flight build. No-op for builds already in a terminal state.
# Create a template and kick off the first build
Source: https://docs.superserve.ai/api-reference/templates/create-a-template-and-kick-off-the-first-build
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /templates
Creates a template and queues the first build. The response includes
both the template id and the build id, so clients can immediately
poll `GET /templates/{id}` for overall status or subscribe to
`GET /templates/{id}/builds/{build_id}/logs` for live output.
Template starts in status `building`. Poll until it reaches `ready`
before creating sandboxes from it. On failure the status becomes
`failed` and `error_message` is populated.
To rebuild an existing template (e.g. after a failure or when the
base image updates), use `POST /templates/{id}/builds`.
# Delete a template
Source: https://docs.superserve.ai/api-reference/templates/delete-a-template
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml delete /templates/{template_id}
Delete a template owned by the caller's team. Returns `409` if any
active or paused sandbox still references this template, or if a
build is currently in progress; cancel those first.
# Get a build by ID
Source: https://docs.superserve.ai/api-reference/templates/get-a-build-by-id
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /templates/{template_id}/builds/{build_id}
# Get a template by ID
Source: https://docs.superserve.ai/api-reference/templates/get-a-template-by-id
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /templates/{template_id}
# List recent builds for a template
Source: https://docs.superserve.ai/api-reference/templates/list-recent-builds-for-a-template
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /templates/{template_id}/builds
# List templates visible to the caller
Source: https://docs.superserve.ai/api-reference/templates/list-templates-visible-to-the-caller
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /templates
Returns the caller's team's templates plus the curated system
templates (available to everyone, identified by the `superserve/` name
prefix — e.g. `superserve/base`, `superserve/python-3.11`, `superserve/node-22`),
ordered by creation time (newest first) by default.
## Pagination, sorting, and search
Pass `limit` (and `offset`) to page; `X-Total-Count` reports the total
across all pages. Omitting `limit` returns the full list. Narrow the
shelves with `owner`, sort with `sort` + `order`, and search names with
`q` (case-insensitive substring). The legacy `name_prefix` prefix
filter is still honored for backward compatibility.
# Rebuild an existing template
Source: https://docs.superserve.ai/api-reference/templates/rebuild-an-existing-template
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /templates/{template_id}/builds
Queues a new build for this template. Used to retry after a failed
build, or to rebuild when the base image has been updated.
The **first** build is queued automatically when the template is
created via `POST /templates` — this endpoint is only for
subsequent builds.
## Idempotency
If an in-flight build (`pending`/`building`/`snapshotting`) already
exists for this template with the same build spec, this endpoint
returns the existing build's id with `200` instead of creating a
duplicate.
# Stream build logs via SSE
Source: https://docs.superserve.ai/api-reference/templates/stream-build-logs-via-sse
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /templates/{template_id}/builds/{build_id}/logs
Server-Sent Events stream of the build's stdout/stderr. Connecting
replays buffered output from the start of the build, then streams
live as it arrives. Closes when the build reaches a terminal state.
# Run commands in a sandbox
Source: https://docs.superserve.ai/commands/overview
Execute shell commands inside a sandbox with sync or streaming output.
Run any shell command inside a sandbox using `sandbox.commands.run()`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({
name: "data-analyzer",
fromTemplate: "superserve/python-3.11",
})
const result = await sandbox.commands.run("python analyze.py")
console.log(result.stdout)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(name="data-analyzer", from_template="superserve/python-3.11")
result = sandbox.commands.run("python analyze.py")
print(result.stdout)
sandbox.kill()
```
This page covers `run()`. If you need to send input to a process while it runs, like a REPL, [`spawn()`](/commands/sessions) keeps the session open instead.
Using `AsyncSandbox`? The same methods are there, awaitable. See the [Sandbox reference](/sdk-reference/sandbox).
## Handling exit codes
`run()` doesn't throw on a non-zero exit code. Check `exitCode` on the result yourself.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const result = await sandbox.commands.run("exit 42")
if (result.exitCode !== 0) {
console.error(`Command failed: ${result.stderr}`)
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
result = sandbox.commands.run("exit 42")
if result.exit_code != 0:
print(f"Command failed: {result.stderr}")
```
## Working directory and environment
Pass `cwd` and `env` to control the execution context.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const result = await sandbox.commands.run("python script.py", {
cwd: "/app",
env: { PORT: "3000", DEBUG: "1" },
timeoutMs: 60_000,
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
result = sandbox.commands.run(
"python script.py",
cwd="/app",
env={"PORT": "3000", "DEBUG": "1"},
timeout_seconds=60,
)
```
Watch the units: TypeScript takes `timeoutMs` (milliseconds), Python takes `timeout_seconds` (seconds).
## Timeouts
Pass a timeout to kill a command that runs longer than you expect.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const result = await sandbox.commands.run("sleep 100", {
timeoutMs: 5_000,
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
result = sandbox.commands.run("sleep 100", timeout_seconds=5)
```
For longer commands, [stream the output](/commands/streaming) as it runs. To send input to a process while it runs, [open a session](/commands/sessions).
# Interactive processes
Source: https://docs.superserve.ai/commands/sessions
Send input to a running command, signal it, and stream its output back.
`run()` starts a command and gives you the result once it finishes. `spawn()` is for when you need to stay connected while it runs: send it input, signal it, and read its output as it arrives. You get back a session and keep it until the process exits.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const session = await sandbox.commands.spawn("python -i", {
onStdout: (data) => process.stdout.write(data),
})
session.stdin.write("print(2 + 2)\n")
session.stdin.close() // EOF, so the REPL exits
const result = await session.wait()
console.log(`exited ${result.exitCode}`)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# spawn is async only, so use AsyncSandbox.
async with await sandbox.commands.spawn(
"python -i",
on_stdout=lambda data: print(data, end=""),
) as session:
await session.stdin.write("print(2 + 2)\n")
await session.stdin.close() # EOF, so the REPL exits
result = await session.wait()
print(f"exited {result.exit_code}")
```
In Python, `spawn()` is on `AsyncSandbox` only; the synchronous `Sandbox.commands.spawn()` raises and points you there. In TypeScript it works in the browser and in Node 22+. Older Node needs a `WebSocket` polyfill such as [`ws`](https://www.npmjs.com/package/ws).
## When to use which
Most of the time you want `run()`: scripts, builds, anything that takes its input up front and hands back output. Switch to `spawn()` when the process reads from stdin, when you need to signal it, or when you're streaming an agent's output and feeding it more as it goes.
| | `run()` | `spawn()` |
| --------- | ---------------------------- | -------------------------------- |
| Gives you | the result, once it finishes | a session, while it runs |
| Stdin | none | `stdin.write()`, `stdin.close()` |
| Signals | none | `kill()` |
## The session
`spawn()` resolves once the process is running and hands back a session:
* `stdin.write(data)` takes a string or raw bytes. `stdin.close()` sends EOF, which programs like `cat` or a REPL wait for before they finish.
* `kill(signal)` sends a signal, `SIGTERM` by default.
* `wait()` resolves with the result when the process exits.
* Wrap the session in `await using` (TypeScript) or `async with` (Python) and it kills the process and closes the connection when the block ends.
The [SDK reference](/sdk-reference/commands#commandsession) lists the full surface.
## If the connection drops
`wait()` fails if the socket closes before the command finishes. The process keeps running inside the sandbox, so save the ID and pick it back up with `Sandbox.connect(id)`. A paused sandbox wakes on its own before the session opens.
# Stream command output
Source: https://docs.superserve.ai/commands/streaming
Watch a command's stdout and stderr as it runs.
For builds, training runs, servers, or anything else that takes a while, pass `onStdout` / `on_stdout` and `onStderr` / `on_stderr` callbacks. Output arrives as the command produces it, one line at a time, or in chunks when it comes fast.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const result = await sandbox.commands.run("npm run build", {
onStdout: (data) => process.stdout.write(data),
onStderr: (data) => process.stderr.write(data),
timeoutMs: 300_000,
})
console.log(`Build exited ${result.exitCode}`)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import sys
result = sandbox.commands.run(
"npm run build",
on_stdout=lambda data: sys.stdout.write(data),
on_stderr=lambda data: sys.stderr.write(data),
timeout_seconds=300,
)
print(f"Build exited {result.exit_code}")
```
Even while streaming, the returned `result` still holds the complete `stdout` and `stderr`.
`AsyncSandbox` takes the same callbacks with an awaitable `run()`. See the [Sandbox reference](/sdk-reference/sandbox).
## Idle timeout
The timer resets on every chunk, so a command that keeps producing output won't trip `timeoutMs` no matter how long it runs. A command that goes completely silent still has to finish before the timeout.
## Network drops
If the stream ends before the `finished` event (a network hiccup, say), `run()` throws. Retry from the caller if you need the resilience. The command keeps running in the sandbox after your client disconnects, so you can reconnect with `Sandbox.connect(id)` if you saved the ID.
## Capture and stream together
Streaming and sync return aren't mutually exclusive - capture chunks for logs while also collecting the full buffer for analysis.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const result = await sandbox.commands.run("./long-job.sh", {
onStdout: (data) => process.stdout.write(data), // watch it live
})
await uploadLogs(result.stdout) // full buffer, already captured for you
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
result = sandbox.commands.run(
"./long-job.sh",
on_stdout=lambda data: print(data, end=""), # watch it live
)
upload_logs(result.stdout) # full buffer, already captured for you
```
# PR Loop
Source: https://docs.superserve.ai/cookbook/pr-loop
An open-source PR review loop you install into any repo — Claude Code reviews every commit from a warm Superserve sandbox, with your own credentials and branding.
PR Loop is an open-source [agent loop](https://github.com/superserve-ai/superserve/tree/main/examples/loops) that shepherds your open pull requests. Each time a commit is pushed to a PR, Claude Code — running headless inside a warm Superserve sandbox — reviews the new code against a calibrated rubric, runs your project's own tests as a verifier, posts **one** concise review, and escalates risky changes to a human. It proposes; a human merges. It never merges, force-pushes, or edits CI.
Everything is self-hosted and yours: your repo, your GitHub identity, your Claude subscription, your sandboxes, your review norms.
## How it works
Three roles, and only one of them is a sandbox:
1. **Trigger** — a GitHub Actions workflow fires on `pull_request` events (`opened`, `synchronize`, `reopened`). No idle cron.
2. **Orchestrator** — a stateless script from the published `@superserve/loops` package runs on the CI runner: it finds the repo's loop sandbox by metadata, runs one tick inside it, then pauses it.
3. **Worker sandbox** — a Firecracker microVM booted from the `superserve/claude-code` template. The repo clone, the review state, and Claude Code all live here, warm between ticks.
The review runs in a sandbox rather than on the CI runner because:
* **Warm checkout.** The repo and its dependencies are cloned once, on the first tick. Every later tick resumes the same box and skips setup entirely.
* **Isolation.** A PR diff is untrusted code. It is reviewed — and your test suite runs against it — inside an isolated microVM, never on your runner or laptop.
* **\~\$0 idle.** The box is paused between ticks; you pay for seconds of compute per review, plus your LLM tokens.
## Prerequisites
| You need | Where to get it |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| **Superserve API key** (`ss_live_…`) | [console.superserve.ai](https://console.superserve.ai?utm_source=docs\&utm_medium=link) |
| **A Claude credential** | `claude setup-token` (Pro/Max/Team/Enterprise subscription, mints a \~1-year token) — or a metered `ANTHROPIC_API_KEY` |
| **bun** | [bun.sh](https://bun.sh) — only to run the installer |
| **GitHub access** | Nothing extra — reviews post as `github-actions[bot]` via the workflow's built-in token. A PAT or App is opt-in (below). |
## Install
Run inside the repo you want reviewed:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
bunx @superserve/loops add pr-loop
```
The installer does everything:
1. Prompts for your Superserve API key (hidden input) and — when the `claude` CLI is installed — runs `claude setup-token` for you to mint the long-lived Claude token.
2. Stores that token as a Superserve secret (`claude-oauth`) bound to `api.anthropic.com` — it is [swapped in at egress](/secrets/binding) and never enters the sandbox.
3. Sets `SUPERSERVE_API_KEY` as a GitHub Actions secret on the repo.
4. Writes `.github/workflows/loop-pr-loop.yml` — the only file added to your repo — then commits it (scoped to just that file; anything else you had staged stays staged) and pushes it after you confirm (`--yes` is the non-interactive consent).
Push a commit to any PR and it is reviewed within seconds.
The push only ever publishes the installer's own commit. If your branch is
already ahead of upstream with unpublished work, the installer refuses to
auto-push — and whenever it can't or won't push (no upstream, no write
access, confirmation declined), it prints the exact commands to run yourself.
The workflow runs the published package (`bunx @superserve/loops@stable run
pr-loop …`) — no loop source is vendored into your repo. `@stable` is a gated
release channel, so loop improvements roll out without you ever editing the
file.
For CI or scripted installs, pass credentials via env or flags instead of prompts:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
SUPERSERVE_API_KEY=… CLAUDE_CODE_OAUTH_TOKEN=… \
bunx @superserve/loops add pr-loop --yes
# preview without changing anything
bunx @superserve/loops add pr-loop --dry-run
```
## Try one tick locally
No install, no repo changes — good for seeing the resolved plan and one live review first:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# resolved plan only, needs no keys
bunx @superserve/loops run pr-loop --repo owner/name --dry-run
# one real review from your machine (the review still runs in a cloud sandbox)
SUPERSERVE_API_KEY=ss_live_… \
CLAUDE_CODE_OAUTH_TOKEN=… \
GITHUB_TOKEN=$(gh auth token) \
bunx @superserve/loops run pr-loop --repo owner/name --pr 42
```
Omit `--pr` to sweep every open PR in one tick, or add `--watch=15m` to re-tick on an interval.
## Where your credentials live
| Credential | Where it lives | Does it enter the sandbox? |
| ------------------ | -------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Superserve API key | GitHub Actions secret | No — only the orchestrator on the CI runner uses it |
| Claude token | Superserve secret `claude-oauth` | No — the box sees a stand-in value; the real token is attached at egress, only toward `api.anthropic.com` |
| GitHub token | Minted per run by GitHub Actions | Yes, but it is repo-scoped, least-privilege, and expires when the run ends |
See [Secrets](/secrets/overview) for how egress swapping works. A GitHub PAT (optional, below) gets the same treatment as the Claude token: stored as a Superserve secret, swapped at egress.
## Bring your own bot identity
By default reviews post as `github-actions[bot]` — zero GitHub credentials to create. Two opt-in upgrades:
### A custom name — machine-user PAT
Create a dedicated GitHub account (e.g. `acme-review-bot`) and mint a **fine-grained** PAT on the target repo with `Contents: Read`, `Pull requests: Read & write`, `Issues: Read & write`. Classic `repo`-scope tokens are refused — `repo` includes push and merge rights, which would void the loop's propose-only guarantee. Then install with:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
bunx @superserve/loops add pr-loop --github-token
```
Reviews now post as that account. The PAT is stored as the Superserve secret `loop-github-token` and swapped at egress. This path is also how one repo's workflow reviews a *different* repo.
### A name and an avatar — your own GitHub App
A GitHub App gets you the `[bot]` badge, a profile picture, and short-lived per-repo tokens. The App — and its private key — stay yours, in your own repo's (or org's) secrets; nothing is shared.
1. **Create the App**: GitHub → **Settings → Developer settings → GitHub Apps → New GitHub App**. The name becomes the handle (`acme-pr-loop` → `acme-pr-loop[bot]`). Uncheck the webhook. Permissions: `Contents: Read-only`, `Pull requests: Read & write`, `Issues: Read & write`.
2. **Upload a logo** under Display information (this is the avatar), **generate a private key**, and **install** the App on the repo you're reviewing.
3. **Store the App credentials** as Actions secrets — repo-level, or **org** secrets so every repo inherits them:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
gh secret set LOOP_APP_ID --body ""
gh secret set LOOP_APP_PRIVATE_KEY < ~/Downloads/your-app.*.private-key.pem
```
4. **Install with `--github-app`** — the installer writes the App-token workflow for you, no YAML editing:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
bunx @superserve/loops add pr-loop --github-app
```
The generated workflow mints a fresh installation token each run via `actions/create-github-app-token`, scoped to that one repo and expiring within the hour, and posts reviews as your App. Because the credentials are referenced by secret name, setting `LOOP_APP_ID` / `LOOP_APP_PRIVATE_KEY` as **org** secrets once covers every future repo — install the loop into a new repo and it inherits the identity with no extra token setup.
## Make it review your way
These work with the published loop as-is — nothing to fork:
* **Review norms and the merge bar.** Each run the loop reads your repo's `CLAUDE.md` / `AGENTS.md` / `README.md` for the review conventions and what "ready to merge" means in your project. Write your team's rules there and reviews follow them.
* **The verifier's checks.** The loop runs your project's *own* tests, lint, and build — discovered from `CLAUDE.md`, `AGENTS.md`, `Makefile`, or CI config on the default branch. Nothing loop-specific to define.
* **Your own skills.** Commit them to your repo at `.claude/skills//SKILL.md`. Claude Code runs inside your cloned repo, so it discovers project skills automatically.
Deeper changes — your own rubric, MCP servers, models, tool policy — mean running the loop from source. The [loop's README](https://github.com/superserve-ai/superserve/tree/main/examples/loops/pr-loop#deeper-changes-run-from-source) covers that path.
## Rotate, revoke, uninstall
* **Rotate a credential by re-running the installer.** `add` is idempotent: it rotates the existing Superserve secrets in place and rewrites the same workflow file. The Claude token lasts about a year; when it expires, run `claude setup-token` again and re-run the installer.
* **Revoke independently.** Revoke the Claude token in your Claude account, the PAT in GitHub settings, or the Superserve API key in the [console](https://console.superserve.ai?utm_source=docs\&utm_medium=link) — each kills its own half without touching the others.
* **Uninstall** by deleting the workflow file, then kill the warm sandbox (console → Sandboxes → the box named `pr-loop`, or `Sandbox.killById`) and delete the `claude-oauth` secret.
## Safety model
* **Never merges, force-pushes, or edits `.github/workflows`.** The strongest action it takes is a review plus a `ready-to-merge` label — kept in bounds by the skill's rules and, decisively, by the least-privilege token scope below: a `contents: read` token cannot merge or push, no matter what runs in the sandbox.
* **Prompt-injection firewall.** PR content is treated as data, not instructions. The loop refuses instructions embedded in diffs or comments and only runs the project's own checks, read from the default branch — never commands suggested by the PR.
* **Least privilege.** The workflow grants `contents: read` and `pull-requests: write`, nothing else. Fork PRs are skipped by default (their token is read-only); review them via the PAT path if you want them covered.
* **Human gates.** Security, auth, payments, or core-infra changes — and PRs stuck across several runs — get a `needs-human` label and an `@`-mention instead of silent action.
Two cost meters, don't conflate them: pausing the sandbox makes *sandbox
compute* roughly free between ticks, but *LLM tokens* are billed separately by
Anthropic. A subscription token from `claude setup-token` keeps ticks off
metered API billing.
## See also
The loop's code, review rubric, and the step-by-step README — from "just try
it" to a branded bot with your own skills.
How credentials are swapped in at egress so they never enter the sandbox.
Run Claude Code interactively in a Superserve sandbox.
Pause, resume, and the warm-state model the loop is built on.
# Errors
Source: https://docs.superserve.ai/errors
Typed errors thrown by the Superserve SDK and when to catch each one.
Every error raised by the SDK extends a common base class - `SandboxError` in TypeScript, `SandboxError` in Python - so you can catch it broadly or narrow down to a specific case.
## Error hierarchy
| TypeScript | Python | HTTP status | Meaning |
| --------------------- | --------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `SandboxError` | `SandboxError` | - | Base class for every SDK error. |
| `AuthenticationError` | `AuthenticationError` | `401` | Missing or invalid API key / access token. |
| `ValidationError` | `ValidationError` | `400` | Request body or parameters rejected by the API. |
| `NotFoundError` | `NotFoundError` | `404` | Sandbox or resource does not exist. |
| `ConflictError` | `ConflictError` | `409` | Sandbox is not in a valid state for the operation (e.g., pausing an already-paused sandbox, or patching `network` rules while paused). |
| `RateLimitError` | `RateLimitError` | `429` | Rate limit or per-team quota reached. Branch on `code` (see below). |
| `TimeoutError` | `SandboxTimeoutError` | - | Request or command timed out. |
| `ServerError` | `ServerError` | `5xx` | Platform error - usually transient (500/502/503/504). GET/DELETE requests auto-retry with exponential backoff. |
Python renames `TimeoutError` to `SandboxTimeoutError` so it doesn't shadow Python's built-in `TimeoutError`.
## Catching a specific error
Catch the typed subclass you care about and fall back to `SandboxError` for everything else.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { NotFoundError, Sandbox, SandboxError } from "@superserve/sdk"
try {
const sandbox = await Sandbox.connect("missing-id")
} catch (err) {
if (err instanceof NotFoundError) {
// Sandbox doesn't exist or was already deleted
} else if (err instanceof SandboxError) {
// Any other SDK error
}
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import NotFoundError, Sandbox, SandboxError
try:
sandbox = Sandbox.connect("missing-id")
except NotFoundError:
# Sandbox doesn't exist or was already deleted
pass
except SandboxError:
# Any other SDK error
pass
```
## Base error properties
Every SDK error extends `SandboxError` and exposes:
* `statusCode?: number` / `status_code: int | None` - HTTP status, if the error originated from an API response
* `code?: string` / `code: str | None` - API-provided error code, when present
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox, SandboxError } from "@superserve/sdk"
try {
await Sandbox.connect("missing-id")
} catch (err) {
if (err instanceof SandboxError) {
console.error(err.statusCode, err.code, err.message)
}
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox, SandboxError
try:
Sandbox.connect("missing-id")
except SandboxError as err:
print(err.status_code, err.code, err)
```
## Idempotent operations
`kill()` / `kill_by_id()` swallow `404` internally - it's safe to call on a sandbox that was already deleted by another process. Every other method surfaces `NotFoundError` to the caller.
## Retries
The SDK automatically retries transient failures on `GET` and `DELETE` requests with exponential backoff and jitter:
* `429 Too Many Requests`
* `5xx` server errors
* Network errors (connection reset, DNS failure)
`POST` / `PATCH` requests are **not** retried automatically because they aren't safely idempotent - the SDK surfaces the error so you can decide.
## `RateLimitError`
Raised on `429` responses. Branch on `code` to distinguish the variant:
| Code | Endpoint(s) | Meaning |
| -------------------- | ----------------- | ------------------------------------------------------------------------------------- |
| `rate_limited` | any | Request rate exceeded; retry after a short backoff. |
| `too_many_builds` | `POST /templates` | Team has reached its concurrent build limit. Wait for an active build to finish. |
| `too_many_templates` | `POST /templates` | Team has reached its total template count limit. Delete one or contact support. |
| `too_many_sandboxes` | `POST /sandboxes` | Team has reached its active sandbox count limit. Pause/delete one or contact support. |
The error `message` already includes the cap and a contact-support hint, so surfacing it directly to users is usually fine.
## `BuildError`
Raised by `Template.waitUntilReady()` / `wait_until_ready()` when the awaited build lands on status `failed`.
**Fields:**
* `code` - stable error prefix (`image_pull_failed`, `step_failed`, `boot_failed`, `snapshot_failed`, `start_cmd_failed`, `ready_cmd_failed`, `build_failed`)
* `buildId` / `build_id` - the build that failed
* `templateId` / `template_id` - the template id
* `message` - human-readable detail. The backend's `error_message` follows a `": "` convention (e.g. `"image_too_large: image is too large for the requested disk_mib"`); the SDK splits it so `code` holds the prefix and `message` holds the detail. When the backend doesn't include a prefix or message, `message` falls back to `"Template build failed"`.
See [BuildSpec reference: build error codes](/templates/build-spec#build-error-codes) for a full list of codes and their meanings.
# Read and write files
Source: https://docs.superserve.ai/filesystem/read-write
Upload and download files to and from a sandbox's filesystem.
Use `sandbox.files` to move data in and out of a sandbox. The SDK handles data-plane routing and authentication transparently.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.files.write("/app/config.json", '{"key": "value"}')
const text = await sandbox.files.readText("/app/config.json")
console.log(text)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.files.write("/app/config.json", '{"key": "value"}')
text = sandbox.files.read_text("/app/config.json")
print(text)
```
Using `AsyncSandbox`? The same `files` methods are there, awaitable.
## Write binary content
`write()` accepts strings or raw bytes. Parent directories are created automatically.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { readFileSync } from "node:fs"
const buffer = readFileSync("./local-image.png")
await sandbox.files.write("/app/image.png", buffer)
await sandbox.files.write("/app/data.bin", new Uint8Array([1, 2, 3, 4]))
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
with open("local-image.png", "rb") as f:
sandbox.files.write("/app/image.png", f.read())
sandbox.files.write("/app/data.bin", bytes([1, 2, 3, 4]))
```
## Read as bytes or text
* `read()` returns raw bytes (`Uint8Array` in TypeScript, `bytes` in Python)
* `readText()` / `read_text()` returns a UTF-8-decoded string
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const bytes: Uint8Array = await sandbox.files.read("/app/image.png")
const text: string = await sandbox.files.readText("/app/config.json")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
data: bytes = sandbox.files.read("/app/image.png")
text: str = sandbox.files.read_text("/app/config.json")
```
## Download a directory as a zip
`downloadDir()` / `download_dir()` exports a whole directory as a ZIP archive —
useful for pulling out logs, build artifacts, or results in one call. It returns
the raw zip bytes; large directories can exceed the default 30s timeout, so pass
a longer `timeoutMs` / `timeout` when needed.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { writeFileSync } from "node:fs"
const zip = await sandbox.files.downloadDir("/app/results", {
timeoutMs: 120_000,
})
writeFileSync("results.zip", zip)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
zip_bytes = sandbox.files.download_dir("/app/results", timeout=120)
with open("results.zip", "wb") as f:
f.write(zip_bytes)
```
## Path rules
Paths passed to `files.*` methods must:
* **Start with `/`** - only absolute paths are accepted
* **Contain no `..` segments** - no traversal
Parent directories are created automatically on write - you don't need to `mkdir -p` first.
## Errors
See the [files reference](/sdk-reference/files#errors) and the full [error hierarchy](/errors).
# Agno
Source: https://docs.superserve.ai/integrations/agent-harnesses/agno
Run code from Agno agents in isolated Superserve sandboxes with SuperserveTools.
Agno includes a `SuperserveTools` toolkit for running agent-generated code in isolated cloud sandboxes. The sandbox persists across tool calls, so files and installed packages remain available throughout the session.
`SuperserveTools` is available in Agno 2.7.4 and later.
## Setup
Install Agno, the OpenAI model provider, and the Superserve SDK:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
uv pip install -U "agno>=2.7.4" openai superserve
export OPENAI_API_KEY=sk-...
export SUPERSERVE_API_KEY=ss_live_...
```
See [API keys](/api-key) to create a Superserve API key.
## Run an Agno agent in a sandbox
`SuperserveTools` gives the agent tools for Python and shell execution, file operations, sandbox inspection, preview URLs, and lifecycle management.
```python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from agno.agent import Agent
from agno.models.openai import OpenAIResponses
from agno.tools.superserve import SuperserveTools
agent = Agent(
name="Sandboxed Python Agent",
model=OpenAIResponses(id="gpt-5.5"),
tools=[SuperserveTools(timeout=600)],
instructions=[
"Execute requested code with run_python_code or run_command.",
"Return the code and its actual execution output.",
],
)
agent.print_response(
"Generate the first 10 Fibonacci numbers, then calculate their sum and average."
)
```
The toolkit creates a Python-ready sandbox on the first tool call. With `persistent=True`, the default, Agno stores its ID in session state and reuses the same sandbox across tool calls and runs in that session. Sync and async agents use the same sandbox automatically.
## Configure the sandbox
Pass Superserve sandbox options directly to the toolkit:
```python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
tools = [
SuperserveTools(
template="superserve/node-22",
timeout=600,
auto_delete_seconds=3600,
secrets={"OPENAI_API_KEY": "openai-prod"},
enable_pause_sandbox=True,
enable_resume_sandbox=True,
)
]
```
| Option | Purpose |
| -------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `template` | Select the sandbox runtime. The default is `superserve/code-interpreter`. |
| `sandbox_id` | Connect the toolkit to an existing sandbox. |
| `timeout` | Set the sandbox lifetime before it is automatically stopped. |
| `auto_delete_seconds` | Set a hard time-to-live after which the sandbox is deleted. |
| `secrets` | Bind [team secrets](/secrets/binding) without exposing their values in the sandbox. |
| `enable_pause_sandbox` and `enable_resume_sandbox` | Let the agent pause and resume its current sandbox. |
| `all` | Enable every toolkit function, including opt-in lifecycle and secret tools. |
## Resources
Review every toolkit option and function.
Open the runnable example in the Agno repository.
# Claude Agent SDK
Source: https://docs.superserve.ai/integrations/agent-harnesses/claude-agent-sdk
Use Superserve sandboxes as the execution runtime for agents built with the Claude Agent SDK.
The Claude Agent SDK is Anthropic's framework for building agents with tool-use loops. Expose `sandbox.commands.run` as an in-process MCP tool so every command the agent generates runs in an isolated VM instead of on your machine.
## Setup
```bash TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
npm install @superserve/sdk @anthropic-ai/claude-agent-sdk zod
export SUPERSERVE_API_KEY=ss_live_...
```
```bash Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
pip install superserve claude-agent-sdk
export SUPERSERVE_API_KEY=ss_live_...
```
## Wire the sandbox into the agent
The highlighted block defines a `bash` tool backed by the sandbox. Everything else is standard Claude Agent SDK usage.
```typescript TypeScript {7-19} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk"
import { Sandbox } from "@superserve/sdk"
import { z } from "zod"
const sandbox = await Sandbox.create({ name: "agent-runtime" })
const bash = tool(
"bash",
"Run a shell command in the sandbox.",
{ command: z.string() },
async ({ command }) => {
const result = await sandbox.commands.run(command, { timeoutMs: 60_000 })
return {
content: [
{ type: "text", text: `${result.stdout}\n${result.stderr}` },
],
}
},
)
const superserve = createSdkMcpServer({
name: "superserve",
version: "1.0.0",
tools: [bash],
})
for await (const message of query({
prompt: "List the files in /tmp and tell me how many there are.",
options: {
mcpServers: { superserve },
allowedTools: ["mcp__superserve__bash"],
},
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result)
}
}
await sandbox.kill()
```
```python Python {7-14} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import anyio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, create_sdk_mcp_server, tool
from superserve import AsyncSandbox
sandbox: AsyncSandbox | None = None
@tool("bash", "Run a shell command in the sandbox.", {"command": str})
async def bash(args):
result = await sandbox.commands.run(args["command"], timeout_seconds=60)
return {
"content": [
{"type": "text", "text": f"{result.stdout}\n{result.stderr}"},
],
}
superserve = create_sdk_mcp_server(name="superserve", version="1.0.0", tools=[bash])
options = ClaudeAgentOptions(
mcp_servers={"superserve": superserve},
allowed_tools=["mcp__superserve__bash"],
)
async def main():
global sandbox
sandbox = await AsyncSandbox.create(name="agent-runtime")
async with ClaudeSDKClient(options=options) as client:
await client.query("List the files in /tmp and tell me how many there are.")
async for msg in client.receive_response():
print(msg)
await sandbox.kill()
anyio.run(main)
```
Add equivalent tools for file reads/writes the same way; see [Run commands](/commands/overview) and [Read & write files](/filesystem/read-write).
# OpenAI Agents SDK
Source: https://docs.superserve.ai/integrations/agent-harnesses/openai-agents-sdk
Use Superserve sandboxes as the execution runtime for agents built with the OpenAI Agents SDK.
The OpenAI Agents SDK is OpenAI's framework for building agents with tool-use loops. Expose `sandbox.commands.run` as a function tool so every command the agent generates runs in an isolated VM instead of on your machine.
## Setup
```bash TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
npm install @superserve/sdk @openai/agents zod
export SUPERSERVE_API_KEY=ss_live_...
```
```bash Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
pip install superserve openai-agents
export SUPERSERVE_API_KEY=ss_live_...
```
## Wire the sandbox into the agent
The highlighted block defines a `bash` tool backed by the sandbox. Everything else is standard OpenAI Agents SDK usage.
```typescript TypeScript {7-15} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Agent, run, tool } from "@openai/agents"
import { Sandbox } from "@superserve/sdk"
import { z } from "zod"
const sandbox = await Sandbox.create({ name: "agent-runtime" })
const bash = tool({
name: "bash",
description: "Run a shell command in the sandbox.",
parameters: z.object({ command: z.string() }),
execute: async ({ command }) => {
const result = await sandbox.commands.run(command, { timeoutMs: 60_000 })
return `${result.stdout}\n${result.stderr}`
},
})
const agent = new Agent({
name: "Workspace Assistant",
instructions: "Use the bash tool to inspect the sandbox before answering.",
tools: [bash],
})
const result = await run(
agent,
"List the files in /tmp and tell me how many there are.",
)
console.log(result.finalOutput)
await sandbox.kill()
```
```python Python {7-11} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import asyncio
from agents import Agent, Runner, function_tool
from superserve import Sandbox
sandbox = Sandbox.create(name="agent-runtime")
@function_tool
def bash(command: str) -> str:
"""Run a shell command in the sandbox."""
result = sandbox.commands.run(command, timeout_seconds=60)
return f"{result.stdout}\n{result.stderr}"
agent = Agent(
name="Workspace Assistant",
instructions="Use the bash tool to inspect the sandbox before answering.",
tools=[bash],
)
async def main():
result = await Runner.run(
agent,
"List the files in /tmp and tell me how many there are.",
)
print(result.final_output)
sandbox.kill()
asyncio.run(main())
```
Add equivalent tools for file reads/writes the same way; see [Run commands](/commands/overview) and [Read & write files](/filesystem/read-write).
# Claude Code
Source: https://docs.superserve.ai/integrations/coding-agents/claude
Run Claude Code in a Superserve sandbox.
Claude Code is Anthropic's terminal coding agent. Run it in a Superserve sandbox so its shell commands and file edits stay isolated from your machine.
## Via the console
1. On the [Secrets page](https://console.superserve.ai/secrets?utm_source=docs\&utm_medium=link), create a secret with the **Anthropic** provider and paste your API key — you only do this once, and the key never enters the sandbox.
2. Go to [console.superserve.ai](https://console.superserve.ai?utm_source=docs\&utm_medium=link) and click **Create sandbox**.
3. Pick the **`superserve/claude-code`** template (Claude Code preinstalled).
4. Under **Advanced Options → Secrets**, bind that secret to `ANTHROPIC_API_KEY`.
5. Create the sandbox, open its **Terminal**, and run `claude`.
## Via the SDK
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
npm install @superserve/sdk
export SUPERSERVE_API_KEY=ss_live_...
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox, Secret } from "@superserve/sdk"
// One-time: store your key as a secret. It never enters the sandbox.
await Secret.create({
name: "anthropic-key",
value: "sk-ant-...",
provider: "anthropic",
})
const sandbox = await Sandbox.create({
name: "claude-code",
fromTemplate: "superserve/claude-code",
secrets: { ANTHROPIC_API_KEY: "anthropic-key" },
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox, Secret
# One-time: store your key as a secret. It never enters the sandbox.
Secret.create(name="anthropic-key", value="sk-ant-...", provider="anthropic")
sandbox = Sandbox.create(
name="claude-code",
from_template="superserve/claude-code",
secrets={"ANTHROPIC_API_KEY": "anthropic-key"},
)
```
Claude Code reads `ANTHROPIC_API_KEY` as usual, but its value is a [stand-in token](/secrets/overview) — the real key is attached only on requests to Anthropic, never exposed to the agent.
Open the sandbox in the [console](https://console.superserve.ai?utm_source=docs\&utm_medium=link), launch the terminal, and run `claude`.
## Persist sessions
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.pause() // checkpoint state to disk
await sandbox.resume() // restore exactly where you left off
```
# Codex
Source: https://docs.superserve.ai/integrations/coding-agents/codex
Run the OpenAI Codex CLI in a Superserve sandbox.
Codex is OpenAI's terminal coding agent. Run it in a Superserve sandbox so its shell commands and file edits stay isolated from your machine.
## Via the console
1. On the [Secrets page](https://console.superserve.ai/secrets?utm_source=docs\&utm_medium=link), create a secret with the **OpenAI** provider and paste your API key — you only do this once, and the key never enters the sandbox.
2. Go to [console.superserve.ai](https://console.superserve.ai?utm_source=docs\&utm_medium=link) and click **Create sandbox**.
3. Pick the **`superserve/node-22`** template.
4. Under **Advanced Options → Secrets**, bind that secret to `OPENAI_API_KEY`.
5. Create the sandbox, open its **Terminal**, and run:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
npm install -g @openai/codex && codex
```
## Via the SDK
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
npm install @superserve/sdk
export SUPERSERVE_API_KEY=ss_live_...
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox, Secret } from "@superserve/sdk"
// One-time: store your key as a secret. It never enters the sandbox.
await Secret.create({ name: "openai-key", value: "sk-...", provider: "openai" })
const sandbox = await Sandbox.create({
name: "codex",
fromTemplate: "superserve/node-22",
secrets: { OPENAI_API_KEY: "openai-key" },
})
await sandbox.commands.run("npm install -g @openai/codex")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox, Secret
# One-time: store your key as a secret. It never enters the sandbox.
Secret.create(name="openai-key", value="sk-...", provider="openai")
sandbox = Sandbox.create(
name="codex",
from_template="superserve/node-22",
secrets={"OPENAI_API_KEY": "openai-key"},
)
sandbox.commands.run("npm install -g @openai/codex")
```
Codex reads `OPENAI_API_KEY` as usual, but its value is a [stand-in token](/secrets/overview) — the real key is attached only on requests to OpenAI, never exposed to the agent.
Open the sandbox in the [console](https://console.superserve.ai?utm_source=docs\&utm_medium=link), launch the terminal, and run `codex`.
## Persist sessions
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.pause()
await sandbox.resume()
```
# Kilocode
Source: https://docs.superserve.ai/integrations/coding-agents/kilocode
Run Kilocode in a Superserve sandbox.
Pair Kilocode with a Superserve sandbox to run its generated commands and file edits in an isolated VM.
## Via the console
1. On the [Secrets page](https://console.superserve.ai/secrets?utm_source=docs\&utm_medium=link), create a secret for your model provider (e.g. **Anthropic**) and paste your API key — you only do this once, and the key never enters the sandbox.
2. Go to [console.superserve.ai](https://console.superserve.ai?utm_source=docs\&utm_medium=link) and click **Create sandbox**.
3. Pick the **`superserve/node-22`** template.
4. Under **Advanced Options → Secrets**, bind that secret to your provider's key env var (e.g. `ANTHROPIC_API_KEY`).
5. Create the sandbox, open its **Terminal**, and install Kilocode.
## Via the SDK
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
npm install @superserve/sdk
export SUPERSERVE_API_KEY=ss_live_...
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox, Secret } from "@superserve/sdk"
// One-time: store your key as a secret. It never enters the sandbox.
await Secret.create({
name: "anthropic-key",
value: "sk-ant-...",
provider: "anthropic",
})
const sandbox = await Sandbox.create({
name: "kilocode",
fromTemplate: "superserve/node-22",
secrets: { ANTHROPIC_API_KEY: "anthropic-key" },
})
await sandbox.commands.run("npm install -g @kilocode/cli")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox, Secret
# One-time: store your key as a secret. It never enters the sandbox.
Secret.create(name="anthropic-key", value="sk-ant-...", provider="anthropic")
sandbox = Sandbox.create(
name="kilocode",
from_template="superserve/node-22",
secrets={"ANTHROPIC_API_KEY": "anthropic-key"},
)
sandbox.commands.run("npm install -g @kilocode/cli")
```
Kilocode reads `ANTHROPIC_API_KEY` as usual, but its value is a [stand-in token](/secrets/overview) — the real key is attached only on requests to the provider, never exposed to the agent.
Open the sandbox in the [console](https://console.superserve.ai?utm_source=docs\&utm_medium=link), launch the terminal, and run `kilocode`.
## Persist sessions
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.pause()
await sandbox.resume()
```
# Opencode
Source: https://docs.superserve.ai/integrations/coding-agents/opencode
Run Opencode in a Superserve sandbox.
Opencode is an open-source terminal coding agent. Run it in a Superserve sandbox so its shell commands and file edits stay isolated from your machine.
## Via the console
1. On the [Secrets page](https://console.superserve.ai/secrets?utm_source=docs\&utm_medium=link), create a secret with the **Anthropic** provider and paste your API key — you only do this once, and the key never enters the sandbox.
2. Go to [console.superserve.ai](https://console.superserve.ai?utm_source=docs\&utm_medium=link) and click **Create sandbox**.
3. Pick the **`superserve/node-22`** template.
4. Under **Advanced Options → Secrets**, bind that secret to `ANTHROPIC_API_KEY`.
5. Create the sandbox, open its **Terminal**, and run:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
curl -fsSL https://opencode.ai/install | bash && opencode
export PATH=/home/user/.opencode/bin:$PATH
```
## Via the SDK
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
npm install @superserve/sdk
export SUPERSERVE_API_KEY=ss_live_...
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox, Secret } from "@superserve/sdk"
// One-time: store your key as a secret. It never enters the sandbox.
await Secret.create({
name: "anthropic-key",
value: "sk-ant-...",
provider: "anthropic",
})
const sandbox = await Sandbox.create({
name: "opencode",
fromTemplate: "superserve/node-22",
secrets: { ANTHROPIC_API_KEY: "anthropic-key" },
})
await sandbox.commands.run("curl -fsSL https://opencode.ai/install | bash")
await sandbox.commands.run("echo 'export PATH=/home/user/.opencode/bin:$PATH' >> ~/.bashrc")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox, Secret
# One-time: store your key as a secret. It never enters the sandbox.
Secret.create(name="anthropic-key", value="sk-ant-...", provider="anthropic")
sandbox = Sandbox.create(
name="opencode",
from_template="superserve/node-22",
secrets={"ANTHROPIC_API_KEY": "anthropic-key"},
)
sandbox.commands.run("curl -fsSL https://opencode.ai/install | bash")
sandbox.commands.run("echo 'export PATH=/home/user/.opencode/bin:$PATH' >> ~/.bashrc")
```
Opencode reads `ANTHROPIC_API_KEY` as usual, but its value is a [stand-in token](/secrets/overview) — the real key is attached only on requests to Anthropic, never exposed to the agent.
Open the sandbox in the [console](https://console.superserve.ai?utm_source=docs\&utm_medium=link), launch the terminal, and run `opencode`.
## Persist sessions
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.pause()
await sandbox.resume()
```
# Run Claude Managed Agents on Superserve
Source: https://docs.superserve.ai/integrations/managed-agents/claude-managed-agents
A guide to running Claude Managed Agents inside your own Superserve sandboxes, as a self-hosted environment.
[Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) is Anthropic's configurable harness and infrastructure for running Claude as an autonomous agent. You define an agent (model, system prompt, tools, and MCP servers), then open sessions and stream events as Claude reads files, runs commands, and calls tools to complete a task.
By default, those tool calls run inside Anthropic-managed cloud containers. A self-hosted environment moves just the container to your own infrastructure. Everything else stays on Anthropic's side: the agent loop, model calls, prompt caching, event stream, and session history. Only the filesystem and shell (where `bash`, `read`, `write`, and similar tools execute) run in a Superserve sandbox.
## How it works
* **Anthropic** runs the API, agent loop, and a per-environment work queue that signals when tools need to execute.
* **You** run an orchestrator that watches the queue, manages sandbox lifecycle, and starts the tool runner inside each sandbox. Separately, your application creates sessions and engages end users.
* **Superserve** provides per-session sandboxes - isolated microVMs with their own filesystem, network namespace, and process tree.
Tool dispatch depends on the tool type:
* **Filesystem and shell tools** (`bash`, `read`, `write`, `edit`, `glob`, `grep`) execute inside your Superserve sandbox. The tool runner handles each call against the sandbox's filesystem and shell, posting results back to the session.
* **Web tools** (`web_search`, `web_fetch`) and **MCP server tools** route through Anthropic's servers. The sandbox is not involved.
Each session gets its own sandbox. Filesystem state persists across tool calls, and idle sandboxes can be paused and resumed on demand.
A [reference implementation](https://github.com/superserve-ai/superserve/tree/main/guides/managed-agents/claude-managed-agents) with the orchestrator, runner, and setup scripts is available on GitHub in both Python and TypeScript.
## Prerequisites
* A [Superserve account](https://console.superserve.ai) and API key
* An [Anthropic account](https://platform.claude.com) with environments access
* Python 3.12+ or Node.js 22+ on the orchestrator host
## Create a self-hosted environment
In the [Claude Platform Console](https://platform.claude.com/workspaces/default/environments): **Workspace > Environments > New > Self-hosted**. Or create one via the API:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import Anthropic from "@anthropic-ai/sdk"
const client = new Anthropic()
const environment = await client.beta.environments.create({
name: "superserve",
config: { type: "self_hosted" },
})
console.log(environment.id)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import anthropic
client = anthropic.Anthropic()
environment = client.beta.environments.create(
name="superserve",
config={"type": "self_hosted"},
)
print(environment.id)
```
Open the environment in the Console and click **Generate environment key**. Export these values on the host where your orchestrator will run:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
export ANTHROPIC_ENVIRONMENT_KEY="sk-ant-oat01-..."
export ANTHROPIC_ENVIRONMENT_ID="env_..."
export SUPERSERVE_API_KEY="ss_live_..."
```
Create your agent. The same definition works for both cloud and self-hosted - the environment is chosen per session.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const agent = await client.beta.agents.create({
name: "sandbox-agent",
model: "claude-sonnet-4-6",
system: "You are a coding assistant with a Linux sandbox.",
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
agent = client.beta.agents.create(
name="sandbox-agent",
model="claude-sonnet-4-6",
system="You are a coding assistant with a Linux sandbox.",
)
```
## Build the sandbox template
Build a Superserve template with Python and the Anthropic SDK pre-installed. Sandboxes created from this template boot in under 50ms - no image pull or package install at session time.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Template } from "@superserve/sdk"
const template = await Template.create({
name: "claude-managed-agent",
from: "python:3.12-slim",
vcpu: 2,
memoryMib: 2048,
steps: [
{
run:
"apt-get update && apt-get install -y --no-install-recommends " +
"curl git jq procps && rm -rf /var/lib/apt/lists/*",
},
{ run: "pip install --no-cache-dir anthropic" },
{ run: "mkdir -p /workspace /mnt/session/outputs" },
{ workdir: "/workspace" },
],
})
await template.waitUntilReady({
onLog: (ev) => {
if (ev.stream !== "system") process.stdout.write(ev.text)
},
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Template, RunStep, WorkdirStep
template = Template.create(
name="claude-managed-agent",
from_="python:3.12-slim",
vcpu=2,
memory_mib=2048,
steps=[
RunStep(run=(
"apt-get update && apt-get install -y --no-install-recommends "
"curl git jq procps && rm -rf /var/lib/apt/lists/*"
)),
RunStep(run="pip install --no-cache-dir anthropic"),
RunStep(run="mkdir -p /workspace /mnt/session/outputs"),
WorkdirStep(workdir="/workspace"),
],
)
template.wait_until_ready(
on_log=lambda ev: print(ev.text, end="")
if ev.stream.value != "system"
else None,
)
```
Run this once. The template persists in your Superserve account and every session reuses it.
Add any runtimes, system packages, or internal tools your agent needs to the template's build steps. The snapshot captures the full filesystem, so sandboxes inherit everything without install cost at session time.
## Write the in-sandbox runner
The runner is a small Python script that the orchestrator starts inside each sandbox. It calls Anthropic's `handle_item()`, which attaches to the session event stream, executes tool calls (`bash`, `read`, `write`, `edit`, `glob`, `grep`) against the sandbox, heartbeats the work-item lease, and stops cleanly on exit.
```python runner.py theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import asyncio
import os
from anthropic import AsyncAnthropic
async def main():
environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
async with AsyncAnthropic(auth_token=environment_key) as client:
await client.beta.environments.work.worker(
environment_key=environment_key,
workdir="/workspace",
).handle_item()
asyncio.run(main())
```
`handle_item()` reads `ANTHROPIC_SESSION_ID`, `ANTHROPIC_WORK_ID`, and `ANTHROPIC_ENVIRONMENT_ID` from environment variables automatically. The orchestrator sets all four when it launches the runner.
## Run the orchestrator
The orchestrator long-polls Anthropic's work queue, ensures a Superserve sandbox is running for each session, and launches the runner inside it. For multi-turn sessions it reuses the same sandbox, resuming from a paused state if needed - preserving the agent's filesystem and in-memory state across turns.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import Anthropic from "@anthropic-ai/sdk"
import { Sandbox } from "@superserve/sdk"
import { readFileSync } from "node:fs"
const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!
const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!
const runner = readFileSync("runner.py", "utf8")
const client = new Anthropic({ authToken: environmentKey })
async function handleWork(work: { id: string; data: { id: string } }) {
const sessionId = work.data.id
// Find existing sandbox for this session, or create a new one
const existing = await Sandbox.list({
metadata: { "cma.session_id": sessionId },
})
const live = existing.filter(
(s) => s.status === "active" || s.status === "paused",
)
let sandbox: Sandbox
if (live.length > 0) {
sandbox = await Sandbox.connect(live[0].id)
if (live[0].status === "paused") await sandbox.resume()
} else {
sandbox = await Sandbox.create({
name: `cma-${sessionId.slice(0, 8)}`,
fromTemplate: "claude-managed-agent",
metadata: { "cma.session_id": sessionId },
network: { allowOut: ["api.anthropic.com"] },
})
}
await sandbox.files.write("/workspace/runner.py", runner)
await sandbox.commands.run(
"nohup python3 /workspace/runner.py > /workspace/runner.log 2>&1 &",
{
env: {
ANTHROPIC_ENVIRONMENT_KEY: environmentKey,
ANTHROPIC_WORK_ID: work.id,
ANTHROPIC_SESSION_ID: sessionId,
ANTHROPIC_ENVIRONMENT_ID: environmentId,
},
},
)
}
for await (const work of client.beta.environments.work.poller({
environmentId,
environmentKey,
autoStop: false,
})) {
await handleWork(work)
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import asyncio
import os
from pathlib import Path
from anthropic import AsyncAnthropic
from superserve import AsyncSandbox, NetworkConfig
ENVIRONMENT_KEY = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
ENVIRONMENT_ID = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
RUNNER = Path("runner.py").read_text()
async def handle_work(work):
session_id = work.data.id
# Find existing sandbox for this session, or create a new one
existing = await AsyncSandbox.list(
metadata={"cma.session_id": session_id},
)
live = [s for s in existing if s.status in ("active", "paused")]
if live:
sandbox = await AsyncSandbox.connect(live[0].id)
if live[0].status == "paused":
await sandbox.resume()
else:
sandbox = await AsyncSandbox.create(
name=f"cma-{session_id[:8]}",
from_template="claude-managed-agent",
metadata={"cma.session_id": session_id},
network=NetworkConfig(allow_out=["api.anthropic.com"]),
)
await sandbox.files.write("/workspace/runner.py", RUNNER)
await sandbox.commands.run(
"nohup python3 /workspace/runner.py > /workspace/runner.log 2>&1 &",
env={
"ANTHROPIC_ENVIRONMENT_KEY": ENVIRONMENT_KEY,
"ANTHROPIC_WORK_ID": work.id,
"ANTHROPIC_SESSION_ID": session_id,
"ANTHROPIC_ENVIRONMENT_ID": ENVIRONMENT_ID,
},
)
async def main():
async with AsyncAnthropic(auth_token=ENVIRONMENT_KEY) as client:
async for work in client.beta.environments.work.poller(
environment_id=ENVIRONMENT_ID,
environment_key=ENVIRONMENT_KEY,
auto_stop=False,
):
await handle_work(work)
asyncio.run(main())
```
What the orchestrator does on each work item:
1. **Finds or creates** a sandbox for the session using a metadata tag. Existing paused sandboxes are resumed rather than recreated.
2. **Locks down egress** — each sandbox can only reach `api.anthropic.com`, preventing data exfiltration to arbitrary endpoints.
3. **Uploads and launches** the runner with per-session credentials passed as environment variables.
Long-polling works behind any NAT or firewall. For push-based dispatch, use a webhook instead.
### Webhook variant
Subscribe to `session.status_run_started` [webhooks](https://platform.claude.com/docs/en/managed-agents/webhooks), then drain the work queue on each delivery. The sandbox lifecycle logic is the same — only the trigger changes.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import Anthropic from "@anthropic-ai/sdk"
const client = new Anthropic({ authToken: environmentKey })
export async function handleWebhook(req: Request): Promise {
const body = await req.text()
const event = client.beta.webhooks.unwrap(body, {
headers: Object.fromEntries(req.headers),
})
if (event.data.type !== "session.status_run_started") {
return Response.json({ status: "ignored" })
}
for await (const work of client.beta.environments.work.poller({
environmentId,
environmentKey,
blockMs: null,
drain: true,
autoStop: false,
})) {
await handleWork(work)
}
return Response.json({ status: "ok" })
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
async def handle_webhook(raw: bytes, headers: dict) -> dict:
event = client.beta.webhooks.unwrap(raw.decode(), headers=headers)
if event.data.type != "session.status_run_started":
return {"status": "ignored"}
async for work in client.beta.environments.work.poller(
environment_id=ENVIRONMENT_ID,
environment_key=ENVIRONMENT_KEY,
block_ms=None,
drain=True,
auto_stop=False,
):
await handle_work(work)
return {"status": "ok"}
```
Keep `ANTHROPIC_ENVIRONMENT_KEY` on the orchestrator host only. The orchestrator passes it into each sandbox as an environment variable scoped to the runner process. Never set `ANTHROPIC_API_KEY` inside the sandbox - that would expose an organization-scoped credential to agent tool calls.
To reduce costs, pause sandboxes that sit idle between turns. Superserve's `pause()` checkpoints the full VM state - memory, processes, and filesystem - at zero compute cost. The next work item for that session calls `resume()` and picks up exactly where it left off, in under 50ms.
## Start a session
Create a session, send a message, and stream the response.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import Anthropic from "@anthropic-ai/sdk"
const client = new Anthropic()
const session = await client.beta.sessions.create({
agent: process.env.ANTHROPIC_AGENT_ID!,
environment_id: process.env.ANTHROPIC_ENVIRONMENT_ID!,
})
await client.beta.sessions.events.send(session.id, {
events: [
{
type: "user.message",
content: [
{
type: "text",
text: "Summarize every .py file in the workspace.",
},
],
},
],
})
const stream = await client.beta.sessions.events.stream(session.id)
for await (const event of stream) {
if (event.type === "agent.message") {
for (const block of event.content) {
if ("text" in block) process.stdout.write(block.text)
}
}
if (
event.type === "session.status_idle" &&
event.stop_reason?.type === "end_turn"
) break
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import os
import anthropic
client = anthropic.Anthropic()
session = client.beta.sessions.create(
agent=os.environ["ANTHROPIC_AGENT_ID"],
environment_id=os.environ["ANTHROPIC_ENVIRONMENT_ID"],
)
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.message",
"content": [
{
"type": "text",
"text": "Summarize every .py file in the workspace.",
}
],
}
],
)
with client.beta.sessions.events.stream(session.id) as stream:
for event in stream:
if event.type == "agent.message":
for block in event.content:
if hasattr(block, "text"):
print(block.text, end="")
if (
event.type == "session.status_idle"
and getattr(event.stop_reason, "type", None) == "end_turn"
):
break
```
See [Events and streaming](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) for the full event vocabulary.
### Pre-prepared sandboxes
For sessions that need custom data loaded before the agent starts - a cloned repo, a dataset, customer-specific files - create and seed a sandbox ahead of time, then pass its ID via session metadata. The orchestrator detects the metadata and attaches the existing sandbox instead of creating a new one.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
import Anthropic from "@anthropic-ai/sdk"
const sandbox = await Sandbox.create({
name: "prepped-session",
fromTemplate: "claude-managed-agent",
})
await sandbox.files.write("/workspace/data.csv", dataContents)
await sandbox.commands.run("git clone https://github.com/org/repo /workspace/repo")
const session = await client.beta.sessions.create({
agent: agentId,
environment_id: environmentId,
metadata: { "superserve.sandbox_id": sandbox.id },
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(
name="prepped-session",
from_template="claude-managed-agent",
)
sandbox.files.write("/workspace/data.csv", data_contents)
sandbox.commands.run("git clone https://github.com/org/repo /workspace/repo")
session = client.beta.sessions.create(
agent=agent_id,
environment_id=environment_id,
metadata={"superserve.sandbox_id": sandbox.id},
)
```
The orchestrator reads `superserve.sandbox_id` from `work.data.metadata` and connects to that sandbox instead of creating one from scratch.
[Memory](https://platform.claude.com/docs/en/managed-agents/memory) is not yet supported with self-hosted sandboxes.
## What you get
* **Firecracker microVM isolation.** Each session runs in its own lightweight VM - not a shared container. Process tree, filesystem, and network namespace are fully isolated. A compromised sandbox cannot affect others.
* **Fast startup (`<50ms`).** Sandboxes are ready instantly. No image pull, no package install, no boot sequence at session time.
* **Pause and resume.** Checkpoint the full sandbox state between turns. Resume picks up exactly where it left off - running processes, open files, in-memory state. Pay for compute only when the agent is actively working.
* **Per-sandbox network isolation.** Each sandbox gets its own network namespace with CIDR and domain-based egress filtering. Lock the agent down to `api.anthropic.com` only, or open access to specific internal services.
* **The sandbox is yours.** Beyond running the agent's tools, you control the full VM. Pre-install packages in the template, mount data via the files API, stream command output for observability. The tool runner is one process in a VM you own.
* **One line to switch.** Point `environment_id` at a cloud environment and the same application code works unchanged. The only Superserve-specific piece is the orchestrator.
## Recipes
Ready-to-run examples that show specific use cases. Each recipe includes a working orchestrator, runner, and setup scripts in both Python and TypeScript.
Agent researches topics across multi-turn sessions. Sandbox pauses between turns — you pay only for active compute. Notes, citations, and drafts persist in `/workspace`.
Conversational coding assistant with durable state. Clone once, install once — packages, repos, and build artifacts survive across sessions.
Agent fans out N sandboxes in parallel — one per variant — runs benchmarks concurrently, and synthesizes a comparison report. Total time equals the slowest variant.
## See also
Anthropic's full documentation for agents, sessions, tools, and events.
Anthropic's self-hosted reference — worker config, monitoring, and operations.
Customize your sandbox template with build steps, start commands, and resource limits.
Lock down sandbox egress with allow and deny lists.
Working orchestrator, runner, and setup scripts you can clone and run.
# MCP Server
Source: https://docs.superserve.ai/integrations/mcp
Create, run, and manage Superserve sandboxes from any MCP client.
The Superserve **MCP server** (`@superserve/mcp`) exposes sandbox primitives as [Model Context Protocol](https://modelcontextprotocol.io) tools, so any MCP-capable client — Claude, Cursor, VS Code, Windsurf, Codex — can create sandboxes, run commands, read and write files, build templates, broker secrets, and control network access in an isolated Firecracker microVM.
Run it two ways: **locally** over stdio via `npx`, or against the **hosted** endpoint at `https://mcp.superserve.ai` with no local install. Both authenticate with your `SUPERSERVE_API_KEY` and target a sandbox per call by ID. It's a thin wrapper over the [TypeScript SDK](/sdk-reference/sandbox), so the per-sandbox data-plane token never reaches the model.
## Quickstart
Add the server to your client (see [Install](#install)), then ask the agent to *"create a sandbox and run `python --version` in it."* The agent calls `sandbox_create`, then `sandbox_exec`, and reports the result — no code from you.
You need a Superserve API key — create one on the [API key](/api-key) page. There's no global install; `npx` fetches the server on first use.
## Install
Set `SUPERSERVE_API_KEY` in the server's `env` — MCP clients do not inherit it
from your shell. Prefer a secret-input prompt over pasting the raw key where
your client supports it (see VS Code below).
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
claude mcp add superserve \
--env SUPERSERVE_API_KEY=ss_live_xxxxxxxxxxxxxxxx \
-- npx -y @superserve/mcp
```
Add to `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/`):
```json theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
{
"mcpServers": {
"superserve": {
"command": "npx",
"args": ["-y", "@superserve/mcp"],
"env": { "SUPERSERVE_API_KEY": "ss_live_xxxxxxxxxxxxxxxx" }
}
}
}
```
Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
```json theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
{
"mcpServers": {
"superserve": {
"command": "npx",
"args": ["-y", "@superserve/mcp"],
"env": { "SUPERSERVE_API_KEY": "ss_live_xxxxxxxxxxxxxxxx" }
}
}
}
```
Add to `.vscode/mcp.json`. The `inputs` block prompts for the key instead of storing it in plain text:
```json theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
{
"inputs": [
{
"id": "superserve-key",
"type": "promptString",
"description": "Superserve API key",
"password": true
}
],
"servers": {
"superserve": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@superserve/mcp"],
"env": { "SUPERSERVE_API_KEY": "${input:superserve-key}" }
}
}
}
```
Add to `~/.codeium/windsurf/mcp_config.json`:
```json theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
{
"mcpServers": {
"superserve": {
"command": "npx",
"args": ["-y", "@superserve/mcp"],
"env": { "SUPERSERVE_API_KEY": "ss_live_xxxxxxxxxxxxxxxx" }
}
}
}
```
Add to `~/.codex/config.toml`. `env_vars` forwards `SUPERSERVE_API_KEY` from your environment, so the raw key isn't stored in the config file (export it in your shell first). Codex also reads the server's `instructions` for cross-tool workflow guidance.
```toml theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
[mcp_servers.superserve]
command = "npx"
args = ["-y", "@superserve/mcp"]
env_vars = ["SUPERSERVE_API_KEY"]
```
For the [hosted](#hosted-remote) endpoint, use `url = "https://mcp.superserve.ai"` with `bearer_token_env_var = "SUPERSERVE_API_KEY"`.
## Hosted (remote)
Don't want to run anything locally? The hosted endpoint at `https://mcp.superserve.ai` speaks [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) — no `npx`, no Node. Send your Superserve API key as a **bearer token**. The endpoint is stateless and account-scoped (your key already maps to your team), and the per-sandbox data-plane token never leaves the server.
Bearer auth works in any client that lets you set a request header — Claude
Code, Cursor, VS Code, and the Anthropic Messages API connector. Claude.ai,
Claude Desktop's Custom Connector UI, and ChatGPT developer mode don't offer a
static-bearer / custom-header field (they expect OAuth), which the hosted
endpoint doesn't support yet — use the [local](#install) install there.
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
claude mcp add --transport http superserve https://mcp.superserve.ai \
--header "Authorization: Bearer ss_live_xxxxxxxxxxxxxxxx"
```
Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
```json theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
{
"mcpServers": {
"superserve": {
"url": "https://mcp.superserve.ai",
"headers": { "Authorization": "Bearer ss_live_xxxxxxxxxxxxxxxx" }
}
}
}
```
Add to `.vscode/mcp.json`. The `inputs` block prompts for the key instead of storing it in plain text:
```json theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
{
"inputs": [
{
"id": "superserve-key",
"type": "promptString",
"description": "Superserve API key",
"password": true
}
],
"servers": {
"superserve": {
"type": "http",
"url": "https://mcp.superserve.ai",
"headers": { "Authorization": "Bearer ${input:superserve-key}" }
}
}
}
```
Pass it as a connector in an [Anthropic Messages API](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector) request:
```json theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
{
"mcp_servers": [
{
"type": "url",
"name": "superserve",
"url": "https://mcp.superserve.ai",
"authorization_token": "ss_live_xxxxxxxxxxxxxxxx"
}
]
}
```
Same tools and behavior as the local server — the only difference is transport and that the key travels as a bearer header instead of an `env` var.
## Tools
| Tool | What it does |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `sandbox_create` | Create a new sandbox; returns its `id`. Active and ready immediately. Accepts `secrets` and egress rules. |
| `sandbox_update` | Change a sandbox's metadata or egress (`allow_out`/`deny_out`) rules after creation. |
| `sandbox_list` | List your sandboxes (active and paused), filterable by metadata. |
| `sandbox_info` | Get one sandbox's status, resources, metadata, network rules, and secret bindings. Read-only. |
| `sandbox_exec` | Run a shell command; returns stdout, stderr, exit code. Auto-resumes a paused sandbox. |
| `sandbox_files_read` | Read a file (UTF-8 text, or base64 for binary). |
| `sandbox_files_write` | Create or overwrite a file. Parent directories are created automatically. |
| `sandbox_files_list` | List a directory's entries (name, type, size, modification time). |
| `sandbox_files_download_dir` | Download a directory as a base64 ZIP (symlinks skipped). Capped at 10 MiB; larger → SDK/CLI. |
| `sandbox_pause` | Pause a sandbox; state is preserved. |
| `sandbox_resume` | Resume a paused sandbox (usually unnecessary — exec auto-resumes). |
| `sandbox_kill` | Permanently delete a sandbox. |
| `sandbox_preview_url` | Build the public URL for a listening port (unauthenticated — anything on that port is internet-exposed). |
| `sandbox_network_log` | Audit a sandbox's outbound connections (host, verdict, bytes). Auto-resumes a paused sandbox. |
| `sandbox_template_list` | List the templates (base images) your team can launch from. |
| `sandbox_template_create` | Build a custom template with a specific vCPU/memory/disk shape or preinstalled software (async — poll for ready). |
| `secret_list` | List bindable team secrets (metadata only — never values). |
| `sandbox_attach_secret` | Bind a stored secret to a running sandbox under an env var. |
| `sandbox_detach_secret` | Remove a secret binding from a sandbox. |
Most tools take a `sandbox_id`; the exceptions are `sandbox_create`, `sandbox_list`, `sandbox_template_list`, `sandbox_template_create`, and `secret_list`. Start with one of those to get an ID, then thread it into later calls. Read-only tools (`sandbox_list`, `sandbox_info`, `sandbox_files_read`, `sandbox_files_list`, `sandbox_files_download_dir`, `sandbox_preview_url`, `sandbox_template_list`, `secret_list`) are annotated so clients can skip confirmation prompts; `sandbox_kill` is annotated destructive.
## Example
A typical agent flow for *"spin up a sandbox, write a Python script that prints the first primes, and run it"*:
```text theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox_create { name: "primes" }
→ { id: "a1b2c3…", name: "primes", status: "active" }
sandbox_files_write { sandbox_id: "a1b2c3…", path: "/app/primes.py", content: "…" }
→ { path: "/app/primes.py", bytes: 142 }
sandbox_exec { sandbox_id: "a1b2c3…", command: "python /app/primes.py" }
→ { exit_code: 0, stdout: "2 3 5 7 11 13 17 19 23 29", stderr: "" }
```
When it's done, the agent can `sandbox_pause` (state preserved, cheaper to keep around) or `sandbox_kill` (permanent).
## Configuration
| Variable | Required | Description |
| --------------------- | -------- | ------------------------------------------------------------------------- |
| `SUPERSERVE_API_KEY` | Yes | Your Superserve API key (starts with `ss_live_`). |
| `SUPERSERVE_BASE_URL` | No | Override the control-plane URL (defaults to `https://api.superserve.ai`). |
## Behavior and limits
* **Auto-resume.** `sandbox_exec` and the file tools transparently resume a paused sandbox, so agents never need to call `sandbox_resume` first. `sandbox_resume` exists only to warm a sandbox explicitly.
* **Output is capped for context.** `sandbox_exec` truncates stdout and stderr to 32 KiB each — a truncated result sets `truncated: true` and reports the original byte length. `sandbox_files_read` **rejects** files larger than 1 MiB (it does not return partial content); the error tells you to read a slice with `sandbox_exec` (e.g. `head -c`) or download the whole file with the SDK/CLI. `sandbox_files_write` inline content is capped at 8 MiB.
* **Default command timeout is 60s**, capped at 10 minutes. Override it per call with `timeout_ms`.
* **Egress is controllable.** `allow_out` (domain patterns or CIDRs) adds allowed destinations; `deny_out` (CIDRs only) blocks them. `allow_out` alone does **not** lock a sandbox down — for a strict allowlist, combine it with `deny_out: ["0.0.0.0/0"]` (deny all, then allow the listed destinations). Set these on `sandbox_create` or `sandbox_update`, and audit what a sandbox actually reached with `sandbox_network_log`.
* **Errors are actionable.** A failed tool call returns a short message telling the agent what to do next — e.g. *"Sandbox quota reached. Pause or kill a sandbox, or retry later."* — rather than a raw stack trace, so the agent can self-correct.
## Secrets, templates, and ports
**Secrets.** Don't pass credentials as plaintext `env_vars`. Instead:
1. Create the secret once with the [TypeScript SDK](/sdk-reference/sandbox) (`Secret.create()`) or the [console](/secrets/overview) — the raw value never travels through the agent or the MCP server, so **secret creation is intentionally not an MCP tool**.
2. Discover bindable secrets with `secret_list` (metadata only — values never leave the platform).
3. Bind at creation — `secrets: { ANTHROPIC_API_KEY: "anthropic-prod" }` on `sandbox_create` — or later with `sandbox_attach_secret` / `sandbox_detach_secret`.
The sandbox sees a proxy token; the platform swaps in the real credential only for outbound requests to the secret's allowed hosts.
**Templates.** A sandbox inherits its vCPU/memory/disk from its template and can't override them at `sandbox_create` time. To get a specific shape (say, a 4 vCPU sandbox) or preinstalled software, build a template with `sandbox_template_create`, then poll `sandbox_template_list` until its `status` is `ready` before passing it as `from_template`.
**Ports.** Start a server in the sandbox (`sandbox_exec`, e.g. `python3 -m http.server 8000`), then call `sandbox_preview_url` to get its public URL. Any process bound to a port is reachable at `https://{port}-{id}.sandbox.superserve.ai` with **no authentication** — only expose ports you intend to be public.
## Not yet in the MCP surface
The MCP server covers the common agent loop; the table above is the complete v1 tool set. A few SDK capabilities aren't exposed yet — reach for the [TypeScript SDK](/sdk-reference/sandbox) directly for:
* **Secret creation** — `Secret.create()` (the MCP server only *binds* existing secrets).
* **Streaming and interactive commands** — streaming `run()` callbacks and `commands.spawn` (stdin, signals, long-running processes).
* **Large or streaming transfers** — directory download is supported up to 10 MiB via `sandbox_files_download_dir`; beyond that (and for archive/streaming uploads or single files past the 1 MiB read / 8 MiB inline-write caps), use the SDK/CLI (`files.downloadDir`, streaming upload).
* **Billing and provider discovery** — usage data and `Provider.list()` for secret-provider setup.
These are tracked as follow-ups.
## How it works
The server wraps the [TypeScript SDK](/sdk-reference/sandbox) and only ever holds your control-plane `SUPERSERVE_API_KEY`. Each tool call connects to the target sandbox by ID; the SDK manages the per-sandbox **data-plane access token** internally and rotates it on resume, so it's never exposed to the model or returned in tool output. Tools are stateless — there's no hidden "current sandbox" — which keeps behavior predictable across multi-turn and parallel tool calls.
## Troubleshooting
* **Tools don't appear, or the server fails to start.** The API key is almost always the cause — MCP clients do **not** inherit environment variables from your shell. Set `SUPERSERVE_API_KEY` in the server's `env` block (see [Install](#install)), not just in your terminal.
* **`Authentication failed`.** The key is missing or invalid. Production keys start with `ss_live_`; create one on the [API key](/api-key) page.
* **First call is slow.** `npx` downloads the package on first use and caches it; later starts are fast.
* **Requires Node 18+.** The local server runs on Node via `npx`. (The [hosted](#hosted-remote) endpoint has no local runtime requirement.)
* **`401 Unauthorized` from the hosted endpoint.** The bearer token is missing or isn't a valid `ss_live_` key. Send it as `Authorization: Bearer ss_live_…` (see [Hosted](#hosted-remote)).
## Related
Pause, resume, and delete sandboxes.
Exec, streaming, cwd, env, and timeouts.
Broker provider keys without exposing them to the sandbox.
The library the MCP server wraps.
# Hermes
Source: https://docs.superserve.ai/integrations/personal-agents/hermes
Run the Hermes agent in a Superserve sandbox.
Hermes is NousResearch's open-source personal AI agent. Run it in a Superserve sandbox so its tools and shell commands stay isolated from your machine.
## Via the console
1. On the [Secrets page](https://console.superserve.ai/secrets?utm_source=docs\&utm_medium=link), create a secret for your model provider (e.g. **Anthropic**) and paste your API key — you only do this once, and the key never enters the sandbox.
2. Go to [console.superserve.ai](https://console.superserve.ai?utm_source=docs\&utm_medium=link) and click **Create sandbox**.
3. Pick the **`superserve/hermes`** template (Hermes and `tmux` preinstalled).
4. Under **Advanced Options → Secrets**, bind that secret to your provider's key env var (e.g. `ANTHROPIC_API_KEY`).
5. Create the sandbox, open its **Terminal**, and run `hermes`.
## Via the SDK
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
npm install @superserve/sdk
export SUPERSERVE_API_KEY=ss_live_...
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox, Secret } from "@superserve/sdk"
// One-time: store your key as a secret. It never enters the sandbox.
await Secret.create({
name: "anthropic-key",
value: "sk-ant-...",
provider: "anthropic",
})
const sandbox = await Sandbox.create({
name: "hermes",
fromTemplate: "superserve/hermes",
secrets: { ANTHROPIC_API_KEY: "anthropic-key" },
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox, Secret
# One-time: store your key as a secret. It never enters the sandbox.
Secret.create(name="anthropic-key", value="sk-ant-...", provider="anthropic")
sandbox = Sandbox.create(
name="hermes",
from_template="superserve/hermes",
secrets={"ANTHROPIC_API_KEY": "anthropic-key"},
)
```
Hermes reads `ANTHROPIC_API_KEY` as usual, but its value is a [stand-in token](/secrets/overview) — the real key is attached only on requests to the provider, never exposed to the agent.
Open the sandbox in the [console](https://console.superserve.ai?utm_source=docs\&utm_medium=link), launch the terminal, and run `hermes`.
## Keep it running
Start Hermes inside `tmux` so it survives closing the terminal or browser tab:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
tmux new -d -s hermes 'hermes gateway'
```
Press `Ctrl+b` then `d` to detach. Reattach later with:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
tmux attach -t hermes
```
## Persist sessions
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.pause()
await sandbox.resume()
```
# OpenClaw
Source: https://docs.superserve.ai/integrations/personal-agents/openclaw
Run OpenClaw in a Superserve sandbox.
OpenClaw is an open-source personal AI assistant. Run it in a Superserve sandbox so its actions and shell commands stay isolated from your machine.
## Via the console
1. On the [Secrets page](https://console.superserve.ai/secrets?utm_source=docs\&utm_medium=link), create a secret for your model provider (e.g. **Anthropic**) and paste your API key — you only do this once, and the key never enters the sandbox.
2. Go to [console.superserve.ai](https://console.superserve.ai?utm_source=docs\&utm_medium=link) and click **Create sandbox**.
3. Pick the **`superserve/openclaw`** template (OpenClaw and `tmux` preinstalled).
4. Under **Advanced Options → Secrets**, bind that secret to your provider's key env var (e.g. `ANTHROPIC_API_KEY`).
5. Create the sandbox, open its **Terminal**, and run `openclaw onboard` to start the setup wizard.
## Via the SDK
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
npm install @superserve/sdk
export SUPERSERVE_API_KEY=ss_live_...
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox, Secret } from "@superserve/sdk"
// One-time: store your key as a secret. It never enters the sandbox.
await Secret.create({
name: "anthropic-key",
value: "sk-ant-...",
provider: "anthropic",
})
const sandbox = await Sandbox.create({
name: "openclaw",
fromTemplate: "superserve/openclaw",
secrets: { ANTHROPIC_API_KEY: "anthropic-key" },
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox, Secret
# One-time: store your key as a secret. It never enters the sandbox.
Secret.create(name="anthropic-key", value="sk-ant-...", provider="anthropic")
sandbox = Sandbox.create(
name="openclaw",
from_template="superserve/openclaw",
secrets={"ANTHROPIC_API_KEY": "anthropic-key"},
)
```
OpenClaw reads `ANTHROPIC_API_KEY` as usual, but its value is a [stand-in token](/secrets/overview) — the real key is attached only on requests to the provider, never exposed to the agent.
Open the sandbox in the [console](https://console.superserve.ai?utm_source=docs\&utm_medium=link), launch the terminal, and run `openclaw onboard` to start the setup wizard.
## Keep it running
Start OpenClaw inside `tmux` so it survives closing the terminal or browser tab:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
tmux new -s openclaw
openclaw gateway run
```
Press `Ctrl+b` then `d` to detach. Reattach later with:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
tmux attach -t openclaw
```
## Persist sessions
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.pause()
await sandbox.resume()
```
# Introduction
Source: https://docs.superserve.ai/introduction
Superserve provides sandbox infrastructure to run code in isolated cloud environments powered by Firecracker MicroVMs.
Each sandbox starts from a [template](/templates/overview) — a reusable base image with your dependencies baked in.
Use the menu at the top right of any page to add our docs as an MCP server
in Claude Code, Cursor, or VS Code — giving your agent direct access to
search and read Superserve documentation.
## What you can do
* **Create sandboxes** - spin up isolated VMs in seconds, ready to run commands immediately
* **Execute commands** - run shell commands and stream output
* **Read and write files** - transfer files to and from the sandbox filesystem
* **Pause and resume** - checkpoint sandbox state to disk and restore it later
* **Custom templates** - pre-bake team dependencies into base images and launch from them
* **Secrets** - give a sandbox an API key without the real value ever entering it
* **Network controls** - per-sandbox egress allow/deny rules, plus a log of every connection a sandbox made
## Next steps
Create your first sandbox and run your first command.
Authenticate the SDK with your Superserve account.
Every method, option, and type for the TypeScript and Python SDKs.
Guides to integrate with agents, harnesses, filesystems, and more.
# Quickstart
Source: https://docs.superserve.ai/quickstart
Create your first sandbox, run a command, and read a file.
Install the SDK, set your API key, and create a sandbox.
## 1. Install the SDK
```bash npm theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
npm install @superserve/sdk
```
```bash pip theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
pip install superserve
```
## 2. Set your API key
Grab your API key from the [Superserve Console](https://console.superserve.ai?utm_source=docs\&utm_medium=link) and export it.
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
export SUPERSERVE_API_KEY=ss_live_...
```
See [Get an API key](/api-key) for more options.
## 3. Create a sandbox and run a command
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({ name: "quickstart" })
const result = await sandbox.commands.run("echo 'Hello from Superserve!'")
console.log(result.stdout)
await sandbox.files.write("/tmp/hello.txt", "Hello, world!")
const content = await sandbox.files.readText("/tmp/hello.txt")
console.log(content)
await sandbox.kill()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(name="quickstart")
result = sandbox.commands.run("echo 'Hello from Superserve!'")
print(result.stdout)
sandbox.files.write("/tmp/hello.txt", "Hello, world!")
content = sandbox.files.read_text("/tmp/hello.txt")
print(content)
sandbox.kill()
```
## Where to go next
Sync, streaming, cwd, env, timeouts.
Checkpoint state to save money between runs.
Tag and query sandboxes at scale.
Restrict egress to specific domains or CIDRs.
# Connect to an existing sandbox
Source: https://docs.superserve.ai/sandbox/connect
Reconnect to a sandbox you created earlier using its ID, and list sandboxes on your team.
A sandbox outlives the process that created it. Store `sandbox.id` somewhere durable (a database, a queue message), and reconnect later with `Sandbox.connect()`.
* `Sandbox.connect(id)` - returns a live `sandbox` with `commands` and `files` ready to use. If the sandbox is `paused`, it's auto-resumed before returning.
* `sandbox.getInfo()` - fetch the current status and metadata for a sandbox you already have (read-only, no state change)
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.connect("7a3f2b8c-1234-...")
await sandbox.commands.run("ls /app")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.connect("7a3f2b8c-1234-...")
sandbox.commands.run("ls /app")
```
## Find sandboxes with `list`
List all sandboxes on the team, or filter by metadata. Filters combine with AND.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const all = await Sandbox.list()
const prod = await Sandbox.list({
metadata: { env: "prod", owner: "agent-7" },
})
for (const info of prod) {
console.log(info.id, info.name, info.status)
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
all_boxes = Sandbox.list()
prod = Sandbox.list(metadata={"env": "prod", "owner": "agent-7"})
for info in prod:
print(info.id, info.name, info.status.value)
```
`list()` returns an array of [`SandboxInfo`](/sdk-reference/sandbox#sandboxinfo) - no live `sandbox` is constructed. Call `Sandbox.connect(id)` when you need one.
# Create a sandbox
Source: https://docs.superserve.ai/sandbox/create
Spin up an isolated Firecracker MicroVM in seconds, ready to run commands immediately.
`Sandbox.create()` boots a fresh VM and returns when it's ready to use. There's no readiness check or polling - the returned instance is already `active`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({ name: "data-analyzer" })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(name="data-analyzer")
```
## With options
Attach metadata tags, inject environment variables, cap the active lifetime, or lock down network egress at creation time.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({
name: "data-analyzer",
timeoutSeconds: 3600,
metadata: { env: "prod", owner: "agent-7" },
envVars: { LOG_LEVEL: "debug" },
secrets: { OPENAI_API_KEY: "openai-prod" },
network: {
allowOut: ["api.openai.com", "*.github.com"],
denyOut: ["0.0.0.0/0"],
},
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox, NetworkConfig
sandbox = Sandbox.create(
name="data-analyzer",
timeout_seconds=3600,
metadata={"env": "prod", "owner": "agent-7"},
env_vars={"LOG_LEVEL": "debug"},
secrets={"OPENAI_API_KEY": "openai-prod"},
network=NetworkConfig(
allow_out=["api.openai.com", "*.github.com"],
deny_out=["0.0.0.0/0"],
),
)
```
Use **`secrets`** for credentials, not `envVars`. A secret binds an env var to a stored credential and attaches the real value to outbound requests — the real credential never enters the sandbox. See [Secrets](/secrets/overview).
## Common options
| Option | Type | Description |
| ------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `name` | `string` | **Required.** Human-readable sandbox name. |
| `timeoutSeconds` / `timeout_seconds` | `number` | Auto-pause after this many seconds of active time (per session; re-armed on resume). See [Lifecycle](/sandbox/lifecycle#auto-pause). |
| `autoDeleteSeconds` / `auto_delete_seconds` | `number` | Delete the sandbox once continuously paused for this many seconds. See [Lifecycle](/sandbox/lifecycle#auto-delete). |
| `metadata` | `Record` | String tags. See [Metadata](/sandbox/metadata). |
| `envVars` / `env_vars` | `Record` | Env vars applied to every process. See [Environment variables](/sandbox/environment-variables). |
| `network` | `NetworkConfig` | Egress allow/deny rules. See [Networking](/sandbox/networking). |
See the [Sandbox reference](/sdk-reference/sandbox#sandbox-create) for every option including `apiKey`, `baseUrl`, and `signal`.
## Create from a template
Boot a sandbox from a system or team template via `fromTemplate` (name, UUID, or `Template` instance).
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const sandbox = await Sandbox.create({
name: "my-sandbox",
fromTemplate: "superserve/python-3.11", // name
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox = Sandbox.create(
name="my-sandbox",
from_template="superserve/python-3.11",
)
```
The sandbox's vCPU, memory, and disk size are inherited from the template; they cannot be overridden. See [Templates overview](/templates/overview) for how to build your own.
Next: [run commands](/commands/overview), [write files](/filesystem/read-write), or [pause the sandbox](/sandbox/lifecycle) to save costs between runs.
# Environment variables
Source: https://docs.superserve.ai/sandbox/environment-variables
Inject environment variables at sandbox creation - applied to every process spawned inside the VM.
Env vars set at creation time are applied to **every** process the sandbox runs: `commands.run()`, shells, and anything those spawn. They persist across `pause()` / `resume()` cycles.
Env var values are delivered **in plain text** — any process in the sandbox can read them. For API keys and other credentials, use [secrets](/secrets/overview) instead: the sandbox only ever holds a stand-in token, and the real value is attached to outbound requests.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const sandbox = await Sandbox.create({
name: "data-analyzer",
envVars: {
OPENAI_API_KEY: "sk-...",
DATABASE_URL: "postgres://...",
NODE_ENV: "production",
},
})
const result = await sandbox.commands.run("echo $NODE_ENV")
console.log(result.stdout) // "production\n"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox = Sandbox.create(
name="data-analyzer",
env_vars={
"OPENAI_API_KEY": "sk-...",
"DATABASE_URL": "postgres://...",
"NODE_ENV": "production",
},
)
result = sandbox.commands.run("echo $NODE_ENV")
print(result.stdout) # "production\n"
```
## Per-command overrides
The `env` option on `commands.run()` adds to or overrides the sandbox-wide env for a single command.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const result = await sandbox.commands.run("echo $NODE_ENV", {
env: { NODE_ENV: "staging" },
})
console.log(result.stdout) // "staging\n"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
result = sandbox.commands.run(
"echo $NODE_ENV",
env={"NODE_ENV": "staging"},
)
print(result.stdout) # "staging\n"
```
Values pass through unmodified - no shell expansion or interpolation. Escape `$` and special characters yourself if needed.
# Pause, resume, and delete
Source: https://docs.superserve.ai/sandbox/lifecycle
Manage sandbox state - pause to save compute costs, resume on demand, delete when you're done.
A sandbox transitions between two long-lived states:
* **`active`** - VM is running, processes executing, compute billed
* **`paused`** - VM paused, state persisted to disk, no compute billed
You'll briefly see **`resuming`** while a paused sandbox is being restored to `active` - retry shortly. **`failed`** indicates the sandbox couldn't boot or resume; the entry remains until you delete it. Deletion removes the sandbox entirely; further API calls return `404`.
```
active ↔ paused → deleted
```
## Pause
`pause()` checkpoints the full VM state (memory, processes, filesystem) to disk and stops billing for compute.
```typescript TypeScript {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({ name: "long-job" })
await sandbox.pause()
```
```python Python {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(name="long-job")
sandbox.pause()
```
## Resume
`resume()` restores a `paused` sandbox you already hold a reference to. Processes pick up where they left off.
```typescript TypeScript {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({ name: "long-job" })
await sandbox.pause()
await sandbox.resume()
await sandbox.commands.run("ls /tmp")
```
```python Python {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(name="long-job")
sandbox.pause()
sandbox.resume()
sandbox.commands.run("ls /tmp")
```
A paused sandbox is auto-resumed by both `Sandbox.connect()` and `commands.run()`.
After a resume, keep using the same `sandbox` — your `commands` and `files` calls keep working with no changes on your side.
## Kill
`kill()` deletes the sandbox and all its resources. It's idempotent - deleting an already-deleted sandbox is a no-op that swallows the `404`.
```typescript TypeScript {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({ name: "one-shot" })
await sandbox.kill()
```
```python Python {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(name="one-shot")
sandbox.kill()
```
## Kill without an instance
In serverless contexts you often don't have the `sandbox` - just the ID. Use `killById` / `kill_by_id` to delete it anyway.
```typescript TypeScript {3} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
await Sandbox.killById("7a3f2b8c-1234-...")
```
```python Python {3} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
Sandbox.kill_by_id("7a3f2b8c-1234-...")
```
## Auto-pause
Set `timeoutSeconds` to auto-pause a sandbox after it has been active for that long. Pausing checkpoints its state and stops compute billing, so a sandbox you forget about won't keep running and billing. It isn't deleted; the next `commands.run()`, file operation, or `connect()` resumes it where it left off.
```typescript TypeScript {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({
name: "worker",
timeoutSeconds: 3600, // auto-pause after 1h of active time
})
```
```python Python {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(
name="worker",
timeout_seconds=3600, # auto-pause after 1h of active time
)
```
The timeout is scoped to the current active session:
* It's a cap on active time, not idle time. Running work doesn't reset it, so a task still going when the window elapses is paused mid-run. Size it for your longest active session.
* Each resume starts a fresh window.
* The sandbox is paused, not deleted. Resume restores full memory and filesystem.
* Leave it unset (the default) to disable auto-pause; the sandbox stays active until you pause or kill it.
Set or clear it on an existing sandbox:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.update({ timeoutSeconds: 900 }) // auto-pause after 15m active
await sandbox.update({ timeoutSeconds: null }) // disable auto-pause
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.update(timeout_seconds=900) # auto-pause after 15m active
sandbox.update(timeout_seconds=None) # disable auto-pause
```
## Auto-delete
By default a paused sandbox is kept forever. Set `autoDeleteSeconds` to delete it once it has been continuously paused for that long. This suits short-lived or unattended sandboxes: scheduled jobs, one-off experiments, or agent sessions that never get an explicit `kill()`.
```typescript TypeScript {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({
name: "scratch-job",
autoDeleteSeconds: 3600, // delete after 1h of continuous pause
})
```
```python Python {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(
name="scratch-job",
auto_delete_seconds=3600, # delete after 1h of continuous pause
)
```
The countdown is tied to the paused state:
* Pause starts it and resume cancels it. Pausing again starts a fresh window, so a sandbox in use is never deleted.
* `0` deletes the sandbox as soon as it pauses.
* While paused, the sandbox's info includes `autoDeleteAt`, the exact deletion time.
* Maximum window: 30 days.
You can also set or clear the window on an existing sandbox:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
// Arm: delete after 1h of continuous pause. On an already-paused sandbox
// the countdown starts now, so you always get the full window.
await sandbox.update({ autoDeleteSeconds: 3600 })
// Disarm: keep the sandbox until explicitly killed.
await sandbox.update({ autoDeleteSeconds: null })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# Arm: delete after 1h of continuous pause. On an already-paused sandbox
# the countdown starts now, so you always get the full window.
sandbox.update(auto_delete_seconds=3600)
# Disarm: keep the sandbox until explicitly killed.
sandbox.update(auto_delete_seconds=None)
```
Pair `autoDeleteSeconds` with `timeoutSeconds`: the timeout pauses the sandbox, then the auto-delete window cleans it up. A sandbox that never pauses is never auto-deleted.
# Metadata tags
Source: https://docs.superserve.ai/sandbox/metadata
Attach string key-value tags to sandboxes, update them later, and filter sandboxes by tag.
Metadata is a flat `{string: string}` map attached to each sandbox. Use it to track the user, job, environment, or any other context you need to query later.
## Set metadata on create
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const sandbox = await Sandbox.create({
name: "data-analyzer",
metadata: {
env: "prod",
owner: "agent-7",
job_id: "job_01HE...",
},
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox = Sandbox.create(
name="data-analyzer",
metadata={
"env": "prod",
"owner": "agent-7",
"job_id": "job_01HE...",
},
)
```
## Update metadata
`update()` replaces the metadata map - pass the full set you want to keep.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.update({
metadata: { env: "staging", owner: "agent-7" },
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.update(metadata={"env": "staging", "owner": "agent-7"})
```
## Filter sandboxes by metadata
`Sandbox.list()` accepts a `metadata` filter. Multiple keys combine with AND - a sandbox must match every key-value pair to be returned.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const prodForAgent = await Sandbox.list({
metadata: { env: "prod", owner: "agent-7" },
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
prod_for_agent = Sandbox.list(
metadata={"env": "prod", "owner": "agent-7"},
)
```
The `sandbox.metadata` instance property is a snapshot taken at construction. Call `getInfo()` / `get_info()` to fetch the current metadata after an `update()`.
# Network log
Source: https://docs.superserve.ai/sandbox/network-log
See every outbound connection a sandbox made — allowed, blocked, or failed — and every secret-bearing request, in one timeline.
The network log answers a question you can't answer from inside an untrusted sandbox: **what did it actually reach on the network?** Every outbound connection passes through Superserve, so the log is recorded by the platform — code inside the sandbox can't see it or tamper with it.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const page = await sandbox.getNetworkLog({ limit: 50 })
for (const e of page.events) {
if (e.kind === "connection") {
console.log(e.ts, e.host ?? e.dstIp, e.verdict)
} else {
console.log(e.ts, e.method, e.host + e.path, "→", e.status)
}
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
page = sandbox.get_network_log(limit=50)
for e in page.events:
if e.kind == "connection":
print(e.ts, e.host or e.dst_ip, e.verdict)
else:
print(e.ts, e.method, f"{e.host}{e.path}", "→", e.status)
```
## Two kinds of rows
Each event has a `kind`:
* **`connection`** — a raw outbound connection. Carries the host (or `dstIp`), a `verdict`, and byte counts. This is every connection the sandbox opened, whether or not a secret was involved.
* **`request`** — a secret-bearing request (the sandbox used a [secret](/secrets/overview)). Carries `method`, `path`, `status`, and the `secretId` that was used.
Fields that don't apply to a row's kind are omitted.
### Verdicts
Every `connection` row has a verdict:
| Verdict | Meaning |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allowed` | Connected — `bytesSent` / `bytesRecv` are populated |
| `blocked` | Denied by one of your [network rules](/sandbox/networking), or because sandboxes are limited to the public internet — private and internal addresses are always blocked. `matchRule` names the rule that stopped it |
| `failed` | Allowed by policy, but the connection couldn't be established |
`matchRule` names the rule behind a `blocked` connection — a useful signal for catching when a sandbox tried to reach somewhere you didn't expect.
## Filtering
Narrow by verdict, or to a time window:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
// Everything that was blocked.
const blocked = await sandbox.getNetworkLog({ verdict: "blocked" })
// A specific window during an incident.
const window = await sandbox.getNetworkLog({
since: "2026-06-11T14:00:00Z",
before: "2026-06-11T15:00:00Z",
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
blocked = sandbox.get_network_log(verdict="blocked")
window = sandbox.get_network_log(
since="2026-06-11T14:00:00Z",
before="2026-06-11T15:00:00Z",
)
```
A `verdict` filter returns only `connection` rows — `request` rows have no verdict.
## Pagination
The log is cursor-paginated, newest first. Pass `nextCursor` as `before` while `hasMore` is true:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
let cursor: string | undefined
do {
const page = await sandbox.getNetworkLog({ before: cursor, limit: 100 })
for (const e of page.events) handle(e)
cursor = page.nextCursor
} while (cursor)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
cursor = None
while True:
page = sandbox.get_network_log(before=cursor, limit=100)
for e in page.events:
handle(e)
cursor = page.next_cursor
if not cursor:
break
```
## What the log does and doesn't capture
The log records what the **sandbox** sent. It does **not** record work a third party does on the sandbox's behalf — if an agent calls an LLM that itself runs a web search server-side, you'll see the one request to the LLM, not the searches it ran on its own infrastructure. Those packets never left your sandbox.
It covers HTTP and HTTPS egress. DNS resolution and raw non-HTTP sockets aren't in this log.
Pair the network log (what was reached) with [network rules](/sandbox/networking) (what's allowed) and [secrets](/secrets/overview) (credentials attached to outbound requests) for end-to-end control and visibility over a sandbox's network.
# Network rules
Source: https://docs.superserve.ai/sandbox/networking
Lock down sandbox egress to specific CIDRs or domains with allow and deny lists.
By default, a sandbox can reach any public IP on the internet. The platform always blocks egress to private, link-local, and loopback ranges regardless of your allow rules. Use `network` rules to narrow the allowlist further.
Rules control what a sandbox **may** reach. To see what it **actually** reached — every connection, allowed or blocked — use the [network log](/sandbox/network-log).
## Allow specific destinations
`allowOut` / `allow_out` accepts a mix of CIDRs and domain patterns. Combine it with `denyOut: ["0.0.0.0/0"]` to build a strict allowlist - deny everything, then add exceptions.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const sandbox = await Sandbox.create({
name: "restricted",
network: {
allowOut: ["api.openai.com", "*.github.com", "140.82.112.0/20"],
denyOut: ["0.0.0.0/0"],
},
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox, NetworkConfig
sandbox = Sandbox.create(
name="restricted",
network=NetworkConfig(
allow_out=["api.openai.com", "*.github.com", "140.82.112.0/20"],
deny_out=["0.0.0.0/0"],
),
)
```
## Rule format
| Field | Accepts | Notes |
| ------------------------ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allowOut` / `allow_out` | CIDRs + domains | Domains support wildcards (`*.example.com`). A wildcard matches subdomains at any depth but not the bare domain — `*.example.com` does not match `example.com`; list both if you need the apex. |
| `denyOut` / `deny_out` | CIDRs only | Use `0.0.0.0/0` to deny the entire internet and rely on `allowOut` for exceptions. |
Allow rules take precedence over deny rules when they overlap.
## Update rules on a running sandbox
Networking can be updated after creation - changes apply immediately to new connections.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.update({
network: {
allowOut: ["api.anthropic.com", "api.openai.com"],
denyOut: ["0.0.0.0/0"],
},
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.update(
network=NetworkConfig(
allow_out=["api.anthropic.com", "api.openai.com"],
deny_out=["0.0.0.0/0"],
),
)
```
Blocking `*.superserve.ai` will break the SDK's ability to communicate with the sandbox.
# Commands
Source: https://docs.superserve.ai/sdk-reference/commands
Run shell commands in a sandbox, one-shot or as an interactive session.
Run shell commands inside a sandbox via `sandbox.commands`. Running a command on a `paused` sandbox transparently resumes it and executes.
## `run` (synchronous)
Execute a command and wait for it to finish. Returns a [`CommandResult`](#commandresult).
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const result = await sandbox.commands.run("echo hello")
console.log(result.stdout) // "hello\n"
console.log(result.stderr) // ""
console.log(result.exitCode) // 0
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
result = sandbox.commands.run("echo hello")
print(result.stdout) # "hello\n"
print(result.stderr) # ""
print(result.exit_code) # 0
```
## `run` (streaming)
Pass `onStdout` / `on_stdout` and/or `onStderr` / `on_stderr` callbacks. Output is delivered over Server-Sent Events and flushed to your callback as it arrives. The final `result` still contains the full concatenated output.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const result = await sandbox.commands.run("npm run build", {
onStdout: (data) => process.stdout.write(data),
onStderr: (data) => process.stderr.write(data),
timeoutMs: 300_000,
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import sys
result = sandbox.commands.run(
"npm run build",
on_stdout=lambda data: sys.stdout.write(data),
on_stderr=lambda data: sys.stderr.write(data),
timeout_seconds=300,
)
```
Streaming uses an *idle* timeout: the timer resets on every chunk. A command that keeps producing output never trips it, however long it runs. A command that goes silent still has to finish before the timeout.
## `spawn` (interactive session)
`spawn()` hands back a [`CommandSession`](#commandsession) while the process is still running. Stream its output with callbacks, write to `stdin`, signal it, and call `wait()` for the result. Output comes over a WebSocket. The [sessions guide](/commands/sessions) walks through it.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const session = await sandbox.commands.spawn("python -i", {
onStdout: (data) => process.stdout.write(data),
})
session.stdin.write("print(2 + 2)\n")
session.stdin.close()
const result = await session.wait()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# async only, so use AsyncSandbox
session = await sandbox.commands.spawn(
"python -i",
on_stdout=lambda data: print(data, end=""),
)
await session.stdin.write("print(2 + 2)\n")
await session.stdin.close()
result = await session.wait()
```
In Python, `spawn()` is on `AsyncSandbox` only; the synchronous version raises. It takes the same `cwd`, `env`, `timeoutMs` / `timeout_seconds`, `onStdout`, and `onStderr` options as `run`. Leave the timeout off for a long-lived process.
## `CommandSession`
| Member | Description |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| `stdin.write(data)` | Write to stdin: a string (UTF-8) or raw bytes. |
| `stdin.close()` | Close stdin, signalling EOF. |
| `kill(signal?)` | Send a signal (default `"SIGTERM"`). |
| `wait()` | Resolve with the [`CommandResult`](#commandresult) on exit; rejects if the connection drops first. |
| `close()` | Kill the process and close the connection. |
In Python the methods are awaitable (`await session.stdin.write(...)`, `await session.kill()`, `await session.wait()`) and the session is an `async with` context manager. In TypeScript the session supports `await using` for automatic cleanup.
## Options
| Option | Type | Description |
| ------------------------------- | ------------------------ | ------------------------------------------------------------------- |
| `cwd` | `string` | Working directory. Server-side default when unset. |
| `env` | `Record` | Env vars for this command only (merged with sandbox-wide env vars). |
| `timeoutMs` / `timeout_seconds` | `number` | Command timeout. Only sent when specified. |
| `onStdout` / `on_stdout` | `(data: string) => void` | Stream stdout chunks. |
| `onStderr` / `on_stderr` | `(data: string) => void` | Stream stderr chunks. |
| `signal` | `AbortSignal` | TypeScript only. Aborts mid-execution. |
## `CommandResult`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
interface CommandResult {
stdout: string
stderr: string
exitCode: number
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from pydantic import BaseModel
class CommandResult(BaseModel):
stdout: str
stderr: str
exit_code: int
```
## Non-zero exit codes
`run()` does **not** raise for non-zero exits. Inspect the exit code yourself.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const result = await sandbox.commands.run("exit 42")
if (result.exitCode !== 0) {
console.error(`Failed: ${result.stderr}`)
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
result = sandbox.commands.run("exit 42")
if result.exit_code != 0:
print(f"Failed: {result.stderr}")
```
## Errors
Commonly raised:
* `TimeoutError` / `SandboxTimeoutError`: the timeout elapsed before the command finished
* `NotFoundError`: the sandbox was deleted
* `ConflictError`: the sandbox is in an invalid state
* `SandboxError`: the connection dropped mid-stream without a terminal `finished` event (streaming only)
See [Errors](/errors) for the full hierarchy.
# Files
Source: https://docs.superserve.ai/sdk-reference/files
sandbox.files - read and write files inside a sandbox's filesystem.
Read and write files in a sandbox's filesystem with `sandbox.files`.
## `write`
Write a file at an absolute path. Parent directories are created automatically.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
// String content
await sandbox.files.write("/app/config.json", '{"key": "value"}')
// Binary content
await sandbox.files.write("/app/image.png", buffer)
// From Uint8Array
await sandbox.files.write("/app/data.bin", new Uint8Array([1, 2, 3]))
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# String content (UTF-8 encoded)
sandbox.files.write("/app/config.json", '{"key": "value"}')
# Binary content
with open("local-image.png", "rb") as f:
sandbox.files.write("/app/image.png", f.read())
```
**Parameters:**
| Option | Type | Description |
| ----------------------- | ---------------------------- | ---------------------------------------- |
| `path` | `string` / `str` | Absolute destination path. |
| `content` | `FileInput` / `str \| bytes` | File contents. See accepted types below. |
| `timeoutMs` / `timeout` | `number` / `float` | Optional request timeout. |
| `signal` | `AbortSignal` | TypeScript only - abort the request. |
**Accepted content types:**
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
type FileInput = string | Uint8Array | ArrayBuffer | Blob
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# str - encoded as UTF-8
# bytes - written as-is
```
**Cancellation (TypeScript):**
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const controller = new AbortController()
setTimeout(() => controller.abort(), 5000)
await sandbox.files.write("/app/large.bin", buffer, {
signal: controller.signal,
})
```
## `read`
Read a file as raw bytes.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const bytes: Uint8Array = await sandbox.files.read("/app/image.png")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
data: bytes = sandbox.files.read("/app/image.png")
```
## `readText` / `read_text`
Read a file and decode as UTF-8.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const text: string = await sandbox.files.readText("/app/config.json")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
text: str = sandbox.files.read_text("/app/config.json")
```
## `downloadDir` / `download_dir`
Download a directory as a ZIP archive. Returns the raw zip bytes — entries are
prefixed with the directory's base name (e.g. `output/log.txt`), and symlinks
are skipped.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { writeFileSync } from "node:fs"
const zip: Uint8Array = await sandbox.files.downloadDir("/app/output")
writeFileSync("output.zip", zip)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
zip_bytes: bytes = sandbox.files.download_dir("/app/output")
with open("output.zip", "wb") as f:
f.write(zip_bytes)
```
**Parameters:**
| Option | Type | Description |
| ----------------------- | ------------------ | ----------------------------------------------------------------------- |
| `path` | `string` / `str` | Absolute directory path to archive. |
| `timeoutMs` / `timeout` | `number` / `float` | Optional request timeout. Large directories can exceed the 30s default. |
| `signal` | `AbortSignal` | TypeScript only - abort the request. |
The server decides file-vs-directory: if `path` points at a regular file, its
bytes are streamed back as-is (not zipped) — use `read` for files.
## Round-trip example
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const original = "hello, world!\n"
await sandbox.files.write("/tmp/greeting.txt", original)
const roundTripped = await sandbox.files.readText("/tmp/greeting.txt")
console.log(roundTripped === original) // true
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
original = "hello, world!\n"
sandbox.files.write("/tmp/greeting.txt", original)
round_tripped = sandbox.files.read_text("/tmp/greeting.txt")
print(round_tripped == original) # True
```
## Path rules
* Paths **must** start with `/`
* Paths **must not** contain `..` segments
* Parent directories are created automatically on `write`
## Errors
Commonly raised:
* `NotFoundError` - file or directory does not exist (on read / downloadDir)
* `ValidationError` - invalid path (relative, contains `..`, or malformed)
* `AuthenticationError` - access token invalid or sandbox was deleted
* `TimeoutError` / `SandboxTimeoutError` - the optional timeout elapsed
See [Errors](/errors) for the full hierarchy.
# Sandbox
Source: https://docs.superserve.ai/sdk-reference/sandbox
The Sandbox class - factory methods, sandbox methods, properties, and types.
The `Sandbox` class is the main entry point for the SDK. Python ships both a sync `Sandbox` and an `AsyncSandbox` with identical method names.
## Import
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
# Async variant with awaitable methods:
from superserve import AsyncSandbox
```
## Authentication
The SDK reads `SUPERSERVE_API_KEY` from the environment by default. Pass `apiKey` / `api_key` explicitly to override, and `baseUrl` / `base_url` to target a different control-plane URL.
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
export SUPERSERVE_API_KEY=ss_live_...
export SUPERSERVE_BASE_URL=https://api.superserve.ai # optional
```
## Factory methods
### `Sandbox.create`
Create and boot a new sandbox. Synchronous - when the promise resolves, the VM is `active`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const sandbox = await Sandbox.create({
name: "data-analyzer",
timeoutSeconds: 3600,
metadata: { env: "prod" },
envVars: { OPENAI_API_KEY: "sk-..." },
network: { allowOut: ["api.openai.com"], denyOut: ["0.0.0.0/0"] },
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox, NetworkConfig
sandbox = Sandbox.create(
name="data-analyzer",
timeout_seconds=3600,
metadata={"env": "prod"},
env_vars={"OPENAI_API_KEY": "sk-..."},
network=NetworkConfig(
allow_out=["api.openai.com"],
deny_out=["0.0.0.0/0"],
),
)
```
**Options:**
| Option | Type | Description |
| ------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | `string` | **Required.** Human-readable sandbox name. |
| `fromTemplate` / `from_template` | `string \| Template` | Template name, UUID, or `Template` instance to boot from. Defaults to `superserve/base`. |
| `fromSnapshot` / `from_snapshot` | `string` | Snapshot UUID to boot from. |
| `timeoutSeconds` / `timeout_seconds` | `number` | Auto-pause after this many seconds of active time (per session; re-armed on resume). See [Lifecycle](/sandbox/lifecycle#auto-pause). |
| `autoDeleteSeconds` / `auto_delete_seconds` | `number` | Delete the sandbox once continuously paused for this many seconds (max 30 days, `0` = delete on pause). See [Lifecycle](/sandbox/lifecycle#auto-delete). |
| `metadata` | `Record` | String tags. |
| `envVars` / `env_vars` | `Record` | Env vars injected into every process. |
| `secrets` | `Record` | Bind secrets to env vars (`ENV_VAR → secret name`). The agent sees a stand-in token; the real credential is attached to outbound requests. See [Secrets](/secrets/overview). |
| `network` | `NetworkConfig` | Egress allow/deny rules. |
| `apiKey` / `api_key` | `string` | Overrides `SUPERSERVE_API_KEY`. |
| `baseUrl` / `base_url` | `string` | Overrides `SUPERSERVE_BASE_URL`. |
| `signal` | `AbortSignal` | TypeScript only - abort the creation request. |
### `Sandbox.connect`
Reconnect to an existing sandbox by ID. Returns a live `sandbox` with a fresh access token. If the sandbox is currently `paused`, it is auto-resumed before returning so it's immediately usable.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const sandbox = await Sandbox.connect("7a3f2b8c-1234-...")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox = Sandbox.connect("7a3f2b8c-1234-...")
```
### `Sandbox.list`
List sandboxes on the authenticated team. `metadata` filters combine with AND.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const all = await Sandbox.list()
const prod = await Sandbox.list({ metadata: { env: "prod" } })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
all_boxes = Sandbox.list()
prod = Sandbox.list(metadata={"env": "prod"})
```
Returns an array of [`SandboxInfo`](#sandboxinfo).
### `Sandbox.killById` / `Sandbox.kill_by_id`
Delete a sandbox by ID without instantiating it. Idempotent.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await Sandbox.killById("7a3f2b8c-1234-...")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
Sandbox.kill_by_id("7a3f2b8c-1234-...")
```
### `Sandbox.updateById` / `Sandbox.update_by_id`
Update a sandbox by ID without instantiating it. Unlike `connect(id)` followed
by `update(...)`, this does **not** resume a paused sandbox — so it's the right
way to arm auto-delete or change the auto-pause timeout on a box you want to
leave paused.
Same fields and clear-with-`null`/`None` semantics as [`update`](#update).
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await Sandbox.updateById("7a3f2b8c-1234-...", { autoDeleteSeconds: 3600 })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
Sandbox.update_by_id("7a3f2b8c-1234-...", auto_delete_seconds=3600)
```
## Methods on `sandbox`
### `getInfo` / `get_info`
Fetch the current server-side state. Returns a fresh [`SandboxInfo`](#sandboxinfo) - the returned `sandbox`'s own `status` / `metadata` properties are snapshots and are **not** mutated.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const info = await sandbox.getInfo()
console.log(info.status) // "active" | "paused" | "resuming" | "failed"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
info = sandbox.get_info()
print(info.status.value) # "active" | "paused" | "resuming" | "failed"
```
### `pause`
Checkpoint the VM state to disk. The sandbox transitions to `paused`. Returns nothing.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.pause()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.pause()
```
### `resume`
Restore a paused sandbox. The backend rotates the per-sandbox access token; the SDK re-injects it into `sandbox.files` transparently.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.resume()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.resume()
```
### `kill`
Delete the sandbox. Idempotent - swallows `404`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.kill()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.kill()
```
### `update`
Patch `metadata`, `network`, `autoDeleteSeconds`, and/or `timeoutSeconds` on a sandbox.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.update({
metadata: { env: "staging" },
network: { allowOut: ["api.openai.com"] },
})
// Auto-delete: a number arms the window, null disarms it.
await sandbox.update({ autoDeleteSeconds: 3600 })
await sandbox.update({ autoDeleteSeconds: null })
// Auto-pause timeout: same set/clear contract.
await sandbox.update({ timeoutSeconds: 900 })
await sandbox.update({ timeoutSeconds: null })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.update(
metadata={"env": "staging"},
network=NetworkConfig(allow_out=["api.openai.com"]),
)
# Auto-delete: a number arms the window, None disarms it.
sandbox.update(auto_delete_seconds=3600)
sandbox.update(auto_delete_seconds=None)
# Auto-pause timeout: same set/clear contract.
sandbox.update(timeout_seconds=900)
sandbox.update(timeout_seconds=None)
```
On an already-paused sandbox, arming `autoDeleteSeconds` starts the countdown from the moment of the call — never retroactively from when the sandbox paused. See [Lifecycle](/sandbox/lifecycle#auto-delete).
### `attachSecret` / `attach_secret`
Bind a team secret to a live or paused sandbox under an environment variable. The sandbox sees a stand-in token; the real credential is swapped in for outbound requests to the secret's allowed hosts. Takes effect for processes started after the call; a paused sandbox applies it on resume.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.attachSecret("ANTHROPIC_API_KEY", "anthropic-prod")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.attach_secret("ANTHROPIC_API_KEY", "anthropic-prod")
```
### `detachSecret` / `detach_secret`
Remove a secret binding by its environment-variable key. The stand-in token is revoked, so requests using it are refused — within about a minute for a process already running. A paused sandbox applies the change on resume.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.detachSecret("ANTHROPIC_API_KEY")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.detach_secret("ANTHROPIC_API_KEY")
```
### `getNetworkLog` / `get_network_log`
The sandbox's network log — every outbound connection and secret-bearing request, newest first. Returns a `NetworkLogPage`. See the [Network log](/sandbox/network-log) guide for the full model.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const page = await sandbox.getNetworkLog({ verdict: "blocked", limit: 100 })
for (const e of page.events) console.log(e.kind, e.host, e.verdict ?? e.status)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
page = sandbox.get_network_log(verdict="blocked", limit=100)
for e in page.events:
print(e.kind, e.host, e.verdict or e.status)
```
| Option | Type | Description |
| --------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `limit` | `number` | Max events per page. |
| `before` | `string` | Opaque pagination cursor — pass the previous page's `nextCursor` to page through results. Also accepts an RFC3339 timestamp as a time filter (rows strictly older than it). |
| `since` | `string` | RFC3339 — rows at or newer than this. |
| `verdict` | `"allowed" \| "blocked" \| "failed"` | Filter to connections with this verdict (excludes request rows). |
`NetworkLogPage` has `events` ([`NetworkEvent[]`](/sandbox/network-log#two-kinds-of-rows)), `nextCursor` / `next_cursor`, and `hasMore` / `has_more`.
## Properties on `sandbox`
`id`, `name`, `status`, `metadata`, and `secrets` are read-only snapshots taken at construction. Call `getInfo()` / `get_info()` for fresh data.
| Property | Type | Description |
| ---------- | ------------------------------------- | ------------------------------------------------------------------------------------ |
| `id` | `string` | Unique sandbox UUID. |
| `name` | `string` | Human-readable name. |
| `status` | `SandboxStatus` | Status at construction time. |
| `metadata` | `Record` | Tags at construction time. |
| `secrets` | `SandboxSecretBinding[] \| undefined` | Bound secrets (`envKey` → `secretName`, with a `revoked` flag) at construction time. |
| `commands` | `Commands` | See [Commands](/sdk-reference/commands). |
| `files` | `Files` | See [Files](/sdk-reference/files). Rebuilt after `resume()`. |
## Types
### `SandboxStatus`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
type SandboxStatus = "active" | "paused" | "resuming" | "failed"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from enum import Enum
class SandboxStatus(str, Enum):
ACTIVE = "active"
PAUSED = "paused"
RESUMING = "resuming"
FAILED = "failed"
```
* `active` - running and ready to accept commands
* `paused` - stopped with state saved to disk
* `resuming` - transient, briefly visible while a paused sandbox is being restored to `active` (retry shortly)
* `failed` - the sandbox couldn't boot or resume; the entry remains until you delete it
Deletion removes the sandbox entirely - subsequent API calls return `404`.
### `SandboxInfo`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
interface SandboxInfo {
id: string
name: string
status: SandboxStatus
vcpuCount: number
memoryMib: number
createdAt: Date
timeoutSeconds?: number
autoDeleteSeconds?: number
autoDeleteAt?: Date // present only while paused with autoDeleteSeconds set
network?: NetworkConfig
metadata: Record
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from datetime import datetime
from pydantic import BaseModel
class SandboxInfo(BaseModel):
id: str
name: str
status: SandboxStatus
vcpu_count: int
memory_mib: int
created_at: datetime
timeout_seconds: int | None = None
auto_delete_seconds: int | None = None
# Set only while paused with auto_delete_seconds configured.
auto_delete_at: datetime | None = None
network: NetworkConfig | None = None
metadata: dict[str, str]
```
### `NetworkConfig`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
interface NetworkConfig {
allowOut?: string[] // CIDRs or domain patterns
denyOut?: string[] // CIDRs only - use "0.0.0.0/0" to deny all
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from pydantic import BaseModel
class NetworkConfig(BaseModel):
allow_out: list[str] | None = None # CIDRs or domain patterns
deny_out: list[str] | None = None # CIDRs only
```
## Retries
GET and DELETE requests auto-retry on `429`, `502`, `503`, `504`, and network errors with exponential backoff + jitter (3 attempts max). POST and PATCH are not retried - the SDK surfaces the error so you can decide.
See [Errors](/errors).
## Errors
Sandbox methods commonly raise:
* `AuthenticationError` - missing or invalid API key
* `ValidationError` - bad request body
* `NotFoundError` - sandbox doesn't exist (except `kill` / `killById`, which swallow 404)
* `ConflictError` - wrong state (e.g., pausing an already-paused sandbox, or patching `network` while paused)
* `ServerError` - platform error
See [Errors](/errors) for the full hierarchy.
# Secret
Source: https://docs.superserve.ai/sdk-reference/secret
The Secret and Provider classes - factory methods, instance methods, and types for credential management.
`Secret` manages credentials Superserve attaches to outbound requests; `Provider` lists the built-in shortcuts. Python ships both sync (`Secret`, `Provider`) and async (`AsyncSecret`, `AsyncProvider`) variants with identical method names.
## Import
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Secret, Provider } from "@superserve/sdk"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Secret, Provider
# Async variants:
from superserve import AsyncSecret, AsyncProvider
```
The SDK reads `SUPERSERVE_API_KEY` from the environment by default; pass `apiKey` / `api_key` to override.
## Factory methods
### `Secret.create`
Create a secret. Provide either a `provider` shortcut **or** a custom `auth` config with `hosts` — they're mutually exclusive.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
// Provider shortcut.
await Secret.create({ name: "openai-prod", value: key, provider: "openai" })
// Custom auth.
await Secret.create({
name: "internal-api",
value: token,
hosts: ["api.internal.example.com"],
auth: { type: "api-key", header: "X-Api-Key" },
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
Secret.create(name="openai-prod", value=key, provider="openai")
Secret.create(
name="internal-api",
value=token,
hosts=["api.internal.example.com"],
auth={"type": "api-key", "header": "X-Api-Key"},
)
```
**Options:**
| Option | Type | Description |
| ---------- | ------------ | ------------------------------------------------------------------------------------------------------ |
| `name` | `string` | **Required.** Unique secret name. |
| `value` | `string` | **Required.** The credential. Encrypted on the platform; never returned. |
| `provider` | `string` | A built-in shortcut (see [`Provider.list`](#provider-list)). Mutually exclusive with `auth` / `hosts`. |
| `auth` | `SecretAuth` | Custom auth config. Requires `hosts`. |
| `hosts` | `string[]` | Upstream hosts the credential may authenticate. Required with `auth`. |
Returns a `Secret`.
### `Secret.get`
Fetch an existing secret by name. Returns a `Secret`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const secret = await Secret.get("openai-prod")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
secret = Secret.get("openai-prod")
```
### `Secret.list`
List all secrets for the team. Returns an array of [`SecretInfo`](#secretinfo).
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const secrets = await Secret.list()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
secrets = Secret.list()
```
### `Secret.deleteByName` / `Secret.delete_by_name`
Delete a secret by name. Idempotent. Revokes the secret everywhere immediately — bound stand-in tokens stop working.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await Secret.deleteByName("openai-prod")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
Secret.delete_by_name("openai-prod")
```
## Methods on `secret`
### `rotate`
Replace the value. Bound sandboxes keep their env var; the new value is used on subsequent requests. Returns the updated `Secret`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await secret.rotate(newValue)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
secret.rotate(new_value)
```
### `getAudit` / `get_audit`
Requests made with this secret, across all sandboxes, newest first. Returns an array of [`ProxyAuditEvent`](#proxyauditevent).
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const events = await secret.getAudit({ limit: 50, status: "4xx" })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
events = secret.get_audit(limit=50, status="4xx")
```
| Option | Type | Description |
| -------- | ---------------------------------------------- | -------------------------------------------------- |
| `limit` | `number` | Max events to return. |
| `before` | `number` | Cursor — return events older than this event `id`. |
| `status` | `"2xx" \| "3xx" \| "4xx" \| "5xx" \| "errors"` | Filter by status class. |
### `getSandboxes` / `get_sandboxes`
Sandboxes this secret is currently bound to. Returns an array of [`SecretSandboxBinding`](#secretsandboxbinding).
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const bindings = await secret.getSandboxes()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
bindings = secret.get_sandboxes()
```
### `getInfo` / `get_info` · `delete`
`getInfo()` re-fetches the latest [`SecretInfo`](#secretinfo); `delete()` removes this secret (idempotent).
## `Provider.list`
List the built-in provider shortcuts. Returns an array of [`ProviderShortcut`](#providershortcut).
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const providers = await Provider.list()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
providers = Provider.list()
```
## Types
### `SecretInfo`
| Field | Type |
| ---------------------------------------- | ------------------------------------------------------------ |
| `id` | `string` |
| `name` | `string` |
| `authType` / `auth_type` | `"bearer" \| "basic" \| "api-key" \| "custom" \| "per_host"` |
| `authConfig` / `auth_config` | `Record` |
| `providerShortcut` / `provider_shortcut` | `string \| undefined` |
| `hosts` | `string[]` |
| `createdAt` / `created_at` | `Date` / `datetime` |
| `updatedAt` / `updated_at` | `Date` / `datetime` |
| `lastUsedAt` / `last_used_at` | `Date \| undefined` |
### `ProviderShortcut`
| Field | Type |
| ---------------------------- | ------------------------------------------------ |
| `name` | `string` — pass as `provider` to `Secret.create` |
| `display` | `string` |
| `authType` / `auth_type` | `SecretAuthType` |
| `hosts` | `string[]` |
| `tokenShape` / `token_shape` | `string` — e.g. `"sk-ant-api03-..."` |
### `ProxyAuditEvent`
| Field | Type |
| ------------------------------------ | --------------------- |
| `id` | `number` |
| `ts` | `Date` / `datetime` |
| `sandboxId` / `sandbox_id` | `string` |
| `sandboxName` / `sandbox_name` | `string \| undefined` |
| `method` · `host` · `path` | `string` |
| `status` | `number` |
| `upstreamStatus` / `upstream_status` | `number \| undefined` |
| `latencyMs` / `latency_ms` | `number \| undefined` |
| `errorCode` / `error_code` | `string \| undefined` |
### `SecretSandboxBinding`
| Field | Type |
| ------------------------------ | --------------- |
| `sandboxId` / `sandbox_id` | `string` |
| `sandboxName` / `sandbox_name` | `string` |
| `envKey` / `env_key` | `string` |
| `status` | `SandboxStatus` |
For the network log types (`NetworkEvent`, `NetworkLogPage`), see [`Sandbox.getNetworkLog`](/sdk-reference/sandbox#getnetworklog-get-network-log).
# Template
Source: https://docs.superserve.ai/sdk-reference/template
The Template class - factory methods, instance methods, properties, and types.
The `Template` class creates and manages reusable sandbox base images. Templates are built once and then used to launch any number of identically configured sandboxes in seconds.
See the [Templates overview](/templates/overview) for concepts, or jump to the [Create a template](/templates/create) guide for the end-to-end flow.
## Import
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Template } from "@superserve/sdk"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Template
# Step types for the `steps` array:
from superserve import RunStep, EnvStep, EnvStepValue, WorkdirStep, UserStep, UserStepValue
```
## Factory methods
### `Template.create`
Register a new template and queue its first build. Returns as soon as the template row exists and a `build_id` is assigned - the build itself runs asynchronously. Call [`waitUntilReady`](#waituntilready-wait-until-ready) to block until the build finishes.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const template = await Template.create({
name: "my-python-env",
vcpu: 2,
memoryMib: 2048,
diskMib: 4096,
from: "python:3.11",
steps: [
{ run: "pip install numpy pandas" },
{ env: { key: "DEBUG", value: "1" } },
{ workdir: "/app" },
{ user: { name: "appuser", sudo: true } },
],
startCmd: "python server.py",
readyCmd: "curl -f http://localhost:8080/health",
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Template, RunStep, EnvStep, EnvStepValue, WorkdirStep, UserStep, UserStepValue
template = Template.create(
name="my-python-env",
vcpu=2,
memory_mib=2048,
disk_mib=4096,
from_="python:3.11",
steps=[
RunStep(run="pip install numpy pandas"),
EnvStep(env=EnvStepValue(key="DEBUG", value="1")),
WorkdirStep(workdir="/app"),
UserStep(user=UserStepValue(name="appuser", sudo=True)),
],
start_cmd="python server.py",
ready_cmd="curl -f http://localhost:8080/health",
)
```
**Options:**
| Option | Type | Description |
| -------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | `string` | **Required.** Team-scoped identifier. Must not start with `superserve/`. Names are unique per team among non-deleted templates and are released for reuse the moment a template is deleted. |
| `from` / `from_` | `string` | **Required.** OCI base image (e.g. `python:3.11`). See [BuildSpec](/templates/build-spec#from-base-image). |
| `vcpu` | `number` | 1-4. Default `1`. |
| `memoryMib` / `memory_mib` | `number` | 256-4096. Default `1024`. |
| `diskMib` / `disk_mib` | `number` | 1024-8192. Default `4096`. |
| `steps` | [`BuildStep[]`](#buildstep) | Ordered build steps. |
| `startCmd` / `start_cmd` | `string` | Long-running process captured in the snapshot. |
| `readyCmd` / `ready_cmd` | `string` | Readiness probe polled every 2s after `startCmd`. |
| `apiKey` / `api_key` | `string` | Overrides `SUPERSERVE_API_KEY`. |
| `baseUrl` / `base_url` | `string` | Overrides `SUPERSERVE_BASE_URL`. |
| `signal` | `AbortSignal` | TypeScript only - abort the creation request. |
Throws `SandboxError` if the API response is missing `build_id`.
### `Template.connect`
Load an existing template by UUID.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const template = await Template.connect("7a3f2b8c-1234-...")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
template = Template.connect("7a3f2b8c-1234-...")
```
### `Template.list`
List all templates visible to the authenticated team (includes system templates).
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const all = await Template.list()
const mine = await Template.list({ namePrefix: "my-" })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
all_templates = Template.list()
mine = Template.list(name_prefix="my-")
```
Returns an array of [`TemplateInfo`](#templateinfo).
### `Template.deleteById` / `Template.delete_by_id`
Delete a template by UUID without instantiating it. Idempotent on `404`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await Template.deleteById("7a3f2b8c-1234-...")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
Template.delete_by_id("7a3f2b8c-1234-...")
```
Throws `ConflictError` if any non-destroyed sandbox still references the template. The template's name is released for reuse as soon as the call succeeds.
## Methods on `template`
### `getInfo` / `get_info`
Fetch the current server-side state. Returns a fresh [`TemplateInfo`](#templateinfo) - the `template`'s own properties are snapshots and are **not** mutated.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const info = await template.getInfo()
console.log(info.status) // "pending" | "building" | "ready" | "failed"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
info = template.get_info()
print(info.status.value) # "pending" | "building" | "ready" | "failed"
```
### `waitUntilReady` / `wait_until_ready`
Block until the current build reaches a terminal status. The SDK polls `GET /templates/{id}/builds/{buildId}` for the authoritative build status — SSE is used only for live log delivery when `onLog` / `on_log` is provided, never to detect termination. Returns a refreshed [`TemplateInfo`](#templateinfo) with `status = "ready"`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const info = await template.waitUntilReady({
onLog: (event) => {
if (event.stream === "system") return
process.stdout.write(event.text)
},
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
def print_log(ev):
if ev.stream.value != "system":
print(ev.text, end="")
info = template.wait_until_ready(on_log=print_log)
```
**Options:**
| Option | Type | Description |
| ------------------ | ----------------------------- | ------------------------------------------------------------ |
| `onLog` / `on_log` | `(ev: BuildLogEvent) => void` | Called for every SSE log event. |
| `signal` | `AbortSignal` | TypeScript only - aborts the stream and any pending poll. |
| `pollIntervalMs` | `number` | TypeScript only. Build status poll interval. Default `2000`. |
| `poll_interval_s` | `float` | Python only. Build status poll interval. Default `2.0`. |
**Throws:**
* `BuildError` if the build transitions to `failed`. The backend's `error_message` follows a `": "` convention; the SDK splits it so `BuildError.code` is the stable prefix and `BuildError.message` is the human-readable detail. See [BuildSpec: Build error codes](/templates/build-spec#build-error-codes).
* `ConflictError` if the build is `cancelled`
### `streamBuildLogs` / `stream_build_logs`
Pure SSE stream of build logs. The promise resolves when the stream closes. Does **not** wait for a terminal status - use `waitUntilReady` for that.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await template.streamBuildLogs({
onEvent: (ev) => {
console.log(`[${ev.stream}] ${ev.text}`)
},
// buildId defaults to template.latestBuildId
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
def on_event(ev):
print(f"[{ev.stream.value}] {ev.text}")
template.stream_build_logs(on_event=on_event)
# build_id defaults to template.latest_build_id
```
**Options:**
| Option | Type | Description |
| ---------------------- | ----------------------------- | -------------------------------------------------------------- |
| `onEvent` / `on_event` | `(ev: BuildLogEvent) => void` | **Required.** Called for every event. |
| `buildId` / `build_id` | `string` | Target build. Defaults to `latestBuildId` / `latest_build_id`. |
| `signal` | `AbortSignal` | TypeScript only - aborts the stream. |
Throws `SandboxError` if no `buildId` is supplied and the template has no `latestBuildId`.
### `rebuild`
Queue a new build for this template. Returns the new [`TemplateBuildInfo`](#templatebuildinfo). Idempotent: if an in-flight build already matches the same configuration, the existing build is returned.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const build = await template.rebuild()
console.log(build.id, build.status)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
build = template.rebuild()
print(build.id, build.status.value)
```
### `listBuilds` / `list_builds`
List recent builds for this template, newest first.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const builds = await template.listBuilds({ limit: 10 })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
builds = template.list_builds(limit=10)
```
Returns an array of [`TemplateBuildInfo`](#templatebuildinfo).
### `getBuild` / `get_build`
Fetch a single build by ID.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const build = await template.getBuild("build-uuid")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
build = template.get_build("build-uuid")
```
### `cancelBuild` / `cancel_build`
Cancel an in-flight build. Idempotent - no-op for builds already in a terminal state, and for `404`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await template.cancelBuild(build.id)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
template.cancel_build(build.id)
```
### `delete`
Delete this template. Idempotent on `404`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await template.delete()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
template.delete()
```
Throws `ConflictError` if any non-destroyed sandbox still references the template - destroy those sandboxes first.
## Properties on `template`
All properties are read-only snapshots taken at construction. Call `getInfo()` / `get_info()` to refresh.
| Property | Type | Description |
| ------------------------------------- | ------------------- | ------------------------------------------ |
| `id` | `string` | Template UUID. |
| `name` | `string` | Team-scoped identifier. |
| `teamId` / `team_id` | `string` | Owning team UUID. |
| `status` | `TemplateStatus` | Status at construction time. |
| `vcpu` | `number` | vCPU count the template boots with. |
| `memoryMib` / `memory_mib` | `number` | Memory the template boots with. |
| `diskMib` / `disk_mib` | `number` | Disk the template boots with. |
| `sizeBytes?` / `size_bytes?` | `number` | Snapshot size in bytes, once built. |
| `errorMessage?` / `error_message?` | `string` | Last build's error message, if any. |
| `createdAt` / `created_at` | `Date` / `datetime` | When the template row was created. |
| `builtAt?` / `built_at?` | `Date` / `datetime` | When the latest successful build finished. |
| `latestBuildId?` / `latest_build_id?` | `string` | Most recent build UUID. |
## Types
### `TemplateStatus`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
type TemplateStatus = "pending" | "building" | "ready" | "failed"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from enum import Enum
class TemplateStatus(str, Enum):
PENDING = "pending"
BUILDING = "building"
READY = "ready"
FAILED = "failed"
```
* `pending` - template exists; first build is queued
* `building` - a build is in progress
* `ready` - at least one successful build exists; sandboxes can boot from this template
* `failed` - the latest build failed; rebuild or fix the spec
### `TemplateBuildStatus`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
type TemplateBuildStatus =
| "pending"
| "building"
| "snapshotting"
| "ready"
| "failed"
| "cancelled"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from enum import Enum
class TemplateBuildStatus(str, Enum):
PENDING = "pending"
BUILDING = "building"
SNAPSHOTTING = "snapshotting"
READY = "ready"
FAILED = "failed"
CANCELLED = "cancelled"
```
### `TemplateInfo`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
interface TemplateInfo {
id: string
name: string
teamId: string
status: TemplateStatus
vcpu: number
memoryMib: number
diskMib: number
sizeBytes?: number
errorMessage?: string
createdAt: Date
builtAt?: Date
latestBuildId?: string
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from datetime import datetime
from pydantic import BaseModel
class TemplateInfo(BaseModel):
id: str
name: str
team_id: str
status: TemplateStatus
vcpu: int
memory_mib: int
disk_mib: int
size_bytes: int | None = None
error_message: str | None = None
created_at: datetime
built_at: datetime | None = None
latest_build_id: str | None = None
```
### `TemplateBuildInfo`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
interface TemplateBuildInfo {
id: string
templateId: string
status: TemplateBuildStatus
buildSpecHash: string
errorMessage?: string
startedAt?: Date
finalizedAt?: Date
createdAt: Date
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from datetime import datetime
from pydantic import BaseModel
class TemplateBuildInfo(BaseModel):
id: str
template_id: str
status: TemplateBuildStatus
build_spec_hash: str
error_message: str | None = None
started_at: datetime | None = None
finalized_at: datetime | None = None
created_at: datetime
```
### `BuildLogEvent`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
interface BuildLogEvent {
timestamp: Date
stream: "stdout" | "stderr" | "system"
text: string
finished?: boolean
status?: "ready" | "failed" | "cancelled"
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from datetime import datetime
from pydantic import BaseModel
class BuildLogEvent(BaseModel):
timestamp: datetime
stream: BuildLogStream # "stdout" | "stderr" | "system"
text: str
finished: bool | None = None
status: str | None = None # "ready" | "failed" | "cancelled"
```
`stream == "system"` events are status messages from the build runner (build-step boundaries, snapshot progress). Filter them out when rendering to a user-facing console. `finished: true` marks the last event in a stream and carries the terminal `status`.
### `BuildStep`
A discriminated union - exactly one of `run` / `env` / `workdir` / `user` must be set per step. See [BuildSpec: Steps](/templates/build-spec#steps-ordered-build-steps) for semantics.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
type BuildStep =
| { run: string }
| { env: { key: string; value: string } }
| { workdir: string }
| { user: { name: string; sudo?: boolean } }
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from pydantic import BaseModel
class RunStep(BaseModel):
run: str
class EnvStepValue(BaseModel):
key: str
value: str
class EnvStep(BaseModel):
env: EnvStepValue
class WorkdirStep(BaseModel):
workdir: str
class UserStepValue(BaseModel):
name: str
sudo: bool = False
class UserStep(BaseModel):
user: UserStepValue
BuildStep = RunStep | EnvStep | WorkdirStep | UserStep
```
### `BuildError`
Thrown by `waitUntilReady` / `wait_until_ready` when the build transitions to `failed`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
class BuildError extends SandboxError {
readonly code: string // e.g. "image_pull_failed" | "step_failed" | ...
readonly message: string // human-readable detail (prefix stripped)
readonly buildId: string
readonly templateId: string
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
class BuildError(SandboxError):
code: str # e.g. "image_pull_failed" | "step_failed" | ...
# `str(err)` returns the human-readable detail (prefix stripped)
build_id: str
template_id: str
```
The backend's `error_message` follows a `": "` convention (e.g. `"image_too_large: image is too large for the requested disk_mib"`). The SDK splits it so `code` holds the prefix and the error message holds the detail. When the backend doesn't include a prefix, `code` falls back to `"build_failed"` and the message falls back to `"Template build failed"`.
See the [full list of build error codes](/templates/build-spec#build-error-codes) and the general [Errors](/errors) reference.
## Retries
GET and DELETE requests (including `Template.list`, `Template.connect`, `getInfo`, `listBuilds`, `getBuild`, `cancelBuild`, `delete`, `deleteById`) auto-retry on `429`, `502`, `503`, `504`, and network errors with exponential backoff + jitter (3 attempts max). POST requests (`create`, `rebuild`) are not retried - the SDK surfaces the error so you can decide.
## Errors
Template methods commonly raise:
* `AuthenticationError` - missing or invalid API key
* `ValidationError` - bad request body (invalid name, unsupported base image, steps with multiple keys)
* `NotFoundError` - template or build doesn't exist (except `delete` / `deleteById` / `cancelBuild`, which swallow 404)
* `ConflictError` - template has dependent sandboxes (on delete) or build was cancelled (on `waitUntilReady`)
* `BuildError` - build transitioned to `failed` (from `waitUntilReady` only)
* `ServerError` - platform error
See [Errors](/errors) for the full hierarchy.
## Related
* [Templates overview](/templates/overview)
* [Create a template](/templates/create)
* [Rebuild, cancel, delete](/templates/lifecycle)
* [BuildSpec reference](/templates/build-spec)
* [Errors](/errors)
# Audit secret usage
Source: https://docs.superserve.ai/secrets/audit
See every request made with a secret — across all sandboxes — and which sandboxes a secret is bound to.
Because Superserve attaches credentials to outbound requests, every use of a secret is recorded. You get a full trail of what was sent where, with what result — without any cooperation from the code, and without trusting the sandbox.
## Requests made with a secret
`getAudit()` returns the requests authenticated with a secret, across every sandbox that used it, newest first:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const secret = await Secret.get("anthropic-prod")
const events = await secret.getAudit({ limit: 50 })
for (const e of events) {
console.log(e.ts, e.method, e.host + e.path, "→", e.status)
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
secret = Secret.get("anthropic-prod")
events = secret.get_audit(limit=50)
for e in events:
print(e.ts, e.method, f"{e.host}{e.path}", "→", e.status)
```
Each event carries the sandbox that made it, the request line, and both the status returned to the sandbox and the `upstreamStatus` from the real service:
| Field | Description |
| --------------------------- | ----------------------------------------------------------------------------------- |
| `ts` | When the request was made |
| `sandboxId` / `sandboxName` | The sandbox that used the secret (`sandboxName` is null if it's since been deleted) |
| `method` · `host` · `path` | The request line |
| `status` | Status returned to the sandbox |
| `upstreamStatus` | Status from the upstream service |
| `latencyMs` | Round-trip time |
| `errorCode` | Set when the request was blocked or failed |
### Filtering
Narrow to a status class, or page back with the `before` cursor:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
// Only client/server errors.
const errors = await secret.getAudit({ status: "4xx" })
// Page back.
const older = await secret.getAudit({ before: errors.at(-1)!.id })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
errors = secret.get_audit(status="4xx")
older = secret.get_audit(before=errors[-1].id)
```
`status` accepts `"2xx"`, `"3xx"`, `"4xx"`, `"5xx"`, or `"errors"` (requests that failed before reaching the service).
## Sandboxes using a secret
To see where a secret is bound, `getSandboxes()` lists the live bindings:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const bindings = await secret.getSandboxes()
for (const b of bindings) {
console.log(b.sandboxName, "·", b.envKey, "·", b.status)
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
for b in secret.get_sandboxes():
print(b.sandbox_name, "·", b.env_key, "·", b.status)
```
For the full picture of a single sandbox's traffic — not just secret-bearing requests, but every connection it made — use the [network log](/sandbox/network-log).
# Bind secrets to a sandbox
Source: https://docs.superserve.ai/secrets/binding
Map secrets to environment variables at sandbox creation. Code in the sandbox reads the env var; the platform attaches the real value to outbound requests.
Bind secrets when you create a sandbox with the `secrets` option — a map of **environment variable → secret name**:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({
name: "research-agent",
secrets: {
ANTHROPIC_API_KEY: "anthropic-prod",
GITHUB_TOKEN: "github-token",
},
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(
name="research-agent",
secrets={
"ANTHROPIC_API_KEY": "anthropic-prod",
"GITHUB_TOKEN": "github-token",
},
)
```
Inside the sandbox, `ANTHROPIC_API_KEY` is set like any environment variable — but its value is a **stand-in token**, not your real key. Code in the sandbox uses it normally:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# Inside the sandbox — the code has no idea it's not the real key.
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{ ... }'
```
The request leaves the sandbox, the platform swaps the stand-in token for your real credential on the way to `api.anthropic.com`, and Anthropic sees the real key. If code in the sandbox prints `$ANTHROPIC_API_KEY`, dumps its environment, or leaks it some other way, all it has is the stand-in token — worthless anywhere except authenticated requests to the secret's hosts.
## Attach and detach on a running sandbox
You can also change a sandbox's bindings after it exists — useful when an agent session is already up and needs a new credential, or when you want to remove one. Works on `active` and `paused` sandboxes.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
// Add a binding.
await sandbox.attachSecret("ANTHROPIC_API_KEY", "anthropic-prod")
// Remove it later.
await sandbox.detachSecret("ANTHROPIC_API_KEY")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# Add a binding.
sandbox.attach_secret("ANTHROPIC_API_KEY", "anthropic-prod")
# Remove it later.
sandbox.detach_secret("ANTHROPIC_API_KEY")
```
A binding takes effect for **processes started after** the call — a process already running keeps the environment it started with until it re-execs. Detaching cuts the credential's access for new processes immediately and for an already-running process within about a minute. A `paused` sandbox applies either change when it resumes.
## Inspecting bindings
A sandbox reports the secrets bound to it:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
for (const b of sandbox.secrets ?? []) {
console.log(b.envKey, "←", b.secretName, b.revoked ? "(revoked)" : "")
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
for b in sandbox.secrets or []:
print(b.env_key, "←", b.secret_name, "(revoked)" if b.revoked else "")
```
`revoked` flips to `true` when the underlying secret has been deleted — the env var still holds the (now-inert) stand-in token, and any request that relies on it fails rather than falling back to a stale credential.
To go the other way — from a secret to the sandboxes using it — see [`getSandboxes()`](/secrets/audit#sandboxes-using-a-secret).
`secrets` and [`envVars`](/sandbox/environment-variables) both set environment variables, so a given name can come from one or the other — not both. Reusing the same name across them is rejected.
## Combine with network rules
Secrets pair naturally with [network rules](/sandbox/networking): attach the credential *and* restrict the sandbox to only the hosts it should reach. A sandbox that can authenticate to `api.anthropic.com` and reach nothing else is hard to misuse.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const sandbox = await Sandbox.create({
name: "locked-down",
secrets: { ANTHROPIC_API_KEY: "anthropic-prod" },
network: {
allowOut: ["api.anthropic.com"],
denyOut: ["0.0.0.0/0"],
},
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import NetworkConfig
sandbox = Sandbox.create(
name="locked-down",
secrets={"ANTHROPIC_API_KEY": "anthropic-prod"},
network=NetworkConfig(
allow_out=["api.anthropic.com"],
deny_out=["0.0.0.0/0"],
),
)
```
# Create a secret
Source: https://docs.superserve.ai/secrets/create
Store a credential with a provider shortcut or a custom auth scheme, then rotate or delete it without touching your sandboxes.
A secret needs a **name**, a **value** (the real credential), and a way to authenticate — either a [provider shortcut](#provider-shortcuts) or a [custom auth config](#custom-secrets). The value is encrypted on the platform and never returned by the API.
## Provider shortcuts
The fastest path. A shortcut sets the auth scheme and allowed hosts for a known service, so you supply only a name and value:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Secret } from "@superserve/sdk"
const secret = await Secret.create({
name: "openai-prod",
value: process.env.OPENAI_API_KEY!,
provider: "openai",
})
console.log(secret.hosts) // ["api.openai.com"]
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import os
from superserve import Secret
secret = Secret.create(
name="openai-prod",
value=os.environ["OPENAI_API_KEY"],
provider="openai",
)
print(secret.hosts) # ["api.openai.com"]
```
Built-in providers span LLM APIs, dev tools, and SaaS, and the list grows over time. `Provider.list()` returns the full current set — names, hosts, and token shape:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Provider } from "@superserve/sdk"
const providers = await Provider.list()
for (const p of providers) {
console.log(p.name, p.hosts) // "anthropic" ["api.anthropic.com"]
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Provider
for p in Provider.list():
print(p.name, p.hosts) # "anthropic" ["api.anthropic.com"]
```
## Custom secrets
When there's no shortcut, define the auth yourself with `auth` and `hosts`. `auth` and `provider` are mutually exclusive — pick one.
The `auth.type` controls how the credential is attached to outbound requests:
| `type` | Attaches as |
| --------- | ---------------------------------------------------- |
| `bearer` | `Authorization: Bearer ` |
| `api-key` | a named header — `: ` |
| `basic` | HTTP Basic, `:` |
| `custom` | arbitrary header templates referencing `{{ value }}` |
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
// One host, key sent in the Authorization header.
await Secret.create({
name: "linear-key",
value: "lin_api_...",
hosts: ["api.linear.app"],
auth: { type: "api-key", header: "Authorization" },
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# One host, key sent in the Authorization header.
Secret.create(
name="linear-key",
value="lin_api_...",
hosts=["api.linear.app"],
auth={"type": "api-key", "header": "Authorization"},
)
```
### Per-host auth
A single secret can authenticate differently per host — useful when one credential works across, say, a REST API and a git endpoint:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await Secret.create({
name: "github-token",
value: "ghp_...",
hosts: ["api.github.com", "github.com"],
auth: {
perHost: [
{ hosts: ["api.github.com"], type: "bearer" },
{ hosts: ["github.com"], type: "basic", username: "x-access-token" },
],
},
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
Secret.create(
name="github-token",
value="ghp_...",
hosts=["api.github.com", "github.com"],
auth={
"per_host": [
{"hosts": ["api.github.com"], "type": "bearer"},
{"hosts": ["github.com"], "type": "basic", "username": "x-access-token"},
]
},
)
```
`hosts` accepts wildcards (`*.example.com`), but not a whole top-level domain like `*.com`. A wildcard matches subdomains, not the domain itself — list both `example.com` and `*.example.com` if you need each.
## Rotation
Replace a secret's value without recreating it. Bound sandboxes keep their environment variable; the new value is used on subsequent requests — no redeploy, no restart.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const secret = await Secret.get("openai-prod")
await secret.rotate(newKey)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
secret = Secret.get("openai-prod")
secret.rotate(new_key)
```
## List and delete
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const secrets = await Secret.list()
await Secret.deleteByName("openai-prod")
// or, from an instance:
await secret.delete()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
secrets = Secret.list()
Secret.delete_by_name("openai-prod")
# or, from an instance:
secret.delete()
```
Deleting a secret takes effect **immediately, everywhere it's bound** — any sandbox still using it starts failing at once. The environment variable stays, but its stand-in token no longer works. If you only want to change the value, [rotate](#rotation) instead.
## Next steps
Attach the secret to an environment variable so code in the sandbox can use it.
# Secrets
Source: https://docs.superserve.ai/secrets/overview
Give a sandbox an API key without the key ever entering the sandbox. The platform attaches your real credential to outbound requests.
A **secret** is a credential — an API key, token, or password — that you store once and bind to sandboxes. Code running inside the sandbox never sees the real value. It sees a **stand-in token**, and Superserve swaps that token for your real credential as each request leaves the sandbox — only on requests to the hosts you allow.
This matters because sandboxes run untrusted code. A leaked env var, a prompt-injected agent, or a `printenv` in a log can all expose a raw key. With secrets, there's nothing in the sandbox to leak — the real credential never crosses the boundary.
## How it works
`Secret.create()` encrypts the value and stores it with Superserve. You give it a name and the hosts it may authenticate (a [provider shortcut](/secrets/create#provider-shortcuts) sets the hosts for you).
On `Sandbox.create()`, add it to the `secrets` map — `{ ANTHROPIC_API_KEY: "anthropic-prod" }`. The sandbox gets an `ANTHROPIC_API_KEY` it can read like any other variable, but its value is a **stand-in token**, not your key.
The code calls, say, `api.anthropic.com` with that token, exactly as it would with a real key.
On the way out, Superserve recognizes the token, swaps in your real credential for that host, and forwards the request. The service sees your real key; the sandbox never did.
Secrets are **host-scoped**. A secret for `api.anthropic.com` is only ever attached to requests to that host. Sent anywhere else, the stand-in token is useless.
## When to use secrets
Use secrets for anything that **authenticates** — API keys, tokens, passwords. For non-sensitive configuration like log levels, feature flags, or public URLs, plain [`envVars`](/sandbox/environment-variables) is simpler and works fine.
## Provider shortcuts
For common services, a **provider shortcut** preconfigures the auth scheme and allowed hosts so you only supply a name and value:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Secret } from "@superserve/sdk"
await Secret.create({
name: "anthropic-prod",
value: process.env.ANTHROPIC_API_KEY!,
provider: "anthropic",
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import os
from superserve import Secret
Secret.create(
name="anthropic-prod",
value=os.environ["ANTHROPIC_API_KEY"],
provider="anthropic",
)
```
Built-in shortcuts span LLM APIs (Anthropic, OpenAI, Gemini, OpenRouter, …), dev tools (GitHub, GitLab, Vercel, Cloudflare), and SaaS (Stripe, Notion, Resend, …). Call [`Provider.list()`](/secrets/create#provider-shortcuts) for the current list, or define your own auth with [custom secrets](/secrets/create#custom-secrets).
## Next steps
Provider shortcuts, custom auth, and rotation.
Attach secrets to environment variables at create time.
See every request made with a secret.
See everything a sandbox reached on the network.
# Archil
Source: https://docs.superserve.ai/storage/archil
Mount an Archil elastic filesystem in a Superserve sandbox to share and persist workspaces, caches, and datasets across runs.
Archil is an elastic, POSIX-compliant filesystem backed by object storage, with optional sync to your own S3, GCS, R2, or any S3-compatible bucket. Mount an Archil **disk** inside a Superserve sandbox to give it a fast workspace whose data persists across runs, survives the sandbox, and can be mounted by several sandboxes at once with `--shared`.
Because every Superserve sandbox is a full Firecracker microVM, FUSE works **inside** the sandbox with no privileged-host workarounds, so you mount the disk with the same `archil mount` command you'd run on any Linux box.
Create a disk and a disk-scoped **mount token** in the [Archil console](https://console.archil.com) first. Keep its region (e.g. `aws-us-east-1`) and disk reference handy: an owner-qualified name (`myorg/my-disk`) or ID (`dsk-0123456789abcdef`). See [Archil's docs](https://docs.archil.com/getting-started/introduction) for disk setup.
## 1. Bake the Archil client into a template
Install the Archil CLI and its `libfuse2` dependency at build time so every sandbox starts ready to mount. [Templates](/templates/overview) snapshot this, so each sandbox launches fast with nothing to reinstall.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Template } from "@superserve/sdk"
const template = await Template.create({
name: "archil-base",
from: "ubuntu:22.04",
steps: [
{ run: "apt-get update && apt-get install -y curl libfuse2" },
{ run: "curl -fsSL https://archil.com/install | sh" },
],
})
await template.waitUntilReady()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Template, RunStep
template = Template.create(
name="archil-base",
from_="ubuntu:22.04",
steps=[
RunStep(run="apt-get update && apt-get install -y curl libfuse2"),
RunStep(run="curl -fsSL https://archil.com/install | sh"),
],
)
template.wait_until_ready()
```
## 2. Mount the disk at runtime
Create a sandbox from the template, then run `archil mount`. Pass the disk-scoped token as a **per-command** env var so it's present only for the mount itself, not for every process in the sandbox.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({
name: "archil-workspace",
fromTemplate: "archil-base",
})
await sandbox.commands.run("mkdir -p /mnt/archil")
await sandbox.commands.run(
"archil mount myorg/my-disk /mnt/archil --region aws-us-east-1",
{ env: { ARCHIL_MOUNT_TOKEN: process.env.ARCHIL_MOUNT_TOKEN! } },
)
// /mnt/archil is now a live, persistent Archil filesystem
await sandbox.files.write("/mnt/archil/notes.txt", "written from a sandbox")
const listing = await sandbox.commands.run("ls -la /mnt/archil")
console.log(listing.stdout)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import os
from superserve import Sandbox
sandbox = Sandbox.create(
name="archil-workspace",
from_template="archil-base",
)
sandbox.commands.run("mkdir -p /mnt/archil")
sandbox.commands.run(
"archil mount myorg/my-disk /mnt/archil --region aws-us-east-1",
env={"ARCHIL_MOUNT_TOKEN": os.environ["ARCHIL_MOUNT_TOKEN"]},
)
# /mnt/archil is now a live, persistent Archil filesystem
sandbox.files.write("/mnt/archil/notes.txt", "written from a sandbox")
listing = sandbox.commands.run("ls -la /mnt/archil")
print(listing.stdout)
```
Mounting needs root. Superserve runs commands as the template's default user (`root` unless a [`user` build step](/templates/build-spec#user-switch-user) changed it), so the default template above mounts cleanly.
## 3. Flush and unmount before you kill
Archil replicates writes across availability zones before they return, and (when the disk is connected to your own bucket) syncs them there within a few minutes. Unmount before killing the sandbox to flush any in-flight writes cleanly.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.commands.run("archil unmount /mnt/archil")
await sandbox.kill()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.commands.run("archil unmount /mnt/archil")
sandbox.kill()
```
## Share one disk across many sandboxes
Add `--shared` to mount the same disk from several sandboxes at once. Every mount is strongly read-after-write consistent, so a write from one sandbox is immediately visible to the others. That makes it a fit for parallel-agent setups that build on shared state.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.commands.run(
"archil mount myorg/my-disk /mnt/archil --region aws-us-east-1 --shared",
{ env: { ARCHIL_MOUNT_TOKEN: process.env.ARCHIL_MOUNT_TOKEN! } },
)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.commands.run(
"archil mount myorg/my-disk /mnt/archil --region aws-us-east-1 --shared",
env={"ARCHIL_MOUNT_TOKEN": os.environ["ARCHIL_MOUNT_TOKEN"]},
)
```
## How it complements pause and resume
[Pause and resume](/sandbox/lifecycle) checkpoint one sandbox's full VM state: memory, processes, and local disk. An Archil mount adds storage that lives **outside** any single sandbox: it survives `kill()`, can sync to your own object store, and can be mounted by other sandboxes concurrently. Use pause/resume to suspend a long-running job cheaply; use Archil for workspaces, caches, and datasets you want to share or keep beyond a sandbox's life.
The mount token is a credential scoped to a single disk, so mint a dedicated one for sandbox use and pass it per-command, as shown above. For credentials your sandbox code uses to authenticate *outbound* requests, such as API keys, prefer [secrets](/secrets/overview) so the real value never enters the VM.
# Cloud buckets
Source: https://docs.superserve.ai/storage/cloud-buckets
Mount an S3, GCS, or R2 bucket inside a Superserve sandbox with a FUSE driver to read and write object storage as ordinary files.
Mount a cloud storage bucket inside a sandbox with a FUSE driver so your code reads and writes objects as ordinary files. This talks to the bucket directly, with no caching layer in between, which is the simplest option when a sandbox just needs to reach an existing S3, GCS, or R2 bucket. For a faster, cache-backed filesystem over object storage, use [Archil](/storage/archil); for a versioned, branchable workspace, use [Mesa](/storage/mesa).
The setup has two parts: bake the FUSE driver into a [template](/templates/overview), then mount at runtime with `sandbox.commands.run`.
These commands run as root (the template's default user, unless a [`user` build step](/templates/build-spec#user-switch-user) changed it), so none of them use `sudo`.
## Amazon S3
Install [`s3fs`](https://github.com/s3fs-fuse/s3fs-fuse) in a template.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Template } from "@superserve/sdk"
const template = await Template.create({
name: "s3-base",
from: "ubuntu:22.04",
steps: [{ run: "apt-get update && apt-get install -y s3fs" }],
})
await template.waitUntilReady()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Template, RunStep
template = Template.create(
name="s3-base",
from_="ubuntu:22.04",
steps=[RunStep(run="apt-get update && apt-get install -y s3fs")],
)
template.wait_until_ready()
```
At runtime, write the credentials file `s3fs` expects (`ACCESS_KEY_ID:SECRET_ACCESS_KEY`), then mount the bucket.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({ name: "s3-workspace", fromTemplate: "s3-base" })
await sandbox.commands.run("mkdir -p /mnt/bucket")
await sandbox.files.write(
"/root/.passwd-s3fs",
`${process.env.AWS_ACCESS_KEY_ID}:${process.env.AWS_SECRET_ACCESS_KEY}`,
)
await sandbox.commands.run("chmod 600 /root/.passwd-s3fs")
await sandbox.commands.run(
"s3fs my-bucket /mnt/bucket -o allow_other -o passwd_file=/root/.passwd-s3fs -o endpoint=us-east-1 -o url=https://s3.us-east-1.amazonaws.com",
)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import os
from superserve import Sandbox
sandbox = Sandbox.create(name="s3-workspace", from_template="s3-base")
sandbox.commands.run("mkdir -p /mnt/bucket")
sandbox.files.write(
"/root/.passwd-s3fs",
f"{os.environ['AWS_ACCESS_KEY_ID']}:{os.environ['AWS_SECRET_ACCESS_KEY']}",
)
sandbox.commands.run("chmod 600 /root/.passwd-s3fs")
sandbox.commands.run(
"s3fs my-bucket /mnt/bucket -o allow_other -o passwd_file=/root/.passwd-s3fs -o endpoint=us-east-1 -o url=https://s3.us-east-1.amazonaws.com"
)
```
`-o allow_other` lets every process in the sandbox read the mount. Set `endpoint` and `url` to your bucket's region: the example shows `us-east-1`, but `s3fs` needs them set explicitly for any other region (e.g. `us-east-2`). For the full option list, see the [`s3fs` flags](https://manpages.ubuntu.com/manpages/jammy/man1/s3fs.1.html).
## Google Cloud Storage
Google Cloud Storage uses [`gcsfuse`](https://cloud.google.com/storage/docs/gcsfuse-cli). You'll need a bucket and a service account with the **Storage Object User** role on it, plus a [service account key](https://cloud.google.com/iam/docs/keys-create-delete).
Install `gcsfuse` from Google's apt repository in a template.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Template } from "@superserve/sdk"
const template = await Template.create({
name: "gcs-base",
from: "ubuntu:22.04",
steps: [
{ run: "apt-get update && apt-get install -y curl gnupg lsb-release" },
{
run: 'echo "deb [signed-by=/usr/share/keyrings/cloud.google.asc] https://packages.cloud.google.com/apt gcsfuse-$(lsb_release -c -s) main" > /etc/apt/sources.list.d/gcsfuse.list',
},
{
run: "curl https://packages.cloud.google.com/apt/doc/apt-key.gpg > /usr/share/keyrings/cloud.google.asc",
},
{ run: "apt-get update && apt-get install -y gcsfuse" },
],
})
await template.waitUntilReady()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Template, RunStep
template = Template.create(
name="gcs-base",
from_="ubuntu:22.04",
steps=[
RunStep(run="apt-get update && apt-get install -y curl gnupg lsb-release"),
RunStep(
run='echo "deb [signed-by=/usr/share/keyrings/cloud.google.asc] https://packages.cloud.google.com/apt gcsfuse-$(lsb_release -c -s) main" > /etc/apt/sources.list.d/gcsfuse.list'
),
RunStep(
run="curl https://packages.cloud.google.com/apt/doc/apt-key.gpg > /usr/share/keyrings/cloud.google.asc"
),
RunStep(run="apt-get update && apt-get install -y gcsfuse"),
],
)
template.wait_until_ready()
```
At runtime, write the service account key into the sandbox and mount with `--key-file`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({ name: "gcs-workspace", fromTemplate: "gcs-base" })
await sandbox.commands.run("mkdir -p /mnt/bucket")
await sandbox.files.write("/root/gcs-key.json", process.env.GCP_SERVICE_ACCOUNT_KEY!)
await sandbox.commands.run("chmod 600 /root/gcs-key.json")
await sandbox.commands.run(
"gcsfuse --key-file /root/gcs-key.json -o allow_other --file-mode=777 --dir-mode=777 my-bucket /mnt/bucket",
)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import os
from superserve import Sandbox
sandbox = Sandbox.create(name="gcs-workspace", from_template="gcs-base")
sandbox.commands.run("mkdir -p /mnt/bucket")
sandbox.files.write("/root/gcs-key.json", os.environ["GCP_SERVICE_ACCOUNT_KEY"])
sandbox.commands.run("chmod 600 /root/gcs-key.json")
sandbox.commands.run(
"gcsfuse --key-file /root/gcs-key.json -o allow_other --file-mode=777 --dir-mode=777 my-bucket /mnt/bucket"
)
```
For the full option list, see the [`gcsfuse` flags](https://cloud.google.com/storage/docs/gcsfuse-cli#options).
## Cloudflare R2
R2 is S3-compatible, so it reuses the same `s3fs` template. The only difference is the mount command, which points `s3fs` at your R2 endpoint.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({ name: "r2-workspace", fromTemplate: "s3-base" })
await sandbox.commands.run("mkdir -p /mnt/bucket")
await sandbox.files.write(
"/root/.passwd-s3fs",
`${process.env.R2_ACCESS_KEY_ID}:${process.env.R2_SECRET_ACCESS_KEY}`,
)
await sandbox.commands.run("chmod 600 /root/.passwd-s3fs")
await sandbox.commands.run(
"s3fs my-bucket /mnt/bucket -o allow_other -o passwd_file=/root/.passwd-s3fs -o url=https://.r2.cloudflarestorage.com -o use_path_request_style",
)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import os
from superserve import Sandbox
sandbox = Sandbox.create(name="r2-workspace", from_template="s3-base")
sandbox.commands.run("mkdir -p /mnt/bucket")
sandbox.files.write(
"/root/.passwd-s3fs",
f"{os.environ['R2_ACCESS_KEY_ID']}:{os.environ['R2_SECRET_ACCESS_KEY']}",
)
sandbox.commands.run("chmod 600 /root/.passwd-s3fs")
sandbox.commands.run(
"s3fs my-bucket /mnt/bucket -o allow_other -o passwd_file=/root/.passwd-s3fs -o url=https://.r2.cloudflarestorage.com -o use_path_request_style"
)
```
# Mesa
Source: https://docs.superserve.ai/storage/mesa
Mount Mesa versioned virtual filesystems inside a Superserve sandbox to persist and branch agent workspaces across runs.
[Mesa](https://mesa.dev/) is a versioned virtual filesystem you can mount inside a Superserve sandbox. Where a [cloud bucket](/storage/cloud-buckets) gives a sandbox flat object storage, Mesa adds version history and branches, so every edit is committed back and each sandbox can fork its own workspace. This guide covers the full flow: use the Mesa SDK outside the sandbox to set up resources, then the Superserve SDK to mount Mesa inside.
Mounting Mesa in a Superserve sandbox takes three steps:
1. **Outside the sandbox:** use the Mesa SDK (TypeScript or Python) to create repos, sign a short-lived access token, and orchestrate your workflow.
2. **Inside the sandbox:** install the `mesa` CLI, configure it with a short-lived access token, and run `mesa mount --daemonize` to mount your repos as local directories.
3. **Run your agent:** `cd` into the mount path and launch your agent (e.g. Claude Code, Codex, or a custom agent). Any file edits are automatically persisted back to Mesa.
For details on FUSE setup, system dependencies, and container configuration, see Mesa's [POSIX Mount guide](https://docs.mesa.dev/content/mesafs/posix-mount).
## Sandbox setup
Superserve sandboxes run on a FUSE-enabled kernel, and the Mesa installer apt-installs `fuse3` as a dependency, so the standard Mesa install script works on every Superserve template out of the box. Because Superserve sandboxes are full Firecracker microVMs, you don't need `user_allow_other` or `chmod 666 /dev/fuse`.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk";
import { Mesa } from "@mesadev/sdk";
const mesa = new Mesa({ apiKey: process.env.MESA_API_KEY });
// --- Outside the sandbox: set up Mesa resources ---
// Create a repo (or use an existing one)
const repo = await mesa.repos.create({ name: "agent-workspace" });
// Sign a scoped, self-expiring access token for the sandbox. Signed locally
// from your API key with no network call, so your API key never enters the sandbox.
const { token } = await mesa.tokens.create({
scopes: ["read", "write"],
repos: ["my-org/agent-workspace"],
ttl_seconds: 3600, // 1 hour
});
// --- Inside the sandbox: install and mount Mesa ---
const sandbox = await Sandbox.create({ fromTemplate: "superserve/base" });
// Install the Mesa CLI. The installer apt-installs fuse3 as a dependency.
await sandbox.commands.run(
"curl -fsSL https://mesa.dev/install.sh | sh -s -- --yes",
);
// Start Mesa as a background daemon. MESA_ORG and MESA_API_KEY are read
// from the environment, never persisted to disk.
await sandbox.commands.run("mesa mount -d", {
env: {
MESA_ORG: "my-org",
MESA_API_KEY: token,
},
});
// --- Run your agent ---
await sandbox.commands.run(
'cd ~/.local/share/mesa/mnt/my-org/agent-workspace && claude "Implement the feature described in TODO.md"',
);
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import asyncio
import os
from mesa_sdk import Mesa
from superserve import Sandbox
async def main():
# --- Outside the sandbox: set up Mesa resources (the Mesa SDK is async) ---
mesa = Mesa(api_key=os.environ["MESA_API_KEY"])
# Create a repo (or use an existing one)
repo = await mesa.repos.create(name="agent-workspace")
# Sign a scoped, self-expiring access token for the sandbox. Signed locally
# from your API key with no network call, so your API key never enters the sandbox.
minted = await mesa.tokens.create(
scopes=["read", "write"],
repos=["my-org/agent-workspace"],
ttl_seconds=3600, # 1 hour
)
# --- Inside the sandbox: install and mount Mesa ---
sandbox = Sandbox.create(from_template="superserve/base")
# Install the Mesa CLI. The installer apt-installs fuse3 as a dependency.
sandbox.commands.run("curl -fsSL https://mesa.dev/install.sh | sh -s -- --yes")
# Start Mesa as a background daemon. MESA_ORG and MESA_API_KEY are read
# from the environment, never persisted to disk.
sandbox.commands.run(
"mesa mount -d",
env={"MESA_ORG": "my-org", "MESA_API_KEY": minted.token},
)
# --- Run your agent ---
sandbox.commands.run(
'cd ~/.local/share/mesa/mnt/my-org/agent-workspace '
'&& claude "Implement the feature described in TODO.md"'
)
asyncio.run(main())
```
```bash CLI theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# Install the Mesa CLI. The installer apt-installs fuse3 as a dependency.
curl -fsSL https://mesa.dev/install.sh | sh -s -- --yes
# Start Mesa as a background daemon. MESA_ORG and MESA_API_KEY are read
# from the environment, never persisted to disk.
MESA_ORG="my-org" MESA_API_KEY="$MESA_TOKEN" mesa mount -d
# Run your agent
cd ~/.local/share/mesa/mnt/my-org/agent-workspace
claude "Implement the feature described in TODO.md"
```
## Custom template
For faster startup, pre-install the Mesa CLI into a [custom Superserve template](/templates/create) so each sandbox boots with the Mesa CLI already installed:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Template } from "@superserve/sdk";
const template = await Template.create({
name: "agent-with-mesa",
vcpu: 2,
memoryMib: 2048,
diskMib: 4096,
from: "ubuntu:24.04",
steps: [
{
run: "apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*",
},
{ run: "curl -fsSL https://mesa.dev/install.sh | sh -s -- --yes" },
],
});
await template.waitUntilReady();
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Template, RunStep
template = Template.create(
name="agent-with-mesa",
vcpu=2,
memory_mib=2048,
disk_mib=4096,
from_="ubuntu:24.04",
steps=[
RunStep(
run="apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*"
),
RunStep(run="curl -fsSL https://mesa.dev/install.sh | sh -s -- --yes"),
],
)
template.wait_until_ready()
```
```dockerfile Dockerfile theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://mesa.dev/install.sh | sh -s -- --yes
```
Reference it on sandbox creation and skip the runtime install step:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const sandbox = await Sandbox.create({ fromTemplate: "agent-with-mesa" });
await sandbox.commands.run("mesa mount -d", {
env: {
MESA_ORG: "my-org",
MESA_API_KEY: token,
},
});
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox = Sandbox.create(from_template="agent-with-mesa")
sandbox.commands.run(
"mesa mount -d",
env={"MESA_ORG": "my-org", "MESA_API_KEY": minted.token},
)
```
```bash CLI theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
MESA_ORG="my-org" MESA_API_KEY="$MESA_TOKEN" mesa mount -d
```
## Tips
* **Use scoped, short-lived access tokens.** Sign a dedicated token for each sandbox session with only the scopes it needs. It's signed locally from your API key (which never enters the sandbox) and expires on its own. See Mesa's [Authentication](https://docs.mesa.dev/content/concepts/authentication) docs for details.
* **Use `--daemonize`.** Always run `mesa mount --daemonize` in sandbox environments so Mesa runs as a background process and doesn't block your agent's terminal.
* **Build a custom template** for production use. Pre-installing the Mesa CLI avoids the install overhead on every sandbox creation.
* **Mount path follows `$HOME`.** Superserve's guest agent sets `HOME=/home/user`, so the mount lands at `/home/user/.local/share/mesa/mnt//`. Use `~/.local/share/mesa/mnt/...` and it resolves correctly.
# BuildSpec reference
Source: https://docs.superserve.ai/templates/build-spec
How to describe a template's base image, build steps, and runtime defaults
A `BuildSpec` is the canonical declaration of how to build a template. The SDK flattens it onto `TemplateCreateOptions` for convenience: `from` / `steps` / `startCmd` / `readyCmd` are top-level options.
## `from`: base image
An OCI image reference. Resolved to a digest at build time for reproducibility.
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from: "python:3.11" // Docker Hub
from: "ghcr.io/myorg/foo:v1" // any OCI registry
```
**Constraints:**
* Must be a **linux/amd64** image
* **Alpine bases are rejected**
* **Distroless bases are rejected** — they ship no shell, so `run` steps can't execute
## `steps`: ordered build steps
A list of tagged objects executed in order inside the build VM. Exactly one of `run` / `env` / `workdir` / `user` must be set per step.
### `run`: shell command
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
{
run: "pip install -r requirements.txt"
}
```
Wrapped in `/bin/sh -c` inside the build VM.
### `env`: environment variable
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
{ env: { key: "DEBUG", value: "1" } }
```
Sets an env var for subsequent build steps AND as a runtime default. Caller-supplied `envVars` on sandbox create override on conflict.
### `workdir`: working directory
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
{
workdir: "/srv/app"
}
```
Working directory for subsequent build steps and the runtime default cwd. Auto-created and chowned to the current build user. Per-exec `workingDir` overrides at runtime.
### `user`: switch user
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
{ user: { name: "appuser", sudo: true } }
```
Switches the user for subsequent build steps and sets the runtime default exec user. User is created if not present. `sudo: true` grants passwordless sudo.
## `startCmd`: process to start after build
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
startCmd: "python server.py"
```
The snapshot captures the running process, so sandboxes restored from this template come up with it already live.
## `readyCmd`: readiness probe
```typescript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
readyCmd: "curl -f http://localhost:8080/health"
```
Polled every 2s after `startCmd` until it exits `0` or 10 minutes elapse. Use this to wait for a server to bind its port before snapshotting.
## Resource limits
Templates specify the VM shape; sandboxes inherit these and cannot override them per-sandbox.
| Field | Min | Max | Default |
| ----------- | ---- | ---- | ------- |
| `vcpu` | 1 | 4 | 1 |
| `memoryMib` | 256 | 4096 | 1024 |
| `diskMib` | 1024 | 8192 | 4096 |
The **Max** column is the platform ceiling. New teams start with a lower per-team cap (2 vCPU, 2048 MiB memory); email [support@superserve.ai](mailto:support@superserve.ai) to raise it.
## Build error codes
When a build lands on `failed`, `BuildError.code` carries one of these stable identifiers (the SDK parses it from the `": "` prefix on `errorMessage`):
| Code | Meaning |
| ---------------------- | -------------------------------------------------------------------------------------------- |
| `bad_reference` | The `from` image reference is malformed (expected `python:3.11` or `ghcr.io/org/image:tag`). |
| `unsupported_platform` | The base image isn't `linux/amd64`. |
| `image_pull_failed` | Base image couldn't be pulled or resolved — bad reference, or the registry was unreachable. |
| `image_unsafe` | Image rejected for unsafe paths (tar / symlink / hardlink entries escaping the root). |
| `image_too_large` | Flattened image exceeds the requested `diskMib`; raise `diskMib`. |
| `step_failed` | A build step exited non-zero. The message includes the failing step and its exit code. |
| `boot_failed` | Build VM failed to boot. |
| `snapshot_failed` | Snapshot couldn't be captured. |
| `start_cmd_failed` | `startCmd` didn't launch successfully. |
| `ready_cmd_failed` | `readyCmd` didn't exit `0` within 10 minutes. |
| `dispatch_failed` | The build host was unreachable when the build was dispatched. |
| `build_failed` | Catch-all when no specific code applies. |
`Template.create` / `rebuild` can also be rejected **before a build starts** with a `RateLimitError` — `too_many_builds` (team's concurrent-build limit) or `too_many_templates` (team's template-count limit). These are not `BuildError` codes; see [Errors](/errors#ratelimiterror).
## Related
* [Create a template](/templates/create)
* [Errors](/errors)
# Create a template
Source: https://docs.superserve.ai/templates/create
Build your first team template, stream logs, launch a sandbox from it
Build a reusable sandbox environment in three steps: define the template, wait for the build to finish, then create sandboxes from it.
## Create the template
Pass a base image via `from` and (optionally) a list of build steps that run inside the build VM.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Template } from "@superserve/sdk"
const template = await Template.create({
name: "my-python-env",
vcpu: 2,
memoryMib: 2048,
from: "python:3.11",
steps: [
{ run: "pip install numpy pandas" },
{ env: { key: "DEBUG", value: "1" } },
{ workdir: "/app" },
],
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Template
from superserve import RunStep, EnvStep, EnvStepValue, WorkdirStep
template = Template.create(
name="my-python-env",
vcpu=2,
memory_mib=2048,
from_="python:3.11",
steps=[
RunStep(run="pip install numpy pandas"),
EnvStep(env=EnvStepValue(key="DEBUG", value="1")),
WorkdirStep(workdir="/app"),
],
)
```
`Template.create` returns once the template is registered and the first build is queued, **not** when the build is finished. Call `waitUntilReady()` (TS) / `wait_until_ready()` (Python) to block until the build reaches a terminal state.
## Stream build logs
`waitUntilReady` accepts an `onLog` callback that receives each SSE event as the build runs.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await template.waitUntilReady({
onLog: (event) => {
if (event.stream === "system") return // skip system messages
process.stdout.write(event.text)
},
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
def print_log(ev):
if ev.stream.value != "system":
print(ev.text, end="")
template.wait_until_ready(on_log=print_log)
```
On success, `waitUntilReady` returns the refreshed `TemplateInfo` with `status = "ready"`. On failure, it raises `BuildError` with a machine-readable `code` (e.g. `step_failed`, `image_pull_failed`).
## Create a sandbox from the template
Once ready, create sandboxes by passing the `Template` instance, the name, or the UUID.
```typescript TypeScript {13,19,25} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox, Template } from "@superserve/sdk"
// Create the template first (see the section above)
const template = await Template.create({
name: "my-python-env",
from: "python:3.11",
})
await template.waitUntilReady()
// Option 1: pass the Template instance
const sandbox = await Sandbox.create({
name: "run-1",
fromTemplate: template,
})
// Option 2: pass the name
const sandbox2 = await Sandbox.create({
name: "run-2",
fromTemplate: "my-python-env",
})
// Option 3: pass the ID
const sandbox3 = await Sandbox.create({
name: "run-3",
fromTemplate: "8b050452",
})
```
```python Python {13,19,25} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox, Template
# Create the template first (see the section above)
template = Template.create(
name="my-python-env",
from_="python:3.11",
)
template.wait_until_ready()
# Option 1: pass the Template instance
sandbox = Sandbox.create(
name="run-1",
from_template=template,
)
# Option 2: pass the name
sandbox2 = Sandbox.create(
name="run-2",
from_template="my-python-env",
)
# Option 3: pass the ID
sandbox3 = Sandbox.create(
name="run-3",
from_template="8b050452",
)
```
The sandbox's vCPU, memory, and disk size are inherited from the template; they cannot be overridden at sandbox creation (the snapshot dictates VM shape).
## Related
* [Templates overview](/templates/overview)
* [Rebuild, cancel, delete](/templates/lifecycle)
* [BuildSpec reference](/templates/build-spec)
* [SDK reference: Template](/sdk-reference/template)
# Rebuild, cancel, delete
Source: https://docs.superserve.ai/templates/lifecycle
Manage a template over time: trigger new builds, cancel in-flight ones, delete stale templates
## Rebuild
Queue a new build for an existing template, e.g. after a failed build, or to pick up an updated base image.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const build = await template.rebuild()
console.log(`Build queued: ${build.id}`)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
build = template.rebuild()
print(f"Build queued: {build.id}")
```
Rebuild is **idempotent**: if an in-flight build already exists with the same configuration, you get back the existing build instead of a duplicate.
## List and inspect builds
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const builds = await template.listBuilds({ limit: 10 })
for (const b of builds) {
console.log(b.id, b.status, b.errorMessage ?? "")
}
const build = await template.getBuild(builds[0].id)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
builds = template.list_builds(limit=10)
for b in builds:
print(b.id, b.status, b.error_message or "")
build = template.get_build(builds[0].id)
```
## Cancel an in-flight build
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await template.cancelBuild(build.id)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
template.cancel_build(build.id)
```
`cancelBuild` is a no-op for builds already in a terminal state.
## Delete a template
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await template.delete()
// Or by UUID
await Template.deleteById("7a3f2b8c-1234-...")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
template.delete()
# Or by UUID
Template.delete_by_id("7a3f2b8c-1234-...")
```
Both calls are idempotent on `404`. If any non-destroyed sandbox still references the template, the call raises `ConflictError`; destroy those sandboxes first. Once the delete succeeds the template's `name` is released for reuse — you can immediately create a new template with the same name.
## Related
* [Create a template](/templates/create)
* [BuildSpec reference](/templates/build-spec)
* [SDK reference: Template](/sdk-reference/template)
# Templates
Source: https://docs.superserve.ai/templates/overview
Reusable base images with build steps that sandboxes boot from
A **template** is a snapshot of a Linux filesystem (base image, build steps, and optionally a long-running process) that sandboxes boot from. Templates let you pre-install dependencies once and launch identically configured sandboxes in seconds.
Every sandbox is created from a template. If you don't specify one, Superserve defaults to `superserve/base`.
## System templates
Curated by Superserve and available to every team. Identified by the `superserve/` name prefix (which is reserved — team templates can't use it). Each is built from an OCI base image plus a few `apt` / `pip` / `npm` steps. **Every template ships `ca-certificates`, `curl`, and `git`** — the column below lists only what each adds on top.
| Name | Base image | Shape (vCPU · mem · disk) | Adds on top of the base toolkit |
| ----------------------------- | ------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `superserve/base` | `ubuntu:24.04` | 1 · 1024 MiB · 4096 MiB | Nothing — just the base toolkit. Default when `fromTemplate` is omitted. |
| `superserve/python-3.11` | `python:3.11-slim` | 1 · 1024 MiB · 4096 MiB | `requests` (pip). |
| `superserve/node-22` | `node:22-slim` | 1 · 1024 MiB · 4096 MiB | `pnpm`, `typescript` (npm global). |
| `superserve/python-ml` | `python:3.11` | 2 · 2048 MiB · 4096 MiB | `build-essential` (apt); numpy, pandas, scikit-learn, matplotlib, jupyter, ipython, requests (pip). |
| `superserve/code-interpreter` | `python:3.11` | 2 · 2048 MiB · 8192 MiB | `build-essential`, `libfreetype6-dev`, `libpng-dev` (apt); numpy, pandas, scikit-learn, matplotlib, jupyter, ipython, requests, pillow, seaborn, plotly, openpyxl (pip). |
| `superserve/claude-code` | `ubuntu:24.04` | 2 · 2048 MiB · 8192 MiB | `build-essential`, `ripgrep`, `vim`, `python3`, `python3-pip` (apt); Claude Code CLI (`claude`). |
| `superserve/openclaw` | `ubuntu:24.04` | 2 · 2048 MiB · 8192 MiB | `tmux` (apt); OpenClaw CLI. |
| `superserve/hermes` | `ubuntu:24.04` | 2 · 2048 MiB · 8192 MiB | `tmux`, `xz-utils` (apt); Hermes agent. |
A sandbox inherits its VM shape (vCPU, memory, disk) from the template it boots from — see [BuildSpec reference](/templates/build-spec) for the limits.
## Team templates
Team-owned templates let you bake in team-specific dependencies (e.g. `my-python-env` with scientific libs pre-installed). They're created via `Template.create()` and referenced by name.
Team template names **cannot** start with `superserve/`; that prefix is reserved. Names are unique per team among non-deleted templates and are released for reuse the moment a template is deleted.
## Names vs UUIDs
When booting a sandbox, `Sandbox.create({ fromTemplate })` accepts either the template's name (`my-python-env`) or its UUID. Names are more readable; UUIDs are stable across rename/delete.
## Related
* [Create a template](/templates/create)
* [Template lifecycle: rebuild, cancel, delete](/templates/lifecycle)
* [BuildSpec reference](/templates/build-spec)
* [SDK reference: Template](/sdk-reference/template)