Skip to main content

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.

FieldTypeDescription
templateIdStringTemplate ID.
accountIdStringOwning account ID.
appIdStringOptional app the template belongs to.
nameStringHuman-readable template name.
slugStringURL-safe identifier derived from the name.
descriptionStringOptional description.
enabledBooleanWhether new sandboxes may be requested from this template.
deprecatedAtStringISO 8601 timestamp set when the template was deprecated. Absent while active.
currentRevisionNumberIntegerRevision number of the latest revision.
createdAtStringISO 8601 creation timestamp.
updatedAtStringISO 8601 last update timestamp.

The current revision is returned by the get-template operation:

FieldTypeDescription
revisionIdStringRevision ID.
templateIdStringParent template ID.
revisionIntegerMonotonic revision number. Revisions are immutable.
imageDigestStringImmutable image digest the sandbox runs.
imageRefStringHuman-readable image reference.
commandArray of stringsOptional entrypoint command.
workingDirStringOptional working directory inside the sandbox.
portsArrayOptional declared ports.
resourcesObjectCPU and memory request and limit values.
persistenceModeStringDefault persistence mode for sandboxes of this revision: EPHEMERAL, DIRECTORY, SNAPSHOT, or BOTH. See Persistence modes. A sandbox may override it at creation.
persistentWorkspaceObjectVolume 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.
snapshotExcludeArray of stringsSNAPSHOT and BOTH only. Gitignore-like patterns, relative to the workspace, left out of the snapshot (for example node_modules, *.log, build/tmp).
snapshotMaxMbIntegerSNAPSHOT and BOTH only. Cap on the uncompressed size of the snapshot in MB. Default: 2048.
isolationProfileStringRuntime isolation profile. GVISOR in this version.
idleTimeoutSecondsIntegerIdle time after which Quave ONE suspends the sandbox.
hardLifetimeSecondsIntegerMaximum lifetime, after which Quave ONE terminates the sandbox.
suspendedRetentionSecondsIntegerHow long a suspended workspace is retained.
warmPoolObjectOptional warm-pool capacity with desired, min, and max. Mutually exclusive with the volume persistence modes.
createdAtStringISO 8601 creation timestamp.

Sandbox object

FieldTypeDescription
sandboxIdStringSandbox ID.
accountIdStringOwning account ID.
appIdStringApp the sandbox belongs to.
templateIdStringTemplate the sandbox was created from.
templateRevisionIdStringExact template revision the sandbox runs.
templateRevisionNumberIntegerRevision number of that revision.
regionStringRegion the sandbox runs in.
workloadProfileStringWorkload profile the sandbox was provisioned for.
isolationProfileStringRuntime isolation profile, denormalized from the revision.
persistenceModeStringPersistence mode resolved at creation: the persistenceMode you sent, else the revision's default.
volumeNameStringShared volume the sandbox mounts or snapshots to. Present for the volume modes only.
persistenceObjectPersistence status reported by the runtime: mountPath, lastSnapshotAt, lastSnapshotBytes, and lastSnapshotError (set when the last snapshot or restore failed).
ownerKindStringTyped owner reference kind required by the workload profile.
ownerIdStringID of the owner the sandbox is attached to.
externalSessionIdStringOptional external session identifier.
callerKeyStringCaller provenance, in the form user:USER_ID.
statusStringCurrent lifecycle status. See Sandbox statuses below.
statusReasonStringOptional short reason for the current status.
desiredStatusStringStatus Quave ONE is driving the sandbox toward, when different from status.
generationIntegerIncrements on every status change. Useful to detect concurrent transitions.
isWarmAllocationBooleanWhether the sandbox came from a warm pool.
resourcesObjectCPU and memory the sandbox was allocated.
claimedAtStringISO 8601 timestamp when the sandbox was claimed.
readyAtStringISO 8601 timestamp when the sandbox became ready.
lastActiveAtStringISO 8601 timestamp of the last activity. Drives idle suspension.
suspendedAtStringISO 8601 timestamp of the last suspension.
resumedAtStringISO 8601 timestamp of the last resume.
terminatedAtStringISO 8601 termination timestamp.
expiresAtStringISO 8601 hard-lifetime deadline, derived from the revision.
terminationReasonStringWhy the sandbox was terminated.
errorCodeStringStable product error code when the sandbox failed.
errorRetryableBooleanWhether errorCode describes a condition worth retrying.
correlationIdStringCorrelation ID for tracing the sandbox across systems.
createdAtStringISO 8601 creation timestamp.
updatedAtStringISO 8601 last update timestamp.

Note: idempotencyKey is 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:

