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 useSANDBOX_MAX_SECONDS(default- and the runner clamps to
MAX_SANDBOX_SECONDS(default 14400).
- and the runner clamps to
- Jobs are sharded across
RUNNER_SHARDSrunner containers by assessment id, and each container runsRUNNER_CONCURRENCYjobs, 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.