# 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)