Agent Sandboxes API
The Agent Sandboxes API lets you provision isolated gVisor sandboxes for agent workloads in Quave ONE. Sandboxes are created from versioned sandbox templates whose immutable revisions pin the image, resources, persistence mode, and lifetime limits, and Quave ONE owns the whole sandbox lifecycle: allocation, idle termination, hard-lifetime expiry, pause and resume, and termination.
Availability. Agent Sandboxes are available for client regions on the Connect plans. If your account is not on a Connect plan, sandbox provisioning is not enabled for your regions — talk to Quave ONE to enable it.
Make sure to read the Get Started document to understand how the API works.
Note: These endpoints accept user tokens, and MCP or OAuth tokens carrying the scopes documented per operation. Environment tokens are not accepted, because a sandbox records the caller who requested it and an environment token identifies an environment rather than a person.
Sandboxes are account-scoped. Every request must include accountId, and the caller needs access to that account: read operations require account membership, and create or terminate operations require an admin or technical member.
Sandbox template object
A sandbox template is the named, reusable definition an agent asks for. Its current revision holds the runtime configuration.
| Field | Type | Description |
|---|---|---|
templateId | String | Template ID. |
accountId | String | Owning account ID. |
appId | String | Optional app the template belongs to. |
name | String | Human-readable template name. |
slug | String | URL-safe identifier derived from the name. |
description | String | Optional description. |
enabled | Boolean | Whether new sandboxes may be requested from this template. |
deprecatedAt | String | ISO 8601 timestamp set when the template was deprecated. Absent while active. |
currentRevisionNumber | Integer | Revision number of the latest revision. |
createdAt | String | ISO 8601 creation timestamp. |
updatedAt | String | ISO 8601 last update timestamp. |
The current revision is returned by the get-template operation:
| Field | Type | Description |
|---|---|---|
revisionId | String | Revision ID. |
templateId | String | Parent template ID. |
revision | Integer | Monotonic revision number. Revisions are immutable. |
imageDigest | String | Immutable image digest the sandbox runs. |
imageRef | String | Human-readable image reference. |
command | Array of strings | Optional entrypoint command. |
workingDir | String | Optional working directory inside the sandbox. |
ports | Array | Optional declared ports. |
resources | Object | CPU and memory request and limit values. |
persistenceMode | String | Default persistence mode for sandboxes of this revision: EPHEMERAL, DIRECTORY, SNAPSHOT, or BOTH. See Persistence modes. A sandbox may override it at creation. |
persistentWorkspace | Object | Volume modes only. sizeMb is the size of the shared volume created for a sandbox when none is given (nominal on object storage); mountPath is where the volume data directory is mounted in DIRECTORY and BOTH, default /workspace/persist. |
snapshotExclude | Array of strings | SNAPSHOT and BOTH only. Gitignore-like patterns, relative to the workspace, left out of the snapshot (for example node_modules, *.log, build/tmp). |
snapshotMaxMb | Integer | SNAPSHOT and BOTH only. Cap on the uncompressed size of the snapshot in MB. Default: 2048. |
isolationProfile | String | Runtime isolation profile. GVISOR in this version. |
idleTimeoutSeconds | Integer | Idle time after which Quave ONE suspends the sandbox. |
hardLifetimeSeconds | Integer | Maximum lifetime, after which Quave ONE terminates the sandbox. |
suspendedRetentionSeconds | Integer | How long a suspended workspace is retained. |
warmPool | Object | Optional warm-pool capacity with desired, min, and max. Mutually exclusive with the volume persistence modes. |
createdAt | String | ISO 8601 creation timestamp. |
Sandbox object
| Field | Type | Description |
|---|---|---|
sandboxId | String | Sandbox ID. |
accountId | String | Owning account ID. |
appId | String | App the sandbox belongs to. |
templateId | String | Template the sandbox was created from. |
templateRevisionId | String | Exact template revision the sandbox runs. |
templateRevisionNumber | Integer | Revision number of that revision. |
region | String | Region the sandbox runs in. |
workloadProfile | String | Workload profile the sandbox was provisioned for. |
isolationProfile | String | Runtime isolation profile, denormalized from the revision. |
persistenceMode | String | Persistence mode resolved at creation: the persistenceMode you sent, else the revision's default. |
volumeName | String | Shared volume the sandbox mounts or snapshots to. Present for the volume modes only. |
persistence | Object | Persistence status reported by the runtime: mountPath, lastSnapshotAt, lastSnapshotBytes, and lastSnapshotError (set when the last snapshot or restore failed). |
ownerKind | String | Typed owner reference kind required by the workload profile. |
ownerId | String | ID of the owner the sandbox is attached to. |
externalSessionId | String | Optional external session identifier. |
callerKey | String | Caller provenance, in the form user:USER_ID. |
status | String | Current lifecycle status. See Sandbox statuses below. |
statusReason | String | Optional short reason for the current status. |
desiredStatus | String | Status Quave ONE is driving the sandbox toward, when different from status. |
generation | Integer | Increments on every status change. Useful to detect concurrent transitions. |
isWarmAllocation | Boolean | Whether the sandbox came from a warm pool. |
resources | Object | CPU and memory the sandbox was allocated. |
claimedAt | String | ISO 8601 timestamp when the sandbox was claimed. |
readyAt | String | ISO 8601 timestamp when the sandbox became ready. |
lastActiveAt | String | ISO 8601 timestamp of the last activity. Drives idle suspension. |
suspendedAt | String | ISO 8601 timestamp of the last suspension. |
resumedAt | String | ISO 8601 timestamp of the last resume. |
terminatedAt | String | ISO 8601 termination timestamp. |
expiresAt | String | ISO 8601 hard-lifetime deadline, derived from the revision. |
terminationReason | String | Why the sandbox was terminated. |
errorCode | String | Stable product error code when the sandbox failed. |
errorRetryable | Boolean | Whether errorCode describes a condition worth retrying. |
correlationId | String | Correlation ID for tracing the sandbox across systems. |
createdAt | String | ISO 8601 creation timestamp. |
updatedAt | String | ISO 8601 last update timestamp. |
Note:
idempotencyKeyis write-only. You send it when creating a sandbox, and it is never returned in a sandbox object.
Persistence modes
Every sandbox keeps its /workspace on local, ephemeral disk. A persistence mode decides what else is kept, on a shared volume of your account backed by object storage:
| Mode | What persists | Cost |
|---|---|---|
EPHEMERAL | Nothing. The workspace is gone when the sandbox is terminated. Pause is not available. | No volume. |
DIRECTORY | Files written under the mount path (default /workspace/persist) land in the shared volume as they are closed, so they are available to the next sandbox that mounts the same volume. | The objects of the shared volume, billed as shared storage. |
SNAPSHOT | The whole workspace is archived to the shared volume when the sandbox is paused and restored before it serves again after a resume. | One archive object per pause on the shared volume. |
BOTH | Both of the above on the same volume: a live directory plus a workspace snapshot at pause. | Directory objects plus one archive per pause. |
The shared volume is an ordinary object storage volume of the account:
- When you do not name one, Quave ONE creates a volume named
sbvol-followed by the sandbox id on the account's object storage backend in the sandbox's region. The account needs an Object Storage backend on the Shared Storage page in that region, or the create request answers409. - When you pass
volumeName, the sandbox reuses that existing shared volume. It must belong to the account, live in the sandbox's region, and be active (404when it does not exist,409when it is pending purge). - Terminating a sandbox never removes its volume. Remove it from the Shared Storage page or with the shared-volume API when it is no longer needed; removal keeps the data restorable for a purge delay.
Volume modes are not available on templates with warm-pool capacity: such a create request answers 400 with persistence modes with a volume are not available on warm pools.
Create or connect a sandbox
Send a POST request to /api/public/v1/sandbox/create. Requires the quave:write:deploy scope.
| Field | Type | Required | Description |
|---|---|---|---|
accountId | String | Yes | Account that owns the sandbox. |
appId | String | Yes | App the sandbox belongs to. |
templateId | String | Yes | Sandbox template to provision from. It must be enabled, not deprecated, and have at least one revision. |
workloadProfile | String | Yes | TESS_CODE_TURN, QUAVE_INTERACTIVE_AGENT, or QUAVE_AUTOMATION_RUN. |
ownerKind | String | Yes | Owner reference kind. Must be the kind required by the workload profile: EXTERNAL_SESSION, AGENT_CONVERSATION, or AGENT_RUN. |
ownerId | String | Yes | ID of the owner the sandbox is attached to. |
externalSessionId | String | No | Optional external session identifier. |
region | String | Yes | Region to run the sandbox in. |
idempotencyKey | String | Yes | Caller-chosen key that makes this call safe to retry. |
persistenceMode | String | No | Overrides the template's default persistence mode: EPHEMERAL, DIRECTORY, SNAPSHOT, or BOTH. PERSISTENT_VOLUME is accepted as an alias of DIRECTORY. See Persistence modes. |
volumeName | String | No | Existing active shared volume of the account to attach, for the volume modes. When omitted, a volume is created for the sandbox. |
The region is validated at create time: an unknown region answers 400, a region this account cannot use answers 403, and a region without the Agent Sandbox add-on answers 409 — nothing is created in any of those cases. Accounts without the sandboxes entitlement answer 403 on every sandbox endpoint.
This operation is idempotent. The same account, app, caller, and idempotencyKey always resolve to the same sandbox:
- The first call creates the sandbox and answers
201withwasCreated: true. - Any repeated call connects to the existing sandbox and answers
200withwasCreated: false.
Retrying a create request after a network timeout is therefore safe and never provisions a second sandbox. The database uniqueness constraint is established before allocation; if that prerequisite fails, the request fails without allocating another sandbox.
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"appId": "APP_ID",
"templateId": "TEMPLATE_ID",
"workloadProfile": "QUAVE_INTERACTIVE_AGENT",
"ownerKind": "AGENT_CONVERSATION",
"ownerId": "CONVERSATION_ID",
"region": "us-5",
"idempotencyKey": "conversation-42-turn-7"
}' \
https://api.quave.cloud/api/public/v1/sandbox/create
Example response:
{
"success": true,
"wasCreated": true,
"sandbox": {
"sandboxId": "SANDBOX_ID",
"accountId": "ACCOUNT_ID",
"appId": "APP_ID",
"templateId": "TEMPLATE_ID",
"templateRevisionId": "TEMPLATE_REVISION_ID",
"templateRevisionNumber": 3,
"region": "us-5",
"workloadProfile": "QUAVE_INTERACTIVE_AGENT",
"isolationProfile": "GVISOR",
"persistenceMode": "EPHEMERAL",
"ownerKind": "AGENT_CONVERSATION",
"ownerId": "CONVERSATION_ID",
"callerKey": "user:USER_ID",
"status": "REQUESTED",
"generation": 0,
"expiresAt": "2026-08-07T13:00:00.000Z",
"createdAt": "2026-08-07T12:00:00.000Z"
}
}
List sandboxes
Send a GET request to /api/public/v1/sandboxes. Requires the quave:read scope.
| Query parameter | Type | Description |
|---|---|---|
accountId | String | Account ID. Required. |
appId | String | Optional app filter. |
status | String | Optional status filter. Pass one status or comma-separated statuses, for example READY,BUSY. Unknown statuses are ignored. |
limit | Integer | Sandboxes per page. Default: 50. Maximum: 100. |
page | Integer | One-based page number. Default: 1. |
The response includes accountId, page, limit, totalCount, and sandboxes, sorted by creation time, newest first.
curl -X GET \
-H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/sandboxes?accountId=ACCOUNT_ID&status=READY,BUSY&page=1&limit=20'
Get a sandbox
Send a GET request to /api/public/v1/sandbox. Requires the quave:read scope.
| Query parameter | Type | Description |
|---|---|---|
accountId | String | Account ID. Required. |
sandboxId | String | Sandbox ID. Required. |
curl -X GET \
-H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/sandbox?accountId=ACCOUNT_ID&sandboxId=SANDBOX_ID'
Terminate a sandbox
Send a POST request to /api/public/v1/sandbox/terminate. Requires the quave:write:deploy scope.
| Field | Type | Required | Description |
|---|---|---|---|
accountId | String | Yes | Account ID. |
sandboxId | String | Yes | Sandbox ID. |
Termination is idempotent. Terminating a sandbox that is already terminated succeeds and returns the sandbox unchanged, so retries are safe. Termination never removes the sandbox's shared volume; see Persistence modes. A sandbox that has not reached a running state yet is first marked as failed and then terminated, so no sandbox is ever left holding compute.
If the sandbox status changes concurrently while the request is being processed, Quave ONE answers 409 with Sandbox is changing state. Retry the call.
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"sandboxId": "SANDBOX_ID"
}' \
https://api.quave.cloud/api/public/v1/sandbox/terminate
Example response:
{
"success": true,
"sandbox": {
"sandboxId": "SANDBOX_ID",
"status": "TERMINATED",
"terminationReason": "USER_REQUESTED",
"terminatedAt": "2026-08-07T12:30:00.000Z"
}
}
Pause a sandbox
Send a POST request to /api/public/v1/sandbox/pause. Requires the quave:write:deploy scope.
Pausing releases the sandbox's compute while keeping its shared volume. In the SNAPSHOT and BOTH modes the workspace is archived to the volume first. Only a READY or BUSY sandbox with a volume persistence mode can be paused; work in flight on a BUSY sandbox is lost.
| Field | Type | Required | Description |
|---|---|---|---|
accountId | String | Yes | Account ID. |
sandboxId | String | Yes | Sandbox ID. |
snapshotExclude | Array of strings | No | Overrides the revision's snapshot exclude patterns for this pause only. |
snapshotMaxMb | Integer | No | Overrides the snapshot size cap for this pause only. |
force | Boolean | No | Snapshot even when the restore at start failed. Without it such a pause is refused so the last good snapshot is not overwritten by a near-empty workspace. |
On success the sandbox becomes SUSPENDED (paused) and, when the mode snapshots, persistence.lastSnapshotAt and persistence.lastSnapshotBytes describe the archive that was written. Pausing an already paused sandbox succeeds and returns it unchanged.
Failures:
400for anEPHEMERALsandbox: there is nothing to keep; terminate it instead.409when the sandbox is notREADYorBUSY.502when the snapshot failed and503when the sandbox has no reachable pod yet. In both cases the sandbox is left running, its status goes back to what it was, and the response carriesdata.retryable: true, plusdata.retryAfterSecondson503. Retry the pause after that delay.
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"sandboxId": "SANDBOX_ID",
"snapshotExclude": ["node_modules", "*.log"]
}' \
https://api.quave.cloud/api/public/v1/sandbox/pause
Example response:
{
"success": true,
"sandbox": {
"sandboxId": "SANDBOX_ID",
"status": "SUSPENDED",
"persistenceMode": "SNAPSHOT",
"volumeName": "sbvol-sandbox_id",
"persistence": {
"lastSnapshotAt": "2026-09-09T12:30:00.000Z",
"lastSnapshotBytes": 5242880
},
"suspendedAt": "2026-09-09T12:30:00.000Z"
}
}
Resume a sandbox
Send a POST request to /api/public/v1/sandbox/resume. Requires the quave:write:deploy scope.
| Field | Type | Required | Description |
|---|---|---|---|
accountId | String | Yes | Account ID. |
sandboxId | String | Yes | Sandbox ID. |
A SUSPENDED sandbox moves to RESUMING, is deployed again with the same sandbox id on its shared volume, and, in the SNAPSHOT and BOTH modes, restores its latest snapshot before it serves. Poll Get a sandbox until the status is READY. Resuming a sandbox that is already RESUMING, READY, or BUSY returns it unchanged; any other status answers 409.
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"sandboxId": "SANDBOX_ID"
}' \
https://api.quave.cloud/api/public/v1/sandbox/resume
List sandbox templates
Send a GET request to /api/public/v1/sandbox-templates. Requires the quave:read scope.
| Query parameter | Type | Description |
|---|---|---|
accountId | String | Account ID. Required. |
appId | String | Optional app filter. |
includeDeprecated | Boolean | Include deprecated templates. Default: false. |
curl -X GET \
-H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/sandbox-templates?accountId=ACCOUNT_ID'
Get a sandbox template
Send a GET request to /api/public/v1/sandbox-template. Requires the quave:read scope.
| Query parameter | Type | Description |
|---|---|---|
accountId | String | Account ID. Required. |
templateId | String | Template ID. Required. |
The response contains template and currentRevision. currentRevision is null when the template has no revision yet, and a template without a revision cannot serve sandboxes.
curl -X GET \
-H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/sandbox-template?accountId=ACCOUNT_ID&templateId=TEMPLATE_ID'
Save a sandbox template
Send a POST request to /api/public/v1/sandbox-template/save. Requires the quave:write:config scope.
| Field | Type | Required | Description |
|---|---|---|---|
accountId | String | Yes | Account that owns the template. |
templateId | String | No | Template to update. Omit to create a new template. |
appId | String | No | Optional app the template belongs to (create only). |
name | String | Yes | Human-readable template name. |
description | String | No | Optional description. |
enabled | Boolean | No | Whether new sandboxes may be requested from this template. Default: true. |
Creating answers 201; updating answers 200. Runtime configuration (image, command, resources, lifetimes) is never edited here — it lives in immutable revisions.
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"name": "python-repl"
}' \
https://api.quave.cloud/api/public/v1/sandbox-template/save
Create a sandbox template revision
Send a POST request to /api/public/v1/sandbox-template/revision. Requires the quave:write:config scope.
Revisions are immutable and append-only: new sandboxes pin the latest revision, and already-allocated sandboxes keep the revision they started from.
networkControl optionally restricts egress. Its triggers are independent:
- CIDR/IP only:
{"enabled":true,"addressAllowlist":["203.0.113.8"]}. Enabling address restrictions requires at least one non-blank address. - Domain only:
{"enabled":false,"domainAllowlist":["github.com"]}. The non-empty domain list activates the egress proxy even though address restrictions are disabled. - Combined: set
enabled:trueand provide both lists. Domains go through the proxy; address exceptions are enforced by network policy. This is not a domain/IP intersection.
Blank-only domain lists are rejected when creating a revision. IP/domain syntax, reserved infrastructure ranges, and runtime enforcement are additionally validated by the infrastructure service at sandbox allocation. Infrastructure ranges remain denied. Omit the whole block for default egress; an empty domain list does not activate domain restrictions. Existing immutable revisions are not rewritten by these checks.
| Field | Type | Required | Description |
|---|---|---|---|
accountId | String | Yes | Account ID. |
templateId | String | Yes | Template to append the revision to. |
imageDigest | String | Yes | Digest-pinned image (repo/image@sha256:<64 hex chars>). |
imageRef | String | No | Human-readable image reference (informational). |
command | Array of strings | No | Entrypoint argv — never a shell string. Without it the image entrypoint must be long-running, or the sandbox exits immediately and never becomes READY. |
workingDir | String | No | Working directory inside the sandbox. |
zClouds | Integer | No | Sandbox size in zClouds. Default: 1. |
persistenceMode | String | No | Default persistence mode: EPHEMERAL (default), DIRECTORY, SNAPSHOT, or BOTH. PERSISTENT_VOLUME is accepted as an alias of DIRECTORY. |
volumeSizeMb | Integer | No | Size in MB of the shared volume created for a sandbox when none is given (nominal on object storage). Volume modes only. |
mountPath | String | No | Where the volume data directory is mounted in DIRECTORY and BOTH. Default: /workspace/persist. |
snapshotExclude | Array of strings | No | SNAPSHOT and BOTH only. Gitignore-like patterns left out of the snapshot. |
snapshotMaxMb | Integer | No | SNAPSHOT and BOTH only. Cap on the uncompressed snapshot size in MB. Default: 2048. |
idleTimeoutSeconds | Integer | No | Idle time after which the sandbox is terminated. |
hardLifetimeSeconds | Integer | No | Maximum lifetime, after which the sandbox is terminated. |
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"templateId": "TEMPLATE_ID",
"imageDigest": "docker.io/library/debian:bookworm-slim@sha256:...",
"imageRef": "debian:bookworm-slim",
"command": ["sleep", "infinity"],
"zClouds": 1
}' \
https://api.quave.cloud/api/public/v1/sandbox-template/revision
Execute a command in a sandbox
Send a POST request to /api/public/v1/sandbox/execute. Requires the quave:write:dangerous scope.
Personal-authentication runtimes are excluded from generic command, file and orchestrator-session access (HTTP 403), including for account administrators. Manage a personal connection through its owning Warren Solution. The owner connection controller uses a restricted internal process path; this does not enable personal authentication or generic file access through the Sandbox API. Managed personal run dispatch remains separately gated.
Runs a command to completion inside the sandbox through the region gateway and returns its exit code and captured output. The sandbox must be READY or BUSY and carry the sandbox runtime agent; a sandbox deployed without the agent answers 409. Output is returned to you and never stored by Quave ONE.
| Field | Type | Required | Description |
|---|---|---|---|
accountId | String | Yes | Account ID. |
sandboxId | String | Yes | Sandbox ID. |
command | Array of strings | Yes | Command argv — never a shell string. For shell features pass ["sh", "-c", "..."]. |
cwd | String | No | Working directory for the command. |
envVars | Object | No | Extra environment variables for the command. |
timeoutSeconds | Integer | No | Command deadline in seconds. Default: 120. Max: 900. |
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"sandboxId": "SANDBOX_ID",
"command": ["sh", "-c", "python3 -V && uname -a"]
}' \
https://api.quave.cloud/api/public/v1/sandbox/execute
Executing a command counts as sandbox activity for idle suspension. The audit trail records that a command ran (and its exit code) but never the command line, environment variables, or output.
Sandbox statuses
| Status | Description |
|---|---|
REQUESTED | The sandbox was requested and is waiting to be allocated. |
ALLOCATING | Quave ONE is allocating capacity for the sandbox. |
STARTING | The sandbox is starting. |
READY | The sandbox is running and idle. It can accept work. |
BUSY | The sandbox is running work. |
SUSPENDING | Pausing: the workspace is being snapshotted (when the mode snapshots) and the compute released. If the runtime cannot pause, the sandbox goes back to READY or BUSY. |
SUSPENDED | Paused. Compute was released and the shared volume is kept. Resume it with Resume a sandbox. |
RESUMING | The sandbox is being deployed again with the same id; its snapshot is restored before it serves. |
FAILED | The sandbox failed. See errorCode and errorRetryable. |
TERMINATING | Termination is in progress. |
TERMINATED | Terminal status. The sandbox no longer exists and cannot be resumed. |
Quave ONE drives these transitions itself. Sandboxes idle for longer than their template's idleTimeoutSeconds are terminated, and so are sandboxes that reach expiresAt, without any call from you. Pause and resume are explicit calls. A paused sandbox does not count toward the account's active-sandbox quota.
Retryable errors
Sandbox failures surface as stable product error codes rather than raw runtime messages, so you can branch on them safely. The sandbox object carries errorCode together with errorRetryable, which tells you whether waiting and retrying can succeed.
| Error code | Retryable | Description |
|---|---|---|
SANDBOX_SUSPENDED | Yes | The sandbox is suspended and must be resumed first. |
SANDBOX_NOT_READY | Yes | The sandbox is not ready to accept work yet. |
AGENT_UNREACHABLE | Yes | The sandbox runtime is temporarily unreachable. |
SANDBOX_NOT_FOUND | No | The sandbox does not exist in the runtime. |
NAMESPACE_DENIED | No | The sandbox runtime rejected the namespace. |
UNAUTHENTICATED | No | The runtime credential was not accepted. |
TOKEN_EXPIRED | No | The runtime credential expired. |
INSUFFICIENT_SCOPE | No | The runtime credential lacked the required scope. |
PATH_ESCAPE | No | A file path outside the sandbox workspace was rejected. |
UNKNOWN | No | Unclassified failure. Unmapped runtime errors fail closed to this code. |
Retryable codes are safe to retry with backoff, ideally reusing the same idempotencyKey so a retry reconnects instead of provisioning a second sandbox.
Note: Streaming output and reading or writing files inside a sandbox are not part of this API version. Running a command to completion is covered by Execute a command in a sandbox; beyond that, this version covers the sandbox and sandbox template control plane only.
Personal Warren run admission
The Warren Solution may delegate an already owner-reserved private task with
POST /sandbox-orchestrator/codex-runs/claim under the public API prefix.
This is not a user/API-key endpoint: it requires the matching installation service
principal with sandbox:create plus a separate one-task owner grant.
The body contains only grant and runId. The grant expires after five minutes
if unclaimed; expiration does not release authentication storage or its operation
fence. A successful claim permanently binds that operation to one run ID. Repeating
the same claim recovers the receipt; another run ID or installation is denied.
Current owner access and private GitHub push access are rechecked. The response contains the bound run/operation/sandbox IDs and approved repository, base branch, model and token/time bounds, never a prompt, grant, OAuth file or gateway token. The audit records the claim action and run identity without the grant or prompt. The bound task may allocate only its dedicated managed holder using the endpoint below; its fixed process endpoint is described afterward. MCP and ordinary sandbox sessions cannot claim a personal subscription through an account administrator role.
After a successful binding, the matching service principal with sandbox:read
can recover the same metadata receipt with GET /sandbox-orchestrator/codex-runs/:runId
without retaining the raw grant. This read cannot create a binding or authorize a
different task, and current installation ownership is still checked. This binding
endpoint is for an unfinished operation; completed cleanup recovery uses the
sandbox/execution endpoints below.
The same installation principal can control its already claimed holder at
/sandbox-orchestrator/codex-runs/:runId/sandbox:
POSTrequiressandbox:createand an empty body. Repeated calls prepare the existing authentication generation and then allocate the exact reserved, supervised holder. Template, command, environment and credentials cannot be supplied. Owner/private repository access is checked again during allocation. Missing authentication is quarantined, never recreated as an empty login.GETrequiressandbox:read. It returns onlysandboxId,supervisedandphase; it cannot start a process or expose runtime locators and diagnostics.DELETErequiressandbox:terminate. After a persisted trusted exit code 0, it reconciles whole-holder termination and the next released authentication generation before preserving the local sign-in. Otherwise it performs a destructive reset and requires a fresh login. An explicit owner reset always wins over successful cleanup. No action revokes the upstream OpenAI grant.
These calls have no request fields and never mint a gateway session. Repeat cleanup
until phase: terminated and cleanupComplete: true; terminating is not proof
of released storage. Terminal responses include authPreserved, describing that
cleanup, not the current login state. A metadata-only receipt survives clearing
the operation and later runs: old GET/DELETE calls return that receipt without
operating on a newer task. Current owner and active installation principal access
are still required. Completed run IDs cannot authorize another task.
The owner may reset an unclaimed reservation through the Solution connection UI.
No lease is cleared solely because it expired.
For an allocated, claimed holder, /sandbox-orchestrator/codex-runs/:runId/execution
accepts POST (sandbox:create) and GET (sandbox:read). POST accepts only
callbackToken and eventIngestToken, the run-scoped Warren callback credentials;
they never grant owner authority or persist in the connection. The platform checks
their run identity and chooses the installed control host, owner GitHub credential,
frozen task and fixed owner-only entrypoint. Command, environment, seed manifests
and callback URLs are not caller-configurable. Shared Warren inbox messages are
not consumed. An image without the dedicated entrypoint fails before execution.
GET never starts or retries execution. Responses contain only the run/execution
identity, pending, running or exited phase, and a witnessed exit code with a
curated termination cause. Raw process output and runtime locators are excluded.
Starting is one-shot: an uncertain start is quarantined, not repeated. Process
exit does not release the authentication generation or mark the connection ready;
whole-holder teardown remains a separate step. No signal/stream endpoint or generic
MCP provider access is added. After cleanup, GET also returns the terminal
receipt's cleanupComplete, authPreserved and sandbox identity. Cancellation
without an execution witness has a null exit code, never an invented successful
exit. End-to-end owner dispatch and real subscription qualification remain gated.