A sandbox is a computer, not just a scan target. POST /api/v1/sandboxes has two modes: a general sandbox with no target, and a target-bound batch.

The sandbox object is the assessment plus run detail:

{ "…assessment fields…",
  "providerSandboxId": "…or null", "runStatus": "running",
  "egress": ["registry.npmjs.org:443"], "exitCode": null, "result": null }

Launch

Scope assessments:create for both modes.

General sandbox

{ "kind": "sandbox", "budgetMicros": 1000000,
  "egress": ["npm"], "idempotencyKey": "…" }

"targetless": true works too. Returns 201, or 200 on an idempotent retry.

Status error
400 invalid_egress (adds message)
402 insufficient_credits
400 sandbox_launch_failed

Target-bound batch

{ "count": 3, "budgetMicros": 2000000, "instruction": "…",
  "model": "…", "idempotencyKey": "…" }

count defaults to 1 and is capped at 5 (MAX_SANDBOX_BATCH). Returns 201, or 200 when every member was reused:

Status error
409 target_not_ready
429 sandbox_limit (adds active, limit)
402 insufficient_credits (adds requiredMicros, balanceMicros)
409 authentication_required / model_access_required (adds setupUrl)

Attach a target to a running sandbox

POST /api/v1/sandboxes/:id/config attaches a target, project and task. authorized is required.

{ "target": "https://staging.example.com", "project": "Staging",
  "instruction": "Test cross-tenant invoice access.", "authorized": true }

The response includes verificationRequired and setupUrl. verificationRequired is false for a host the workspace owns or allowlists.

Drive it

Method Path What it does
POST /sandboxes/:id/exec { command, cwd?, user?: "root"|"worker", timeoutSeconds? } → exit code, stdout, stderr
POST /sandboxes/:id/files write a file: { path, content, encoding?: "utf8"|"base64" }
GET /sandboxes/:id/files?path= read a file (base64, 1 MiB cap, reports truncation)
POST /sandboxes/:id/egress open extra egress: explicit hosts, or a preset
GET /sandboxes/:id/result exitCode, result.stdout/stderr/commands/artifacts
POST /sandboxes/:id/fork start a second general sandbox
POST /sandboxes/:id/stop stop and refund unused budget

exec needs assessments:create; reads need assessments:read. A finished sandbox answers 409 sandbox_not_running; a runner not holding it answers 409 sandbox_not_live.

timeoutSeconds runs to 3600. Above 600 seconds the runner switches to a detached command and polls it, so one long build is one call.

Egress

Entries are exact host or host:port (port defaults to 443). Private and local addresses, wildcards, and database or mail ports are refused, and the list is capped at 20 entries.

Presets: npm, pypi, github, go, crates, maven, rubygems.

A launch may pass egress when kind is sandbox, and the list may grow while the sandbox runs. Egress is additive: there is no call that removes an entry.

Lifetime and concurrency

  • A job may request maxSeconds; launches use SANDBOX_MAX_SECONDS (default
    1. and the runner clamps to MAX_SANDBOX_SECONDS (default 14400).
  • Jobs are sharded across RUNNER_SHARDS runner containers by assessment id, and each container runs RUNNER_CONCURRENCY jobs, so the same sandbox always lands on the container holding its job.

Idempotency

Launch routes accept an Idempotency-Key header, or an idempotencyKey body field, of up to 100 characters. The same key returns the original resource with 200 instead of creating a second one with 201.

POST /api/v1/sandboxes with a batch count suffixes the base key per member (key:0, key:1, …), so a retried batch reuses every member.

Lineage

GET /api/v1/sandboxes (assessments:read) returns { "sandboxes": [assessment] }. POST /api/v1/sandboxes/:id/fork starts a second general sandbox from the same starting point — useful when you want a clean box without paying for a new target verification.