# 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).
# Create a Stripe Checkout session
Source: https://docs.superserve.ai/api-reference/billing/create-a-stripe-checkout-session
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /stripe/checkout-session
# Create a Stripe customer portal session
Source: https://docs.superserve.ai/api-reference/billing/create-a-stripe-customer-portal-session
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /stripe/customer-portal-session
# Create or retry Checkout with a trusted publication decision
Source: https://docs.superserve.ai/api-reference/billing/create-or-retry-checkout-with-a-trusted-publication-decision
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /stripe/checkout-session/publication-decision
Requires the customer credential's authenticated actor, billing:write,
live billing, and a server-signed publication assertion. The assertion
must use EdDSA, issuer promotion-auth-adapter, audience promotion-account,
operation checkout, and a positive lifetime of at most five minutes.
Its sub and team_id must match the customer credential, and its
operation_id, home_region, decision, success_url and cancel_url must
exactly match the body. The region must match the receiving cell.
The decision is committed with the fresh generation before Stripe
session creation. publication_failed preserves paid access without new
automatic promotion credit; standard uses existing eligibility checks.
Retry the same operation and body with a renewed assertion after a lost
response. Existing generations retain their original payer and decision.
A changed or retired operation conflicts; do not fall back to the legacy
creation route on an error or an older server's missing route.
Bodies over 4096 bytes and unknown fields are rejected. Redirects must
match the regional API's allowed billing redirect configuration.
# 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.
# Get team billing usage
Source: https://docs.superserve.ai/api-reference/billing/get-team-billing-usage
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /teams/{team_id}/billing/usage
Returns billing usage for the specified team and billing period.
Customer callers may only access their own team and must hold
`billing:read`.
# Get timezone-aware billing usage chart buckets
Source: https://docs.superserve.ai/api-reference/billing/get-timezone-aware-billing-usage-chart-buckets
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /billing/usage-series
Returns zero-filled local-calendar buckets for the half-open [start,end)
range. `timezone` must be an IANA timezone name (for example,
`America/Chicago`). The response is limited to 400 buckets. Every
request requires the authenticated team's `billing:read` permission.
`billed_total_usd` is gross CPU and memory usage cost before credits,
taxes, or payments; storage `cost_usd` is an informational equivalent
while storage remains tracked-only and is excluded from that total.
# List team billing periods
Source: https://docs.superserve.ai/api-reference/billing/list-team-billing-periods
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /teams/{team_id}/billing/periods
Lists persisted billing periods for the specified team. Customer
callers may only access their own team and must hold `billing:read`.
# Preview team billing export payload
Source: https://docs.superserve.ai/api-reference/billing/preview-team-billing-export-payload
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /teams/{team_id}/billing/periods/{period_id}/export-preview
# Receive Stripe webhooks
Source: https://docs.superserve.ai/api-reference/billing/receive-stripe-webhooks
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /stripe/webhook
Stripe webhook receiver. Requests are authenticated with the Stripe
signature header instead of an API key.
# Recover an existing Stripe Checkout session
Source: https://docs.superserve.ai/api-reference/billing/recover-an-existing-stripe-checkout-session
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /stripe/checkout-session/recover
Requires the original authenticated billing actor, billing:write and live
billing. Retrieves only the recorded open session, retaining its original
actor, generation and immutable identity evidence without refreshing the
evidence. Never creates a customer, session, subscription or replacement
generation. The original tuple is revalidated under database locks after
provider retrieval; concurrent completion, abandonment or replacement
makes recovery unavailable.
An empty body or {} recovers only the generation observed at the first
backend read. An optional timestamp restricts recovery further. Unknown
fields, null bodies, trailing JSON and bodies over 4 KiB are rejected.
A 409 does not prove an ambiguous create failed or release its fence.
Fresh trusted evidence must be published before attempting creation;
evidence failure or an older server's 404 must not trigger create fallback.
# 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 output in a single
response, retained up to a server-side cap (see `truncated`). For live
or unbounded 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"}`.
Client frames are capped at 4 MiB (servers may enforce as low as
64 KiB during rollout); frames over the cap close the socket with
code 1009, 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; a pause still finishing is waited out briefly, then resumed. 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), or from a saved snapshot named by
`from_snapshot`. When the request returns successfully, the
sandbox is ready to use — you can run commands against it
immediately.
New sandboxes use strict preview routing: only ports explicitly
published through `/sandboxes/{sandbox_id}/preview-ports` are reachable.
`preview_access` defaults newly published ports to `public` or `private`;
each publication may override that default independently.
# 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` (one value or comma-separated values), 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.
# List published preview ports
Source: https://docs.superserve.ai/api-reference/sandboxes/list-published-preview-ports
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /sandboxes/{sandbox_id}/preview-ports
Returns the sandbox's preview routing mode and its published ports.
For a strict `public` or `private` sandbox, only the ports listed here
are published at `https://{port}-{sandbox_id}.`; every
other numeric port returns 404. Each row reports its independent access
mode. Private ports require a port-scoped token. Clients may send it in
a request header named `X-Superserve-Preview-Token`, or use the signed-link
query bootstrap described by `PreviewTokenResponse`; unknown ports
remain closed. An older `legacy_public` sandbox may still route every
listening port until it is updated to a strict mode.
# Mint a token for a private preview port
Source: https://docs.superserve.ai/api-reference/sandboxes/mint-a-token-for-a-private-preview-port
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /sandboxes/{sandbox_id}/preview-ports/{port}/token
Activates token authentication for an already-published private port
and returns a credential scoped to exactly this sandbox, port, and
token generation. A machine client sends the response's `token` value
in a request header whose name is the response's `header` value.
For browser navigation, construct a signed URL by adding the URL-encoded
`token` under the query-parameter name returned in `query_param`. An
ordinary `GET` exchanges a valid query token for a secure host-only
cookie and returns a `302` redirect to the same-origin HTTPS URL with
the credential removed. The bootstrap response is `Cache-Control:
no-store` and `Referrer-Policy: no-referrer`. Non-`GET` requests and
complete WebSocket upgrade handshakes authenticate a valid query token
directly because they cannot round-trip that redirect. The edge strips
all preview-token header, query, and cookie carriers before forwarding
any request to sandbox code.
Public or unpublished ports cannot mint credentials. Minting requires
sandbox write access; an incapable or stale host is rejected rather
than activating a policy it cannot enforce.
The request body is optional. Omit it (or send `{}`) for a token that
remains valid until rotation, access-mode change, or unpublication.
An explicit expiry is also enforced for a cookie bootstrapped from that
token, even if the browser still stores the cookie.
# 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.
- `preview_access` — sets the default access mode for ports published
in the future. Existing per-port modes are unchanged. It also moves
an older `legacy_public` sandbox to strict routing. `legacy_public`
cannot be selected.
# 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.
The request holds until the pause is decided; if the host has not
answered by then, the response is `202 Accepted` and the pause
continues in the background. Send `Prefer: respond-async` to get
`202` as soon as the pause is recorded instead. Either way
`GET /sandboxes/{sandbox_id}` reports `paused` once it lands (or
`failed` if the VM turned out to be gone).
# Publish or update a preview port
Source: https://docs.superserve.ai/api-reference/sandboxes/publish-or-update-a-preview-port
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /sandboxes/{sandbox_id}/preview-ports
Adds a port to the sandbox's published set, optionally with an explicit
`public` or `private` mode. On a new row, omitted `access` inherits the
sandbox's current `preview_access` default. On an existing row, omitted
`access` preserves that row's current mode; only an explicit value
changes it. An explicit private publication is rejected while the
sandbox remains `legacy_public`, because legacy routing exposes all ports.
# 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.
A pause still finishing is waited out briefly before the resume.
# Rotate a private preview port's token generation
Source: https://docs.superserve.ai/api-reference/sandboxes/rotate-a-private-preview-ports-token-generation
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /sandboxes/{sandbox_id}/preview-ports/{port}/token/rotate
Advances only this published private port's token generation, revoking
every older token for it while leaving sibling ports unchanged. The
revocation is committed even if the current host cannot deliver or
enforce the replacement; in that case the request returns an error and
no credential. Rotation requires sandbox write access. A successful
response contains a fresh non-expiring token for the new generation and
identifies both its header and signed-link query carriers. Rotation
immediately makes older browser cookies unusable because every request
revalidates the cookie's embedded generation.
# Unpublish a preview port
Source: https://docs.superserve.ai/api-reference/sandboxes/unpublish-a-preview-port
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml delete /sandboxes/{sandbox_id}/preview-ports/{port}
Removes a port from the published set. On a strict sandbox, the port
immediately returns 404 at the edge.
The operation is idempotent and retry-safe. Repeating the request,
including for a port that is already unpublished, re-synchronizes the
host's authoritative published-port set and returns `204`. Retry a
`500` to converge after a transient host update failure. A `404`
means the sandbox itself does not exist.
# 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.
# Delete a snapshot
Source: https://docs.superserve.ai/api-reference/snapshots/delete-a-snapshot
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml delete /snapshots/{snapshot_id}
Removes the snapshot. A snapshot still being created cannot be
deleted until it is ready, has failed, or has been creating for an
hour without its host settling it.
# Get a snapshot
Source: https://docs.superserve.ai/api-reference/snapshots/get-a-snapshot
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /snapshots/{snapshot_id}
# List a sandbox's snapshots
Source: https://docs.superserve.ai/api-reference/snapshots/list-a-sandboxs-snapshots
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml get /sandboxes/{sandbox_id}/snapshots
Snapshots of the sandbox that have not been deleted, newest first.
Snapshots outlive their sandbox, so this lists them for a deleted
sandbox too.
# Rename a snapshot
Source: https://docs.superserve.ai/api-reference/snapshots/rename-a-snapshot
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml patch /snapshots/{snapshot_id}
# Snapshot a sandbox
Source: https://docs.superserve.ai/api-reference/snapshots/snapshot-a-sandbox
https://raw.githubusercontent.com/superserve-ai/sandbox/refs/heads/main/api/openapi.yaml post /sandboxes/{sandbox_id}/snapshot
Captures the sandbox into a saved snapshot of its memory and disk,
kept until deleted. A new sandbox is created from it with
`from_snapshot` on `POST /sandboxes`, resuming with its processes
intact. The sandbox must be active or paused. A running sandbox is
paused for the capture: under a second for the first memory snapshot
of a sandbox, longer when all of memory has to be written. The call
returns once the snapshot is ready; if the host's answer is lost on
the way, it returns the snapshot still `creating` and the platform
settles it with the host shortly. Snapshots outlive the sandbox they
came from.
Pass `idempotency_key` to make a retry return the snapshot the first
request made instead of taking another, whatever became of it since.
# 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
```
# 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).
# LangChain
Source: https://docs.superserve.ai/integrations/agent-harnesses/langchain
Run Deep Agents, LangGraph, and LangChain agents in isolated Superserve sandboxes with langchain-superserve.
`langchain-superserve` adapts a Superserve sandbox to the Deep Agents sandbox protocol. Because Deep Agents compiles to a LangGraph graph, the same backend works across LangChain, LangGraph, and Deep Agents — agent shell commands and file operations run inside an isolated Firecracker microVM instead of on your machine.
## Setup
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
pip install langchain-superserve
export ANTHROPIC_API_KEY=sk-ant-...
export SUPERSERVE_API_KEY=ss_live_...
```
Requires Python 3.11+. `langchain-anthropic` ships as a base dependency of Deep Agents, so the Anthropic model used below is already installed. See [API keys](/api-key) to create a Superserve API key.
## Deep Agents SDK
Launch a sandbox from a python-equipped template, wrap it in `SuperserveSandbox`, and pass it as the `backend` to `create_deep_agent(...)` — the agent's shell and file operations then run inside the sandbox:
```python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from deepagents import create_deep_agent
from superserve import Sandbox
from langchain_superserve import SuperserveSandbox
sandbox = Sandbox.create(name="my-agent", from_template="superserve/python-3.11")
try:
backend = SuperserveSandbox(sandbox=sandbox)
agent = create_deep_agent(
model="anthropic:claude-sonnet-5",
backend=backend,
system_prompt="You are a coding assistant with a sandboxed shell.",
)
result = agent.invoke(
{"messages": "List the files in /tmp and tell me how many there are."}
)
print(result["messages"][-1].content)
finally:
sandbox.kill()
```
`SuperserveSandbox` synthesizes `ls`/`read`/`edit`/`glob` with in-sandbox `python3`, so launch from a python-equipped image — `superserve/python-3.11`, `superserve/python-ml`, or `superserve/code-interpreter`. `execute`, `grep`, and file upload/download work on any image, including the minimal `superserve/base`.
## Deep Agents Code
Installing `langchain-superserve` also registers a `superserve` sandbox provider for [Deep Agents Code](https://docs.langchain.com/deepagents-code) (`dcode`), the terminal coding agent built on the Deep Agents SDK:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
curl -LsSf https://langch.in/dcode | bash
dcode --install langchain-superserve --package
export SUPERSERVE_API_KEY=ss_live_...
dcode --sandbox superserve
```
The provider defaults to the `superserve/python-3.11` template; override it with the `SUPERSERVE_TEMPLATE` environment variable.
## Superserve extras
Beyond the standard Deep Agents backend, Superserve sandboxes support `pause()`/`resume()` for idle cost savings and proxy-brokered [secret binding](/secrets/binding) (`Sandbox.create(secrets=...)`) so real credentials never enter the sandbox. Manage these through the Superserve SDK on the sandbox you pass to `SuperserveSandbox`.
## Resources
Browse the source, tests, and release notes.
Install the package and check released versions.
# 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.
# Run Cursor Self-Hosted Machines on Superserve
Source: https://docs.superserve.ai/integrations/managed-agents/cursor-cloud-agents
Get the control of Cursor Self-Hosted Machines without having to manage a fleet of machines. Every Cloud Agent gets its own Superserve sandbox, built from your image, deleted or paused when it's done.
[Cursor Cloud Agents](https://cursor.com/docs/cloud-agent) take a task from Cursor, Slack, GitHub, or Linear and come back with a pull request. By default they run on VMs in Cursor's cloud. [Self-Hosted Machines](https://cursor.com/docs/cloud-agent/self-hosted) lets you move the part that touches your code, the checkout, the file edits, and the shell commands, onto machines you control. Cursor keeps running the agent loop, the model calls, and the Cloud Agent experience your team already uses.
With Self-Hosted Machines you decide which image the agent runs on, what it can reach, and what happens to the workspace when it's done. A Team Pool also means managing a fleet of machines, and Cursor's docs are clear about what that involves: keeping enough workers online, patching images, resetting machines between runs, scaling for bursts, and cleaning up after every session.
Superserve gives you the control without having to manage a fleet of machines. Point Cursor's worker controller at the spawn hook in this guide, and every Cloud Agent that lands in your pool gets its own sandbox: created for that request, booted from a template you built, limited to an egress allowlist if you set one, and deleted the moment the worker exits. Follow-ups don't have to start cold either. A sandbox can pause between turns at no compute cost and resume in under 50ms with the checkout and caches intact, which is the hibernation model Cursor designed pools around. Nothing runs before a request arrives, and nothing lingers after.
This is for you if:
* Security or procurement asked where agent commands run, and you'd rather answer with an image, an egress allowlist, and a per-connection log than with a fleet you operate.
* Your environment doesn't fit a Cloud Agent build: a custom base image, a heavy toolchain, or a large checkout you want to keep warm across follow-ups.
* Agents write a real share of your pull requests now, and idle machines have become the cost problem.
If Cursor-hosted Cloud Agents already meet your requirements, stay there. Cursor recommends them for most teams, and so do we. This guide is for the teams that need a pool.
## How it works
* **Cursor** runs the agent loop and a per-team queue of pending pool requests. Each worker opens one outbound HTTPS connection to Cursor, and Cursor sends tool calls over it. Nothing connects into your infrastructure.
* **You** run Cursor's `agent worker controller` on a host you manage, pointed at a spawn hook from this guide. The controller claims requests from the queue and runs the hook once per claim.
* **Superserve** provides the workers. The spawn hook creates a sandbox and starts `agent worker` inside it under the worker id Cursor assigned to the claim. A monitor process recycles the sandbox when the worker exits.
The worker clones the requested repo with a short-lived GitHub token that Cursor mints per run, serves the session, then exits after an idle window.
A [reference
implementation](https://github.com/superserve-ai/superserve/tree/main/guides/managed-agents/cursor-cloud-agents)
with the template builder, spawn hook, and monitor is available on GitHub in
both TypeScript and Python.
## Prerequisites
* A [Superserve account](https://console.superserve.ai) and API key
* A Cursor Enterprise plan with Self-Hosted Machines enabled by a team admin
* A Cursor **service-account** API key. Personal, team, and organization keys cannot start pool workers.
* A Linux or macOS controller host with the Cursor CLI, outbound HTTPS, and Node.js 22+ or Python 3.12+
## Configure your Cursor team
A team admin enables three things in the Cursor dashboard:
1. **Allow Self-Hosted Machines** under **Cloud Agents → Self-Hosted**. This adds the pool picker to Cloud Agent creation.
2. **GitHub token minting for self-hosted workers**, on the same page. The worker starts with `--clone-git-repos`, which asks Cursor for a repo-scoped token at claim time. Without minting, workers connect but cannot check anything out.
3. **The Cursor GitHub App** under **Settings → Integrations**, connected at the team level with access to every repo the pool will serve. Minted tokens inherit their permissions from the app.
Then create a service account under **Settings → API Keys → Service Accounts** and export its key alongside your Superserve key on the controller host:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
export SUPERSERVE_API_KEY="ss_live_..."
export CURSOR_API_KEY=""
```
## Build the worker template
Build a template with the Cursor CLI, `git`, and a `/workspace` directory. Sandboxes created from it boot in under 50ms with the CLI already on `PATH`, so nothing is downloaded at claim time.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Template } from "@superserve/sdk"
const template = await Template.create({
name: "cursor-worker",
from: "ubuntu:24.04",
vcpu: 2,
memoryMib: 2048,
diskMib: 8192,
steps: [
{
run:
"apt-get update && apt-get install -y --no-install-recommends " +
"ca-certificates curl git jq procps unzip && rm -rf /var/lib/apt/lists/*",
},
{ run: "curl -fsS https://cursor.com/install | HOME=/root bash" },
{ run: "ln -sf /root/.local/bin/agent /usr/local/bin/agent && agent --version" },
{ run: "mkdir -p /workspace /var/lib/cursor-worker" },
{ 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 RunStep, Template, WorkdirStep
template = Template.create(
name="cursor-worker",
from_="ubuntu:24.04",
vcpu=2,
memory_mib=2048,
disk_mib=8192,
steps=[
RunStep(
run="apt-get update && apt-get install -y --no-install-recommends "
"ca-certificates curl git jq procps unzip && rm -rf /var/lib/apt/lists/*"
),
RunStep(run="curl -fsS https://cursor.com/install | HOME=/root bash"),
RunStep(run="ln -sf /root/.local/bin/agent /usr/local/bin/agent && agent --version"),
RunStep(run="mkdir -p /workspace /var/lib/cursor-worker"),
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 worker boots from it. The `agent --version` step fails the build immediately if the CLI download or install broke, rather than at the first claim.
Add the runtimes, package caches, and internal CA certificates your repos
need to the build steps. The snapshot captures the full filesystem, so
workers inherit everything with no install cost at claim time.
## Run the controller
Clone the reference implementation, add both keys to `.env`, and build the template:
```bash TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
git clone https://github.com/superserve-ai/superserve.git
cd superserve/guides/managed-agents/cursor-cloud-agents/typescript
npm install
cp .env.example .env
node build-template.mjs
```
```bash Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
git clone https://github.com/superserve-ai/superserve.git
cd superserve/guides/managed-agents/cursor-cloud-agents/python
uv venv && uv pip install -e .
cp .env.example .env
.venv/bin/python build_template.py
```
The controller ships with the Cursor CLI. Install it on the same host:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
curl https://cursor.com/install -fsS | bash
export PATH="$HOME/.local/bin:$PATH"
agent worker controller --help
```
Start the controller and the monitor as two separate processes. Both run until stopped - keep them under systemd, a container, or your own process manager. The scripts read `.env` themselves, but the controller is a Cursor binary and does not, so export it into that shell first.
```bash TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# terminal 1: the controller
set -a && . ./.env && set +a
agent worker controller --spawn "$(pwd)/spawn.mjs" --pool superserve
# terminal 2: the monitor
node monitor.mjs
```
```bash Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# terminal 1: the controller
set -a && . ./.env && set +a
agent worker controller --spawn "$(pwd)/spawn.sh" --pool superserve
# terminal 2: the monitor
.venv/bin/python monitor.py
```
`--pool` registers the pool with Cursor, so `superserve` appears in the pool picker as soon as the controller connects. Any name works except `default`, which does not allow `--clone-git-repos`.
### The spawn hook
The controller runs the hook once per claim with `CURSOR_AGENT_WORKER_ID`, `CURSOR_POOL`, `CURSOR_REQUEST_ID`, and the request's repo fields in its environment. The hook creates a sandbox tagged with the worker id and launches the worker:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const sandbox = await Sandbox.create({
name: `cursor-${workerId.slice(0, 12)}`,
fromTemplate: "cursor-worker",
metadata: {
"cursor.managed": "true",
"cursor.worker_id": workerId,
"cursor.pool": pool,
"cursor.request_id": requestId,
},
autoDeleteSeconds: 86_400, // reap if left paused for a day
})
// launch.sh detaches and runs:
// agent worker --pool --clone-git-repos start
await sandbox.commands.run("bash /var/lib/cursor-worker/launch.sh", {
env: {
CURSOR_API_KEY: process.env.CURSOR_API_KEY,
CURSOR_AGENT_WORKER_ID: workerId,
CURSOR_WORKER_IDLE_RELEASE_TIMEOUT: "600",
},
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox = Sandbox.create(
name=f"cursor-{worker_id[:12]}",
from_template="cursor-worker",
metadata={
"cursor.managed": "true",
"cursor.worker_id": worker_id,
"cursor.pool": pool,
"cursor.request_id": request_id,
},
auto_delete_seconds=86_400, # reap if left paused for a day
)
# launch.sh detaches and runs:
# agent worker --pool --clone-git-repos start
sandbox.commands.run(
"bash /var/lib/cursor-worker/launch.sh",
env={
"CURSOR_API_KEY": os.environ["CURSOR_API_KEY"],
"CURSOR_AGENT_WORKER_ID": worker_id,
"CURSOR_WORKER_IDLE_RELEASE_TIMEOUT": "600",
},
)
```
What the hook does on each claim:
1. **Finds or creates** a sandbox for the worker id. A paused sandbox with the same tag is resumed rather than recreated (see [Hibernate with pause and resume](#hibernate-with-pause-and-resume)).
2. **Launches the worker** detached from the exec call, with `CURSOR_AGENT_WORKER_ID` in its environment. The Cursor CLI reads the id from there and connects as the worker the controller already claimed for.
3. **Verifies it stayed up.** If the worker is not running a few seconds later, the hook prints the worker log, releases the claim so another worker can take the request, deletes the sandbox, and exits non-zero.
Keep `SUPERSERVE_API_KEY` on the controller host only - it never enters a
sandbox. `CURSOR_API_KEY` does: the worker authenticates to Cursor with it,
and the agent's commands run as the same user as the worker, so anything the
worker can read, a task's commands can read too. That is Cursor's worker
model on every platform. Scope the service account to one pool, and treat
the pool as one trust boundary.
Delivering this key as a bound Superserve [secret](/secrets/overview) is not offered here yet: the worker's authentication format has not been verified against the secrets proxy, so the guide ships the key to the worker process as an environment variable.
## Send a task
In Cursor, create a Cloud Agent for a repo the GitHub App can access and select the `superserve` pool. Slack, GitHub, Linear, and the Cloud Agents API can target the pool the same way.
A `cursor-` sandbox appears in the [console](https://console.superserve.ai?utm_source=docs\&utm_medium=link), tagged with the worker, pool, and request ids. Open its terminal to watch the worker:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
tail -f /var/lib/cursor-worker/worker.log
```
The worker clones the repo into `/workspace`, serves the session, and stays connected for follow-ups. Once the idle window closes it exits, and the monitor deletes the sandbox on its next pass.
## Idle release and cleanup
A worker serves one request and stays attached to it. After a session ends it keeps its connection open for `CURSOR_WORKER_IDLE_RELEASE_TIMEOUT` seconds - 600 in this guide, an hour by Cursor's default - so a follow-up reuses the workspace instead of starting cold. When the timer fires the worker exits with code 0. The monitor sweeps every 15 seconds, finds sandboxes whose worker has exited, and deletes them.
### Hibernate with pause and resume
Deleting on idle discards the checkout, the dependency install, and any build cache. A follow-up that arrives an hour later starts from an empty `/workspace`.
Cursor supports hibernation: a pool can declare a reconnect window, and Cursor holds a follow-up for an offline worker instead of reassigning it. Superserve's `pause()` fits this directly - it checkpoints the full sandbox - memory, processes, and filesystem - at zero compute cost, and `resume()` brings it back in under 50ms.
Register the pool with `workerReadyTimeoutSeconds`. Cursor holds a follow-up for up to that long while the worker's machine comes back:
```bash theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
curl --request POST \
--url "https://api.cursor.com/v0/private-workers/pools" \
-u "$CURSOR_API_KEY:" \
--header "Content-Type: application/json" \
--data '{ "scope": "team", "poolName": "superserve", "workerReadyTimeoutSeconds": 900 }'
```
Set `CURSOR_WORKER_HIBERNATE=true` in `.env`. The monitor now pauses a sandbox when its worker exits. `SANDBOX_AUTO_DELETE_SECONDS` still applies, so sandboxes nobody returns to are deleted after a day by default.
In hibernate mode the monitor also polls the pool's pending requests. When Cursor lists a follow-up as claimed by an offline worker and a paused sandbox carries that worker id, the monitor resumes the sandbox and starts a worker under the same id. The follow-up continues on the original workspace.
The spawn hook and the monitor coordinate through sandbox metadata tags
(`cursor.launching`, `cursor.recycling`), and metadata updates have no
compare-and-swap. Each side writes its tag and re-reads before acting, the
launch script holds a lock so two launchers can never start two workers, and
claims are released only after a worker is confirmed gone. What remains is a
window of one API round-trip in which a recycle and a relaunch can overlap;
the outcome in that case is a follow-up that Cursor re-queues, never two
workers on one request.
### Warm workers
For pools where startup latency matters, run the controller with `--warm-idle `. It keeps that many idle workers connected ahead of demand and Cursor assigns requests to them directly. The same spawn hook serves this mode; it runs without a request id.
## Lock down egress
Sandboxes can reach any public IP by default. A worker needs outbound HTTPS to:
* `api2.cursor.sh` and `api2direct.cursor.sh` - the agent session
* `downloads.cursor.com` - CLI self-updates
* `cloud-agent-artifacts.s3.us-east-1.amazonaws.com` - screenshot and recording uploads
* The git host, package registries, and any internal services the agent's work touches
To restrict egress, set `CURSOR_WORKER_ALLOW_OUT` to a comma-separated list of hosts and CIDRs. The spawn hook creates sandboxes that allow those, plus the sandbox's DNS resolvers (`1.1.1.1/32` and `8.8.8.8/32`, without which nothing resolves), and deny everything else. No Superserve host needs to be on the list: the SDK reaches the sandbox through the platform, not through its network. Write single IPs as `/32`; the hook does this for you if you leave the suffix off. See [Network rules](/sandbox/networking) for the format and the [network log](/sandbox/network-log) for every HTTP and HTTPS connection a worker attempted, allowed or blocked.
No inbound ports are required. The worker connects out to Cursor, and Superserve reaches the sandbox through its own control plane.
Git access comes from tokens Cursor mints per run. There is no personal
access token in `.env`, in the template, or in the sandbox. Use a dedicated
service account per pool so a compromised worker can only claim that pool's
work.
## Configuration
The scripts read these from `.env`:
| Setting | Default | What it controls |
| - | - | - |
| `SUPERSERVE_API_KEY` | required | Sandbox creation, resume, and deletion |
| `CURSOR_API_KEY` | required | Service-account key for the controller, handed to each worker |
| `CURSOR_POOL` | required | Pool name. The controller sets it for the spawn hook; the monitor reads it from `.env` and only touches that pool's sandboxes |
| `CURSOR_WORKER_TEMPLATE` | `cursor-worker` | Template each sandbox boots from |
| `CURSOR_WORKER_IDLE_RELEASE_TIMEOUT` | `600` | Seconds a worker waits for follow-ups before exiting |
| `CURSOR_WORKER_CLONE_GIT_REPOS` | `true` | Start the worker with `--clone-git-repos`. Turn off for any-repo workers that handle their own checkout |
| `CURSOR_WORKER_HIBERNATE` | `false` | Pause sandboxes on worker exit instead of deleting them |
| `SANDBOX_AUTO_DELETE_SECONDS` | `86400` | How long a sandbox may stay paused before it is deleted |
| `CURSOR_WORKER_ALLOW_OUT` | unset | Comma-separated egress allowlist. Unset keeps the open default |
| `MONITOR_POLL_SECONDS` | `15` | Monitor sweep interval |
| `MONITOR_GRACE_SECONDS` | `120` | Minimum sandbox age before the monitor may recycle it, so fresh spawns are left alone |
| `MONITOR_WAKE_CONCURRENCY` | `4` | How many hibernated sandboxes the monitor wakes at once when follow-ups arrive in a burst |
The controller sets `CURSOR_AGENT_WORKER_ID`, `CURSOR_REQUEST_ID`, and `CURSOR_WORKER_NAME` on each spawn. Leave them out of `.env`.
## Troubleshooting
Check that the controller is running, that its `--pool` matches the pool users select, and that `CURSOR_API_KEY` is a service-account key. The controller logs every claim and spawn; a silent log means it is not seeing requests.
Read the log tail it printed. Common causes: a key of the wrong type, `--clone-git-repos` without GitHub token minting enabled, or the `default` pool. To run workers that do not clone, set `CURSOR_WORKER_CLONE_GIT_REPOS=false`.
The build VM could not fetch the CLI from `downloads.cursor.com`, or the installed binary did not run on the base image. Stream the build logs to see which.
The monitor is not running. Every sandbox the hook creates carries `cursor.managed=true` in its metadata, so `Sandbox.list({ metadata: { "cursor.managed": "true" } })` finds them all.
All three pieces are required: the pool registered with `workerReadyTimeoutSeconds`, `CURSOR_WORKER_HIBERNATE=true`, and the monitor running with `CURSOR_API_KEY` and `CURSOR_POOL` in its `.env`.
## Resources
Template builder, spawn hook, and monitor in TypeScript and Python.
Pools, the worker controller, hibernation, and the pending-request API.
Build reusable images with your toolchains baked in.
Checkpoint a sandbox between turns and restore it on demand.
# MCP Server
Source: https://docs.superserve.ai/integrations/mcp
Create, run, and manage Superserve sandboxes from any MCP client.
Looking to let an agent create sandboxes itself? This MCP server does that.
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`. Accepts `secrets`, egress rules, and `preview_access`. |
| `sandbox_update` | Change metadata, egress rules, lifecycle windows, or `preview_access`. |
| `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` | Publish a port and return a clean public URL or an expiring private signed URL. |
| `sandbox_network_log` | Audit a sandbox's outbound connections (host, verdict, bytes) without resuming it. |
| `sandbox_computer` | Drive a GUI desktop sandbox: screenshot, mouse, keyboard, scroll, resize, viewer URL. One action per call. |
| `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_network_log`, `sandbox_template_list`, `secret_list`) are annotated so clients can skip confirmation prompts; `sandbox_preview_url` is an idempotent write because it publishes the requested port, and `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, IPs, or CIDRs) adds allowed destinations; `deny_out` (IPs or CIDRs, no domains) 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). If the allowlist has domain rules or the workload resolves hostnames, also allow the sandbox's DNS resolvers, `1.1.1.1/32` and `8.8.8.8/32`, since deny-all blocks DNS too; leave them out of an IP-only allowlist, where an allowed resolver is only an extra outbound channel. 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.** New MCP sandboxes use `public` as the default access for newly
published ports; only explicitly published ports are reachable. Pass
`preview_access: "private"` to `sandbox_create` (or `sandbox_update`) to change
the default for future ports. Existing ports retain their own mode. Start the
server with `sandbox_exec`, then call `sandbox_preview_url`; the tool
idempotently publishes that one port and uses the returned port mode to return
either a clean public URL or an expiring private signed URL. Private links
default to one hour; set `expires_in_seconds` to a value from 1 through 604800
seconds. See [Preview URLs](/sandbox/preview-urls).
## Computer use
`sandbox_computer` drives a GUI desktop in a sandbox created from a desktop
template such as `superserve/desktop`. It takes one `action` per call in the
vocabulary computer-use models emit: `screenshot`, `left_click`, `right_click`,
`middle_click`, `double_click`, `triple_click`, `left_click_drag`,
`left_mouse_down`, `left_mouse_up`, `mouse_move`, `key`, `type`, `scroll`,
`wait`, `resize`, and `stream_url`. Pointer actions take `coordinate: [x, y]`
in screen pixels (origin top-left); `key` and `type` take `text`; `scroll`
takes `scroll_direction` and an optional `scroll_amount`.
```text theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox_create { name: "browser", from_template: "superserve/desktop" }
sandbox_computer { sandbox_id: "a1b2c3…", action: "left_click", coordinate: [640, 32] }
→ text "left_click done (1280x800)" + image/png
sandbox_computer { sandbox_id: "a1b2c3…", action: "type", text: "https://example.com" }
sandbox_computer { sandbox_id: "a1b2c3…", action: "key", text: "Return" }
```
Input actions return a fresh screenshot of the result, so the model's
act-then-look turn is one tool call; pass `screenshot_after: false` when
sending several actions back to back. `wait` pauses for `duration_ms` (capped
at 10 seconds) without a request to the sandbox. `stream_url` returns a live
browser viewer for the desktop under the sandbox's preview policy.
If an input action fails, the input may already have been delivered. The tool
says so and asks the agent to take a screenshot before repeating it. See the
[Desktop guide](/sandbox/desktop) for the underlying SDK surface.
## 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 HTTP and HTTPS connection a sandbox made to a public address on ports 80 and 443
## 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`, or still pausing, it's 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 narrow the result with filters. `metadata`
returns only sandboxes matching every key-value pair you pass; `status`
selects one lifecycle state; `limit` and `offset` page through large lists.
```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" },
})
const running = await Sandbox.list({ status: "active" })
const page = await Sandbox.list({ limit: 100, offset: 100 })
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"})
running = Sandbox.list(status="active")
page = Sandbox.list(limit=100, offset=100)
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.
## Automatic routing hints
Current TypeScript and Python SDKs carry the optional `routing_hint` returned by
create, connect/activate, and resume on command, file, and session requests.
The proxy uses this signed hint to select the sandbox's logical host without
looking up ownership on every new sandbox request. Authorization is unchanged.
Raw HTTP callers can opt in by passing the response's `routing_hint` as
`X-Superserve-Routing-Hint` alongside `X-Access-Token`. Replace both values after
resume or activate. For a WebSocket session, add `route.` to the
subprotocol list alongside `superserve.exec.v1` and `token.`.
Callers that omit the hint keep working through ownership lookup, including its
existing latency and capacity limits.
Hints expire after one hour. SDKs refresh expired hints before their next
operation and refresh a rejected stale route once through activate. A routing
hint grants no access by itself. Never replay a command after an ambiguous
connection loss or timeout: it may already have executed.
# 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: {
// the two IPs are the sandbox's DNS resolvers; deny-all blocks them too
allowOut: ["api.openai.com", "*.github.com", "1.1.1.1/32", "8.8.8.8/32"],
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(
# the two IPs are the sandbox's DNS resolvers; deny-all blocks them too
allow_out=["api.openai.com", "*.github.com", "1.1.1.1/32", "8.8.8.8/32"],
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.
# Computer use
Source: https://docs.superserve.ai/sandbox/desktop
Give an agent a GUI desktop inside a sandbox, with screenshots, mouse, keyboard, and a live browser viewer.
A computer-use agent works a desktop the way a person does: it looks at the
screen, decides, then clicks or types. `sandbox.desktop` gives an agent that
desktop inside a sandbox: screenshots, mouse, keyboard, scroll, a resizable
display, and a live viewer you can open in a browser.
Every call is one request to the sandbox. A click is one round trip, a drag
is one request, a whole model turn can go in one request, and typing has no
per-character delay. A paused sandbox resumes on first use, as
with `commands` and `files`.
## Create a desktop sandbox
The sandbox must come from a desktop-enabled template. `superserve/desktop`
ships the xfce desktop, Chrome, and a browser-based viewer, and runs the
session as the non-root `desktop` user (also the default user for
`commands.run`). The display starts at 1280x800.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({
name: "desktop",
fromTemplate: "superserve/desktop",
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(name="desktop", from_template="superserve/desktop")
```
## The agent loop
Your code owns the model call and the conversation. The sandbox runs the
actions. Each turn is a screenshot in, a list of actions out:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.desktop.resize(1024, 768) // match the model's screen size
let shot = await sandbox.desktop.screenshot()
while (true) {
const actions = await nextActions(shot) // your model call
if (actions.length === 0) break
const step = await sandbox.desktop.step(actions, { waitForChange: true })
if (step.actionError) throw new Error(step.actionError) // stopped at step.executed
if (!step.screenshot) throw new Error(step.screenshotError)
shot = step.screenshot
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.desktop.resize(1024, 768) # match the model's screen size
shot = sandbox.desktop.screenshot()
while True:
actions = next_actions(shot) # your model call
if not actions:
break
step = sandbox.desktop.step(actions, wait_for_change=True)
if step.action_error:
raise RuntimeError(step.action_error) # stopped at step.executed
if step.screenshot is None:
raise RuntimeError(step.screenshot_error)
shot = step.screenshot
```
`step` runs the model's whole turn and captures the frame after it in one
request, so a turn is a single round trip. Input is delivered before the
application has repainted, so with `waitForChange` / `wait_for_change` the
sandbox captures the first frame whose pixels differ from the frame before
the batch, waiting at most `settleMs` / `settle_ms` (default 1000, up to
2000\); the result's `changed` says whether a repaint arrived. A changed frame
proves the display repainted, not that the application finished, so check
the screenshot for what you expect. Without `waitForChange` the sandbox
sleeps `settleMs` and then captures.
`step` does not throw when the batch stops at a failing action or when the
capture fails, since the actions that ran have already landed: check
`actionError` / `action_error` (with `executed`, the index it stopped at) and
`screenshotError` / `screenshot_error`. Resizing to the model's native
resolution first keeps frames small and maps its coordinates 1:1 onto the
screen.
Everything the agent reads from the screen is untrusted input to the model:
page content, file contents, text in dialogs. Keep credentials out of the
desktop, restrict where the sandbox can connect with [network
rules](/sandbox/networking), and have the model ask for approval before an
action with an outside effect.
## Watch it live
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const url = await sandbox.desktop.getStreamUrl()
const readOnly = await sandbox.desktop.getStreamUrl({ viewOnly: true })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
url = sandbox.desktop.get_stream_url()
read_only = sandbox.desktop.get_stream_url(view_only=True)
```
Returns a browser URL for the viewer on port `6080`, publishing that port
under the sandbox's [preview policy](/sandbox/preview-urls). Under `public` the
URL is clean. Under `private` it carries a 60-second signed credential, as
`getSignedPreviewUrl` / `get_signed_preview_url` does; mint a fresh URL for
each viewer. `viewOnly` / `view_only` is a flag on that viewer only and does
not stop someone else from opening the same URL with control, so use a
`private` policy when the viewer must not be shared.
## Screenshots
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import fs from "node:fs/promises"
const shot = await sandbox.desktop.screenshot()
await fs.writeFile("screen.png", shot.data) // PNG bytes; shot.width, shot.height
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from pathlib import Path
shot = sandbox.desktop.screenshot()
Path("screen.png").write_bytes(shot.data) # PNG bytes; shot.width, shot.height
```
The capture includes the cursor. Coordinates in every other call are pixels in
this image, with the origin at the top-left.
## Mouse
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.desktop.click(640, 400)
await sandbox.desktop.click(640, 400, { button: "right" }) // or rightClick / middleClick
await sandbox.desktop.doubleClick(640, 400)
await sandbox.desktop.moveMouse(100, 100)
await sandbox.desktop.drag([10, 10], [200, 200])
await sandbox.desktop.scroll({ dy: 3 }) // positive scrolls down; dx for horizontal
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.desktop.click(640, 400)
sandbox.desktop.click(640, 400, button="right") # or right_click / middle_click
sandbox.desktop.double_click(640, 400)
sandbox.desktop.move_mouse(100, 100)
sandbox.desktop.drag((10, 10), (200, 200))
sandbox.desktop.scroll(dy=3) # positive scrolls down; dx for horizontal
```
A click moves the pointer and clicks in one request. A drag runs as one atomic
batch (press, move, release), so no other input can land in the middle of it. Scroll moves the wheel by whole clicks and targets
whatever is under the pointer, so move first to scroll a specific element.
## Keyboard
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.desktop.write("hello world")
await sandbox.desktop.press("enter")
await sandbox.desktop.press("ctrl+shift+p") // or press(["ctrl", "shift", "p"])
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.desktop.write("hello world")
sandbox.desktop.press("enter")
sandbox.desktop.press("ctrl+shift+p") # or press(["ctrl", "shift", "p"])
```
`write` types literal text with no per-character pacing, so long strings land
in well under a second. `press` sends a key or chord. Friendly names (`enter`,
`esc`, `tab`, `backspace`, `delete`, `up`, `down`, `pageup`, `ctrl`, `alt`,
`shift`, `cmd`) are accepted as written; any other key uses its X keysym name,
so `F5`, `Return`, and `KP_Enter` work as written.
## Batch actions
Models that emit several actions per turn can send them in one request:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.desktop.actions([
{ type: "click", x: 640, y: 32 },
{ type: "write", text: "https://example.com" },
{ type: "press", key: "enter" },
])
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.desktop.actions([
{"type": "click", "x": 640, "y": 32},
{"type": "write", "text": "https://example.com"},
{"type": "press", "key": "enter"},
])
```
The whole batch is validated before anything runs, then runs in order and
stops at the first failing action. `step` is the same batch followed by a
capture in the same request, for the agent loop above; `actions` is for input
you do not need to look at afterwards.
| TypeScript `type` | Python `type` | Fields |
| - | - | - |
| `click` | `click` | `x`, `y`, `button?` |
| `doubleClick` | `double_click` | `x`, `y` |
| `move` | `move` | `x`, `y` |
| `mouseDown` / `mouseUp` | `mouse_down` / `mouse_up` | `x`, `y`, `button?` |
| `press` | `press` | `key` (string or list) |
| `write` | `write` | `text` |
| `scroll` | `scroll` | `dx?`, `dy?` |
Input is not idempotent. If a pointer, key, or batch call fails with a
transport error, the input may already have been delivered. Take a screenshot
and check the screen before repeating it.
## Resize the display
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.desktop.resize(1920, 1080)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.desktop.resize(1920, 1080)
```
Takes effect live, with no restart: open windows re-lay out. Width must be a
multiple of 8 from 320 to 8192; height from 200 to 8192.
## Pause and resume
The desktop survives a pause. Open windows, running applications, and the
screen come back exactly as they were, and the next `desktop` call on a paused
sandbox resumes it first.
## MCP
The MCP server exposes the same surface as the `sandbox_computer` tool, one
action per call (`screenshot`, `left_click`, `type`, `key`, `scroll`,
`left_click_drag`, …) in the vocabulary computer-use models are trained on.
Input actions return a fresh screenshot of the result by default. See
[MCP Sandboxes](/integrations/mcp#computer-use).
## Related
* [SDK reference: Sandbox](/sdk-reference/sandbox#desktop-methods)
* [Preview URLs](/sandbox/preview-urls) — access policy for the viewer port
* [Templates](/templates/overview) — the `superserve/desktop` template
# 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 **`pausing`** while a sandbox is being checkpointed and **`resuming`** while a paused sandbox is being restored to `active`. `connect()` and `resume()` wait a pause out for you; a pause still finishing after that wait answers `409` with code `sandbox_pausing` and a `Retry-After` header, so 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. It returns once the pause is accepted and the sandbox finishes pausing on its own; pass `wait: true` / `wait=True` to return once the sandbox is `paused`. A waited pause is a single request the server holds until the pause lands, so it costs no polling.
```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()
// or, to return only once the sandbox is paused:
// await sandbox.pause({ wait: true })
```
```python Python {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(name="long-job")
sandbox.pause()
# or, to return only once the sandbox is paused:
# sandbox.pause(wait=True)
```
## 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 that returns only sandboxes matching every key-value pair you pass.
```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 HTTP and HTTPS connection a sandbox made to a public address on ports 80 and 443 — 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 HTTP and HTTPS connection to a public address on ports 80 and 443 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 HTTP or HTTPS connection the sandbox opened to a public address on ports 80 and 443, whether or not a secret was involved. Attempts at private, link-local, or loopback addresses are dropped before they reach the log.
* **`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). `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 on ports 80 and 443. DNS resolution, raw non-HTTP sockets, other ports, and the platform's own drops of private, link-local, and loopback addresses happen below this log and aren't recorded in it.
The log keeps the last **7 days**. Older rows are dropped on a rolling basis; page the API if you want to keep a longer history elsewhere.
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 over HTTP and HTTPS to public addresses on ports 80 and 443 — every such 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: [
"1.1.1.1/32", "8.8.8.8/32", // the sandbox's DNS resolvers
"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=[
"1.1.1.1/32", "8.8.8.8/32", # the sandbox's DNS resolvers
"api.openai.com", "*.github.com", "140.82.112.0/20",
],
deny_out=["0.0.0.0/0"],
),
)
```
A deny-all rule also blocks DNS. Sandboxes resolve names through `1.1.1.1` and
`8.8.8.8`, so an allowlist that contains domain rules, or whose workload looks
up hostnames, must let the sandbox reach at least one of them or nothing
resolves - not even the domains you allowed. The examples allow both so a
resolver outage doesn't take DNS down with it. An allowlist made only of IPs
and CIDRs has no such need and is tighter without them: DNS is an outbound
channel like any other, and an allowed resolver can carry data in queries for
a domain no rule permits. Domain rules are matched on the connection, not on
the lookup, so allowing `api.openai.com` does not by itself let the sandbox
look it up.
## Rule format
| Field | Accepts | Notes |
| - | - | - |
| `allowOut` / `allow_out` | IPs, 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. A bare IP is stored and applied as a single-host prefix: `1.1.1.1` reads back as `1.1.1.1/32`. |
| `denyOut` / `deny_out` | IPs and CIDRs only | Use `0.0.0.0/0` to deny the entire internet and rely on `allowOut` for exceptions. Domains are not accepted here. |
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. The sandbox must be `active`; updating `network` on a paused sandbox returns a `ConflictError`. Resume it first, or set the rules at creation so they follow the sandbox through every pause and resume.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.update({
network: {
allowOut: ["1.1.1.1/32", "8.8.8.8/32", "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=["1.1.1.1/32", "8.8.8.8/32", "api.anthropic.com", "api.openai.com"],
deny_out=["0.0.0.0/0"],
),
)
```
Rules only filter traffic the sandbox itself initiates. The SDK reaches the
sandbox through the platform rather than through its network, so commands
and file operations keep working under a deny-all policy.
# Preview URLs
Source: https://docs.superserve.ai/sandbox/preview-urls
Publish sandbox ports with public access or per-port authentication.
Preview URLs route a hostname such as
`https://3000-{sandbox-id}.sandbox.superserve.ai` to a process listening inside
your sandbox. Use an explicit preview policy and publish only the ports you
intend to expose.
## Access policies
The sandbox's `previewAccess` / `preview_access` selects strict publication and
sets the default access for each newly published port:
| Sandbox policy | Which ports are reachable? | Default for a new port |
| - | - | - |
| `public` | Explicitly published ports only | Public, no credential |
| `private` | Explicitly published ports only | Private, credential required |
| `legacy_public` | Every listening port | Not applicable |
Every published port stores its own `public` or `private` access mode. Changing
the sandbox default does not rewrite existing ports, so public and private ports
can coexist in one sandbox.
`legacy_public` exists only for sandboxes created before the publication
migration. New sandboxes default to strict `public` when the field is omitted.
The compatibility mode cannot be selected through create or update APIs.
Moving a legacy sandbox to `public` or `private` immediately closes every port
that has not been published. Publish the intended ports before changing the
policy if you need a seamless transition.
## Choose the sandbox default
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const sandbox = await Sandbox.create({
name: "web-app",
previewAccess: "private",
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox = Sandbox.create(
name="web-app",
preview_access="private",
)
```
Use `public` when new ports should be internet-accessible by default. Use
`private` when new ports should require a credential by default. You can change
the default later with `update`; already-published ports keep their own mode:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.update({ previewAccess: "private" })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.update(preview_access="private")
```
## Publish a port
Start the service, then publish its port. Omit the access option to use the
sandbox default for a new port. An omitted option preserves the mode of an
already-published port; pass an explicit mode to override just that port.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.commands.spawn("npm run dev -- --host 0.0.0.0")
await sandbox.publishPreviewPort(3000, { access: "private" })
const url = sandbox.getPreviewUrl(3000)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
# AsyncSandbox shown because spawn is an async session API.
await sandbox.commands.spawn("npm run dev -- --host 0.0.0.0")
await sandbox.publish_preview_port(3000, access="private")
url = sandbox.get_preview_url(3000)
```
`getPreviewUrl` / `get_preview_url` only constructs the hostname. It does not
publish the port or add credentials. Ports must be integers from `1024` through
`65535`; port `49983` is reserved for sandbox control traffic and cannot be
published.
List the current server-side publication state with `listPreviewPorts()` /
`list_preview_ports()`. The result includes the sandbox default and every
published port's `access`, but never includes tokens.
## Authenticate a private port
Private preview credentials are scoped to one published port. A token for port
`3000` cannot open port `8080` on the same sandbox.
### Machine clients and reverse proxies
Mint a token and send it using the header name returned by the API:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const credential = await sandbox.getPreviewToken(3000)
await fetch(sandbox.getPreviewUrl(3000), {
headers: { [credential.header]: credential.token },
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
credential = sandbox.get_preview_token(3000)
response = httpx.get(
sandbox.get_preview_url(3000),
headers={credential.header: credential.token},
)
```
Omit the expiry to keep a token valid until that port is rotated or
unpublished. For limited-lived machine credentials, pass `expiresInSeconds` /
`expires_in_seconds` from `1` to `604800` seconds.
### Browsers, iframes, and WebSockets
Browsers cannot attach a custom header to a navigation. Request a signed URL
instead:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const url = await sandbox.getSignedPreviewUrl(3000, {
expiresInSeconds: 300,
})
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
url = sandbox.get_signed_preview_url(3000, expires_in_seconds=300)
```
The SDK defaults signed links to 60 seconds. On first navigation, the edge
proxy verifies the query token, stores it in a secure host-scoped cookie, and
redirects to the same URL without the token. Asset requests and WebSocket
upgrades then use the cookie automatically.
Treat signed URLs as secrets until the initial redirect removes their query
token. Avoid logging them or placing them in analytics and referrer data.
## Revoke access
Rotate a port to invalidate all of its existing header tokens, signed links,
and browser cookies without affecting other ports:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const fresh = await sandbox.rotatePreviewToken(3000)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
fresh = sandbox.rotate_preview_token(3000)
```
Unpublishing closes the port at the edge and revokes its credentials:
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.unpublishPreviewPort(3000)
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.unpublish_preview_port(3000)
```
Unpublish is idempotent and retry-safe. Under a strict policy the edge returns
`404` for an unpublished port before revealing sandbox status or checking a
credential.
## MCP and console
The console's Preview panel uses the same server-backed published-port list. It
mints a signed browser URL for each private port without displaying the
credential; public sibling ports keep clean URLs.
The MCP `sandbox_preview_url` tool publishes the requested port. It returns a
clean URL when that port is public and a one-hour signed URL by default when it
is private. Re-publishing without an access override preserves an existing
port's mode. Set `expires_in_seconds` to choose another private-link lifetime.
# Snapshots and forks
Source: https://docs.superserve.ai/sandbox/snapshots
Save a sandbox's memory and disk, then start new sandboxes from that exact point.
A snapshot saves a sandbox's memory and disk at a point in time. Sandboxes you create from a snapshot are called forks. A fork starts exactly where the snapshot left off, with its files, installed packages and running processes in place.
Pausing saves one sandbox so it can continue later. A snapshot is a copy you can start any number of new sandboxes from, and it stays until you delete it, even after the original sandbox is gone.
Use snapshots to:
* **Skip setup:** install dependencies once, snapshot, and start every new sandbox ready to go.
* **Branch from one state:** run several experiments in parallel from the same starting point.
* **Keep a known-good point:** snapshot before a risky change and start fresh from it if something breaks.
## Take a snapshot
You can snapshot an `active` or `paused` sandbox. A running sandbox is paused briefly while the snapshot is taken, usually for under a second, and then continues. The first snapshot of a large sandbox can take longer.
```typescript TypeScript {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Sandbox } from "@superserve/sdk"
const sandbox = await Sandbox.create({ name: "dev-env" })
await sandbox.commands.run("pip install pandas")
const snapshot = await sandbox.snapshot({ name: "with-pandas" })
```
```python Python {5} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Sandbox
sandbox = Sandbox.create(name="dev-env")
sandbox.commands.run("pip install pandas")
snapshot = sandbox.snapshot(name="with-pandas")
```
`snapshot()` returns once the snapshot is `ready`.
## Create a sandbox from a snapshot
Pass the snapshot, or its ID, as `fromSnapshot` / `from_snapshot`. You can't combine it with `fromTemplate`.
```typescript TypeScript {3} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const fork = await Sandbox.create({
name: "experiment-1",
fromSnapshot: snapshot,
})
await fork.commands.run("python -c 'import pandas'")
```
```python Python {3} theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
fork = Sandbox.create(
name="experiment-1",
from_snapshot=snapshot,
)
fork.commands.run("python -c 'import pandas'")
```
Each fork is independent. Changes in one fork don't affect the snapshot, the original sandbox or other forks.
### What a fork starts with
* **Machine:** the snapshot's vCPU, memory and disk size, and the template it was built on.
* **Network rules:** the original sandbox's rules, unless you pass `network`. They apply before anything in the fork runs.
* **Timeout:** the original sandbox's auto-pause timeout, unless you pass `timeoutSeconds` / `timeout_seconds`.
* **Secrets:** the original sandbox's secrets are attached again. Secrets deleted since the snapshot are left out, and any secrets you pass are attached as usual.
* **Environment:** commands you run in the fork see the original sandbox's environment, with your `envVars` / `env_vars` and secrets added on top.
Processes that were already running when the snapshot was taken keep the environment they started with. Their secret values only work in the original sandbox, so restart any process that uses secrets.
## List, rename and delete
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Snapshot } from "@superserve/sdk"
const snapshots = await sandbox.snapshots() // newest first
const same = await Snapshot.get(snapshots[0].id)
const renamed = await same.rename("golden")
await renamed.delete()
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Snapshot
snapshots = sandbox.snapshots() # newest first
same = Snapshot.get(snapshots[0].id)
renamed = same.rename("golden")
renamed.delete()
```
Snapshots stay after their sandbox is deleted. To list them then, use `Snapshot.list(sandboxId)` / `Snapshot.list(sandbox_id)`.
Deleting a snapshot takes effect immediately and doesn't affect sandboxes already created from it.
## Limits
* **100 snapshots** per team.
* **20 snapshots** per sandbox.
* **4 snapshots** being taken at once per team.
Past a limit, `snapshot()` fails with `too_many_snapshots` or `too_many_snapshots_in_flight`. Delete snapshots you no longer need to free up room, or contact [support@superserve.ai](mailto:support@superserve.ai) to raise a limit.
# 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. Cancels the start, or kills the process once it's running. |
| `startupSignal` | `AbortSignal` | TypeScript only. Cancels only the connect and start; a command already running is left alone. Defaults to `signal`. |
## `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
```
## Connection reuse (Python async)
The sync SDK reuses connections across calls. In async Python, calls on an `AsyncSandbox` reuse its connection. Wrap other calls in `async_connection_pool()` to reuse connections across them. The connections close when the block ends.
```python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import AsyncSandbox, AsyncSnapshot, async_connection_pool
async with async_connection_pool():
sandbox = await AsyncSandbox.create(name="example")
snapshots = await AsyncSnapshot.list(sandbox.id)
```
## 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: {
// deny everything except OpenAI and the sandbox's DNS resolvers
allowOut: ["api.openai.com", "1.1.1.1/32", "8.8.8.8/32"],
denyOut: ["0.0.0.0/0"],
},
previewAccess: "private",
})
```
```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(
# deny everything except OpenAI and the sandbox's DNS resolvers
allow_out=["api.openai.com", "1.1.1.1/32", "8.8.8.8/32"],
deny_out=["0.0.0.0/0"],
),
preview_access="private",
)
```
**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` | Snapshot ID or `Snapshot` to create the sandbox from. It starts with the snapshot's files and running processes. See [Snapshots and forks](/sandbox/snapshots). |
| `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. |
| `previewAccess` / `preview_access` | `"public" \| "private"` | Enable explicit publication and set the default access for newly published ports. Each port can override it. Omit to use strict `public`; `legacy_public` is only returned for pre-migration sandboxes. See [Preview URLs](/sandbox/preview-urls). |
| `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` returns only sandboxes
matching every key-value pair you pass; `status` filters to one lifecycle
state; `limit`/`offset` page through the list.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const all = await Sandbox.list()
const prod = await Sandbox.list({ metadata: { env: "prod" } })
const running = await Sandbox.list({ status: "active" })
const page = await Sandbox.list({ limit: 100, offset: 100 })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
all_boxes = Sandbox.list()
prod = Sandbox.list(metadata={"env": "prod"})
running = Sandbox.list(status="active")
page = Sandbox.list(limit=100, offset=100)
```
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) // e.g. "active" — see SandboxStatus
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
info = sandbox.get_info()
print(info.status.value) # e.g. "active" — see SandboxStatus
```
### `pause`
Checkpoint the VM state to disk. Returns once the pause is accepted; the sandbox transitions to `pausing` and then `paused` on its own. Pass `wait` to return once it is `paused`.
| Option | Type | Description |
| - | - | - |
| `wait` | `boolean` | Return once the sandbox is `paused` rather than once the pause is accepted. A sandbox configured to delete on pause counts as done once it is deleted. Default `false`. |
| `timeoutMs` / `timeout` | `number` | With `wait`, how long to wait for `paused` before raising a timeout. TypeScript in milliseconds, Python in seconds. Default 5 minutes. |
| `pollIntervalMs` / `poll_interval_s` | `number` | With `wait`, how often to check the status if the server stopped waiting before the pause landed; a waited pause is otherwise one held request with no polling. Default 1 second. TypeScript in milliseconds, Python in seconds. |
| `signal` (TypeScript only) | `AbortSignal` | Cancels the request, and the polling when `wait` is set. |
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
await sandbox.pause()
// or, to return only once the sandbox is paused:
// await sandbox.pause({ wait: true })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
sandbox.pause()
# or, to return only once the sandbox is paused:
# sandbox.pause(wait=True)
```
### `snapshot`
Save the sandbox's memory and disk as a [`Snapshot`](/sdk-reference/snapshot), which is kept until you delete it. A running sandbox pauses briefly while the snapshot is taken. Returns once the snapshot is `ready`.
| Option | Type | Description |
| - | - | - |
| `name` | `string` | Label, 1 to 64 characters. |
| `idempotencyKey` / `idempotency_key` | `string` | Retrying with the same key returns the same snapshot instead of taking a new one. Generated if you don't pass one. |
| `wait` | `boolean` | Wait until the snapshot is `ready`. Default `true`. |
| `timeoutMs` / `timeout` | `number` | Maximum time for taking the snapshot and waiting for it. TypeScript in milliseconds, Python in seconds. Default 15 minutes. |
| `pollIntervalMs` / `poll_interval_s` | `number` | How often to check while waiting. TypeScript in milliseconds, Python in seconds. Default 2 seconds. |
| `signal` (TypeScript only) | `AbortSignal` | Cancels the request and the wait. |
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const snapshot = await sandbox.snapshot({ name: "before-upgrade" })
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
snapshot = sandbox.snapshot(name="before-upgrade")
```
### `snapshots`
List this sandbox's snapshots, newest first. Takes `limit` and `offset`. Returns `SnapshotInfo[]`.
### `resume`
Restore a paused sandbox. A pause still in progress is waited out first, under the same controls as `pause` with `wait`. The backend rotates the per-sandbox access token; the SDK re-injects it into `sandbox.files` transparently.
| Option | Type | Description |
| - | - | - |
| `timeoutMs` / `timeout` | `number` | How long to wait out a pause in progress before raising a timeout. TypeScript in milliseconds, Python in seconds. Default 5 minutes. |
| `pollIntervalMs` / `poll_interval_s` | `number` | How often to check the status while waiting. When unset, the SDK checks every 50 ms for the first two seconds, then every second. TypeScript in milliseconds, Python in seconds. |
| `signal` (TypeScript only) | `AbortSignal` | Cancels the resume request and any waiting. |
```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`, lifecycle windows, and/or the default access for
newly published preview ports 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 })
await sandbox.update({ previewAccess: "private" })
```
```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)
sandbox.update(preview_access="private")
```
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).
Moving a compatibility sandbox onto `public` or `private` closes every
unpublished port. See [Preview URLs](/sandbox/preview-urls) for the safe
publication and migration flow. Changing between `public` and `private` later
does not rewrite the access mode of ports that are already published.
### Preview URL methods
Preview URLs use an explicit published-port allowlist. The URL builder alone
does not publish or authenticate a port.
| TypeScript | Python | Description |
| - | - | - |
| `getPreviewUrl(port)` | `get_preview_url(port)` | Construct `https://{port}-{id}.{host}` without a network call. |
| `listPreviewPorts()` | `list_preview_ports()` | Return the sandbox default plus every port's access; tokens are never listed. |
| `publishPreviewPort(port, { access? })` | `publish_preview_port(port, access=...)` | Publish a port; omit access to preserve an existing mode or use the new-port default. |
| `unpublishPreviewPort(port)` | `unpublish_preview_port(port)` | Close a port and revoke its credentials. |
| `getPreviewToken(port, options?)` | `get_preview_token(port, expires_in_seconds=...)` | Mint a per-port header/query credential. |
| `getSignedPreviewUrl(port, options?)` | `get_signed_preview_url(port, expires_in_seconds=60)` | Mint a browser bootstrap URL; defaults to 60 seconds. |
| `rotatePreviewToken(port)` | `rotate_preview_token(port)` | Revoke one port's old credentials and return a fresh token. |
See the [Preview URLs guide](/sandbox/preview-urls) for policy selection,
machine headers, browser cookies, and revocation examples.
### Desktop methods
`sandbox.desktop` drives a GUI desktop and requires a desktop-enabled template
such as `superserve/desktop`. Every method is one request; a paused sandbox
resumes on first use.
| TypeScript | Python | Description |
| - | - | - |
| `desktop.screenshot()` | `desktop.screenshot()` | Capture the screen; returns PNG `data` plus `width` and `height`. |
| `desktop.click(x, y, { button? })` | `desktop.click(x, y, button=...)` | Move and click in one request. Also `doubleClick`, `rightClick`, `middleClick`. |
| `desktop.moveMouse(x, y)` | `desktop.move_mouse(x, y)` | Move the pointer without clicking. |
| `desktop.drag(from, to, { button? })` | `desktop.drag(start, end, button=...)` | Press, move, release as one atomic batch. |
| `desktop.scroll({ dx?, dy? })` | `desktop.scroll(dx=0, dy=0)` | Scroll by wheel clicks under the pointer; positive `dy` scrolls down. |
| `desktop.write(text)` | `desktop.write(text)` | Type literal text with no per-character pacing. |
| `desktop.press(key)` | `desktop.press(key)` | Press a key or chord: `"enter"`, `"ctrl+c"`, `["ctrl", "shift", "p"]`. |
| `desktop.actions(actions)` | `desktop.actions(actions)` | Run an ordered batch in one request; stops at the first failing action. |
| `desktop.step(actions, { waitForChange?, settleMs? })` | `desktop.step(actions, wait_for_change=False, settle_ms=0)` | Run a batch and capture the frame after it in one request, the first changed frame with `waitForChange`; failures are reported in the result, not thrown. |
| `desktop.resize(width, height)` | `desktop.resize(width, height)` | Resize the display live. Width a multiple of 8, 320–8192; height 200–8192. |
| `desktop.getStreamUrl({ viewOnly? })` | `desktop.get_stream_url(view_only=False)` | Publish the viewer port and return a browser URL; signed for 60 s under `private`. |
See the [Desktop guide](/sandbox/desktop) for the action vocabulary, key
names, and viewer access.
### `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 HTTP and HTTPS connection to a public address on ports 80 and 443, and every secret-bearing request, newest first. Platform-level drops of private, link-local, and loopback addresses are not recorded. 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`, `previewAccess` / `preview_access`, 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. |
| `previewAccess` / `preview_access` | `"legacy_public" \| "public" \| "private"` | Preview compatibility mode or default access for newly published ports 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()`. |
| `desktop` | `Desktop` | See [Desktop methods](#desktop-methods). Requires a desktop-enabled template. |
## Types
### `SandboxStatus`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
type SandboxStatus =
| "starting"
| "active"
| "pausing"
| "paused"
| "resuming"
| "failed"
| "deleted"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from enum import Enum
class SandboxStatus(str, Enum):
STARTING = "starting"
ACTIVE = "active"
PAUSING = "pausing"
PAUSED = "paused"
RESUMING = "resuming"
FAILED = "failed"
DELETED = "deleted"
```
* `starting` - a new sandbox is booting
* `active` - running and ready to accept commands
* `pausing` - a pause is in progress
* `paused` - stopped with state saved to disk
* `resuming` - a paused sandbox is being restored
* `failed` - the sandbox couldn't boot or resume; the entry remains until you delete it
* `deleted` - removed; deleted sandboxes do not appear in list results
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
previewAccess: "legacy_public" | "public" | "private"
sourceSnapshotId?: string // set when the sandbox was created from a snapshot
}
```
```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]
preview_access: PreviewAccess
# Set when the sandbox was created from a snapshot.
source_snapshot_id: str | None = None
```
### `NetworkConfig`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
interface NetworkConfig {
allowOut?: string[] // IPs, CIDRs, or domain patterns (a bare IP reads back as /32)
denyOut?: string[] // IPs and 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 # IPs, CIDRs, or domain patterns (a bare IP reads back as /32)
deny_out: list[str] | None = None # IPs and CIDRs only - use "0.0.0.0/0" to deny all
```
## 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).
# Snapshot
Source: https://docs.superserve.ai/sdk-reference/snapshot
The Snapshot class - taking, listing, renaming and deleting saved snapshots of a sandbox.
A `Snapshot` is a saved copy of a sandbox's memory and disk. Take one with [`sandbox.snapshot()`](/sdk-reference/sandbox#snapshot), and start new sandboxes from it with `Sandbox.create({ fromSnapshot })`. Python has a sync `Snapshot` and an async `AsyncSnapshot` with the same methods. For how snapshots and forks behave, see [Snapshots and forks](/sandbox/snapshots).
## Import
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
import { Snapshot } from "@superserve/sdk"
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
from superserve import Snapshot
# Async variant:
from superserve import AsyncSnapshot
```
## Factory methods
### `Snapshot.get`
Fetch a snapshot by ID.
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
const snapshot = await Snapshot.get("5b9c2e1a-...")
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
snapshot = Snapshot.get("5b9c2e1a-...")
```
### `Snapshot.list`
List a sandbox's snapshots, newest first. This also works after the sandbox is deleted. For a sandbox you already have, `sandbox.snapshots()` does the same.
| Option | Type | Description |
| - | - | - |
| `limit` | `number` | Maximum number of snapshots to return. Omit it to get them all. |
| `offset` | `number` | Number of snapshots to skip. Use with `limit` to page through results. |
Returns `SnapshotInfo[]`.
### `Snapshot.deleteById` / `Snapshot.delete_by_id`
Delete a snapshot by ID. Safe to call more than once.
## Methods on `snapshot`
### `getInfo` / `get_info`
Fetch the snapshot's current state as a [`SnapshotInfo`](#snapshotinfo).
### `rename`
Change the snapshot's name (1 to 64 characters). Returns the renamed `Snapshot`.
### `delete`
Delete the snapshot. Sandboxes already created from it keep running. Safe to call more than once.
### `waitUntilReady` / `wait_until_ready`
Wait until the snapshot is `ready`. You only need this if you called `sandbox.snapshot()` with `wait: false` / `wait=False`.
If the snapshot failed or was deleted, this raises `SandboxError`. If it still isn't ready when the timeout runs out, TypeScript throws `TimeoutError` and Python raises `SandboxTimeoutError`.
| Option | Type | Description |
| - | - | - |
| `timeoutMs` / `timeout` | `number` | How long to wait. TypeScript in milliseconds, Python in seconds. Default 15 minutes. |
| `pollIntervalMs` / `poll_interval_s` | `number` | How often to check. TypeScript in milliseconds, Python in seconds. Default 2 seconds. |
## Types
### `SnapshotInfo`
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
interface SnapshotInfo {
id: string
sandboxId: string // the original sandbox, which may no longer exist
templateId?: string
kind: "mem+fs"
status: "creating" | "ready" | "failed" | "deleting"
name?: string
sizeBytes: number // 0 until ready
resources: { vcpuCount: number; memoryMib: number; diskMib: number }
createdAt: Date
readyAt?: Date
}
```
```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
class SnapshotInfo(BaseModel):
id: str
sandbox_id: str # the original sandbox, which may no longer exist
template_id: str | None = None
kind: str # "mem+fs"
status: SnapshotStatus # creating | ready | failed | deleting
name: str | None = None
size_bytes: int # 0 until ready
resources: SnapshotResources # vcpu_count, memory_mib, disk_mib
created_at: datetime
ready_at: datetime | None = None
```
## Errors
| Error | When |
| - | - |
| `RateLimitError` (`too_many_snapshots`) | The team or sandbox is at its snapshot limit. |
| `RateLimitError` (`too_many_snapshots_in_flight`) | The team is at its limit on snapshots being taken at once. |
| `ConflictError` | The sandbox isn't `active` or `paused`. |
| `NotFoundError` | The snapshot or sandbox doesn't exist. |
# 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).
The audit log keeps the last **7 days**. Older rows are dropped on a rolling basis; page the API if you want to keep a longer trail elsewhere.
## 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 HTTP and HTTPS connection it made to a public address on ports 80 and 443 — 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: {
// the two IPs are the sandbox's DNS resolvers; deny-all blocks them too
allowOut: ["api.anthropic.com", "1.1.1.1/32", "8.8.8.8/32"],
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(
# the two IPs are the sandbox's DNS resolvers; deny-all blocks them too
allow_out=["api.anthropic.com", "1.1.1.1/32", "8.8.8.8/32"],
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. |
| `superserve/desktop` | `ubuntu:24.04` | 2 · 4096 MiB · 8192 MiB | `xfce4`, `xfce4-terminal`, `tigervnc-standalone-server`, `novnc`, `google-chrome-stable`, `xdotool`, `imagemagick` (apt); desktop session runs as the `desktop` user. For [computer use](/sandbox/desktop). |
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)