ModeWhat persistsCost
EPHEMERALNothing. The workspace is gone when the sandbox is terminated. Pause is not available.No volume.
DIRECTORYFiles 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.
SNAPSHOTThe 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.
BOTHBoth 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 answers 409.
  • 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 (404 when it does not exist, 409 when 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.

FieldTypeRequiredDescription
accountIdStringYesAccount that owns the sandbox.
appIdStringYesApp the sandbox belongs to.
templateIdStringYesSandbox template to provision from. It must be enabled, not deprecated, and have at least one revision.
workloadProfileStringYesTESS_CODE_TURN, QUAVE_INTERACTIVE_AGENT, or QUAVE_AUTOMATION_RUN.
ownerKindStringYesOwner reference kind. Must be the kind required by the workload profile: EXTERNAL_SESSION, AGENT_CONVERSATION, or AGENT_RUN.
ownerIdStringYesID of the owner the sandbox is attached to.
externalSessionIdStringNoOptional external session identifier.
regionStringYesRegion to run the sandbox in.
idempotencyKeyStringYesCaller-chosen key that makes this call safe to retry.
persistenceModeStringNoOverrides the template's default persistence mode: EPHEMERAL, DIRECTORY, SNAPSHOT, or BOTH. PERSISTENT_VOLUME is accepted as an alias of DIRECTORY. See Persistence modes.
volumeNameStringNoExisting 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 201 with wasCreated: true.
  • Any repeated call connects to the existing sandbox and answers 200 with wasCreated: 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 parameterTypeDescription
accountIdStringAccount ID. Required.
appIdStringOptional app filter.
statusStringOptional status filter. Pass one status or comma-separated statuses, for example READY,BUSY. Unknown statuses are ignored.
limitIntegerSandboxes per page. Default: 50. Maximum: 100.
pageIntegerOne-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 parameterTypeDescription
accountIdStringAccount ID. Required.
sandboxIdStringSandbox 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.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox 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.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox ID.
snapshotExcludeArray of stringsNoOverrides the revision's snapshot exclude patterns for this pause only.
snapshotMaxMbIntegerNoOverrides the snapshot size cap for this pause only.
forceBooleanNoSnapshot 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:

  • 400 for an EPHEMERAL sandbox: there is nothing to keep; terminate it instead.
  • 409 when the sandbox is not READY or BUSY.
  • 502 when the snapshot failed and 503 when 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 carries data.retryable: true, plus data.retryAfterSeconds on 503. 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.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox 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 parameterTypeDescription
accountIdStringAccount ID. Required.
appIdStringOptional app filter.
includeDeprecatedBooleanInclude 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 parameterTypeDescription
accountIdStringAccount ID. Required.
templateIdStringTemplate 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.

FieldTypeRequiredDescription
accountIdStringYesAccount that owns the template.
templateIdStringNoTemplate to update. Omit to create a new template.
appIdStringNoOptional app the template belongs to (create only).
nameStringYesHuman-readable template name.
descriptionStringNoOptional description.
enabledBooleanNoWhether 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:true and 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.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
templateIdStringYesTemplate to append the revision to.
imageDigestStringYesDigest-pinned image (repo/image@sha256:<64 hex chars>).
imageRefStringNoHuman-readable image reference (informational).
commandArray of stringsNoEntrypoint argv — never a shell string. Without it the image entrypoint must be long-running, or the sandbox exits immediately and never becomes READY.
workingDirStringNoWorking directory inside the sandbox.
zCloudsIntegerNoSandbox size in zClouds. Default: 1.
persistenceModeStringNoDefault persistence mode: EPHEMERAL (default), DIRECTORY, SNAPSHOT, or BOTH. PERSISTENT_VOLUME is accepted as an alias of DIRECTORY.
volumeSizeMbIntegerNoSize in MB of the shared volume created for a sandbox when none is given (nominal on object storage). Volume modes only.
mountPathStringNoWhere the volume data directory is mounted in DIRECTORY and BOTH. Default: /workspace/persist.
snapshotExcludeArray of stringsNoSNAPSHOT and BOTH only. Gitignore-like patterns left out of the snapshot.
snapshotMaxMbIntegerNoSNAPSHOT and BOTH only. Cap on the uncompressed snapshot size in MB. Default: 2048.
idleTimeoutSecondsIntegerNoIdle time after which the sandbox is terminated.
hardLifetimeSecondsIntegerNoMaximum 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.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox ID.
commandArray of stringsYesCommand argv — never a shell string. For shell features pass ["sh", "-c", "..."].
cwdStringNoWorking directory for the command.
envVarsObjectNoExtra environment variables for the command.
timeoutSecondsIntegerNoCommand 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

StatusDescription
REQUESTEDThe sandbox was requested and is waiting to be allocated.
ALLOCATINGQuave ONE is allocating capacity for the sandbox.
STARTINGThe sandbox is starting.
READYThe sandbox is running and idle. It can accept work.
BUSYThe sandbox is running work.
SUSPENDINGPausing: 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.
SUSPENDEDPaused. Compute was released and the shared volume is kept. Resume it with Resume a sandbox.
RESUMINGThe sandbox is being deployed again with the same id; its snapshot is restored before it serves.
FAILEDThe sandbox failed. See errorCode and errorRetryable.
TERMINATINGTermination is in progress.
TERMINATEDTerminal 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 codeRetryableDescription
SANDBOX_SUSPENDEDYesThe sandbox is suspended and must be resumed first.
SANDBOX_NOT_READYYesThe sandbox is not ready to accept work yet.
AGENT_UNREACHABLEYesThe sandbox runtime is temporarily unreachable.
SANDBOX_NOT_FOUNDNoThe sandbox does not exist in the runtime.
NAMESPACE_DENIEDNoThe sandbox runtime rejected the namespace.
UNAUTHENTICATEDNoThe runtime credential was not accepted.
TOKEN_EXPIREDNoThe runtime credential expired.
INSUFFICIENT_SCOPENoThe runtime credential lacked the required scope.
PATH_ESCAPENoA file path outside the sandbox workspace was rejected.
UNKNOWNNoUnclassified 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:

  • POST requires sandbox:create and 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.
  • GET requires sandbox:read. It returns only sandboxId, supervised and phase; it cannot start a process or expose runtime locators and diagnostics.
  • DELETE requires sandbox: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.