Sandbox API

Long-lived sandboxes

Create one isolated workspace, run many commands, checkpoint it — pod or desktop substrate.

Use a long-lived sandbox when one workflow needs the same filesystem across many commands. Create the box once instead of paying setup on every turn.

NeedUse
Interactive agent/dev loopLong-lived sandbox (sandboxes-default)
Re-run tests in the same workspaceLong-lived sandbox verify
GUI desktop in the browserDesktop sandboxes (desktop-*)
One short snippetCodeEdit
Benchmark + verifierHarbor
Free-form one-shot agentAgent runs

Not for pass@k

Pass@k attempts must be independent. Do not attach multiple attempts to the same sandbox or carry files, conversation state, or answers between them.

Profiles and substrates

The same POST /sandbox/sandboxes/v1 API serves headless workspaces and desktops. The profile picks the substrate:

ProfileSubstrateGUI?
sandboxes-defaultKubernetes pod + PVCNo — exec/files API
desktop-ubuntuKubernetes pod + noVNCYes
desktop-windowsEC2 VM + GuacamoleYes
desktop-macosExternal Mac cloudYes

This page covers the headless path (sandboxes-default). For GUI profiles see Desktop sandboxes.

Lifecycle

For sandboxes-default, the service runs one pod plus a PVC in the trial namespace. Kubernetes objects are runtime truth; object storage holds durable workspace checkpoints. Redis is a lookup and quota store, not the durable workspace.

Create

curl -sS -X POST \
  -H "X-Api-Key: $SANDBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile": "sandboxes-default",
    "autostop_sec": 1800,
    "ttl_sec": 14400
  }' \
  "$SANDBOX_GATEWAY_URL/sandbox/sandboxes/v1"

The response contains sandbox_id. Keep it for every later call. A create or start consumes one concurrency slot for the authenticated tenant:project; when the project is full, the API returns 429 and Retry-After.

You may bootstrap from an HTTPS archive, a permitted GCS prefix, or a prior snapshot:

{
  "profile": "sandboxes-default",
  "source": {
    "archive_url": "https://storage.example/task.zip"
  }
}

The source is fetched once. Later exec calls use the PVC; they do not download the archive again.

Exec in the same workspace

curl -sS -X POST \
  -H "X-Api-Key: $SANDBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":["bash","-lc","python -m pytest -q"],"timeout_sec":300}' \
  "$SANDBOX_GATEWAY_URL/sandbox/sandboxes/v1/$SANDBOX_ID/exec"

Each exec uses the same filesystem until the sandbox is stopped, destroyed, expires, or is reaped for idleness.

Continue with an agent

Attach a catalog agent with integration: sandbox (for example opencode) to the running session:

{
  "agent_id": "opencode",
  "model": "wandb/moonshotai/Kimi-K2.5",
  "instruction": "Continue from the current files and fix the next failure",
  "sandbox_id": "your-sandbox-id"
}

Submit to POST /sandbox/agent/v1/runs with the same tenancy. The session image must already contain that agent CLI. sandbox_id with pass_at_k > 1 is rejected. Agent list: Supported agents.

Stop, start, and destroy

curl -sS -X POST -H "X-Api-Key: $SANDBOX_API_KEY" \
  "$SANDBOX_GATEWAY_URL/sandbox/sandboxes/v1/$SANDBOX_ID/stop"

curl -sS -X POST -H "X-Api-Key: $SANDBOX_API_KEY" \
  "$SANDBOX_GATEWAY_URL/sandbox/sandboxes/v1/$SANDBOX_ID/start"

curl -sS -X DELETE -H "X-Api-Key: $SANDBOX_API_KEY" \
  "$SANDBOX_GATEWAY_URL/sandbox/sandboxes/v1/$SANDBOX_ID"

Default auto-stop is 30 minutes on dev. Destroy sandboxes you no longer need — idle sessions hold storage and, while running, a concurrency slot.

Lifetime: idle stop, TTL, and extending

Two clocks end a session:

ClockFieldWhat happens
Idleautostop_sec (default 1800)Stopped after this long without activity. Workspace is checkpointed; start resumes. 0 = never idle-stop.
Absoluteexpires_at (create: now + ttl_sec, default 86400)Destroyed. A final checkpoint is taken first and snapshots are kept (SANDBOX_SNAPSHOT_RETENTION_SEC, default 30 days). If that checkpoint cannot be taken, the workspace volume is retained (labelled sandbox.turing.com/retained=true) instead of deleted.

Activity is any API call on the sandbox (exec, files, capture, …) and traffic through the viewer / IDE proxy (throttled to one touch per minute per sandbox).

Extend a long-running session instead of recreating it:

# New lifetime counted from now (may shorten or extend), and never idle-stop
curl -sS -X PATCH -H "X-Api-Key: $SANDBOX_API_KEY" -H "Content-Type: application/json" \
  -d '{"ttl_sec": 604800, "autostop_sec": 0}' \
  "$SANDBOX_GATEWAY_URL/sandbox/sandboxes/v1/$SANDBOX_ID"

# Or an absolute expiry (ISO 8601; naive = UTC)
curl -sS -X PATCH -H "X-Api-Key: $SANDBOX_API_KEY" -H "Content-Type: application/json" \
  -d '{"expires_at": "2026-12-01T00:00:00Z"}' \
  "$SANDBOX_GATEWAY_URL/sandbox/sandboxes/v1/$SANDBOX_ID"

# Heartbeat between agent turns (resets the idle clock; 409 when not running)
curl -sS -X POST -H "X-Api-Key: $SANDBOX_API_KEY" \
  "$SANDBOX_GATEWAY_URL/sandbox/sandboxes/v1/$SANDBOX_ID/touch"

Each grant is capped by the profile max_ttl_sec (default 30 days, catalog infra/catalog/profiles/*.yaml; operators may override per tenant with SANDBOX_TENANT_MAX_TTL_SEC). Larger values are clamped, not rejected: the response carries the effective expires_at plus a warnings list. ttl_sec on the record is always expires_at - created_at.

reuse: true with a larger ttl_sec extends the reused sandbox (TTL only grows) and replaces autostop_sec with the requested value.

Session pods carry a PodDisruptionBudget, safe-to-evict: "false" and a sandbox-session PriorityClass, so routine node drains wait for them. GKE still proceeds with an upgrade after about an hour, so keep the workspace checkpointed.

Warm sessions vs warm nodes

PoolHoldsBenefit
Session warm poolReady pod + PVC for a profileFaster create/claim
Node warm placeholdersEmpty gVisor node capacityAvoids node scale-up

Neither speeds up model reasoning.

Tenancy

Ownership comes from the authenticated principal. List/get/exec only expose sandboxes in the same tenant:project.

See Caching and reuse and Architecture.