Sandbox API

Errors and limits

HTTP status codes, concurrency quota, and request input bounds.

When something fails, start here. A 400 will never succeed unchanged; a 429 means wait and retry; a sync 504 means poll the job id — do not resubmit.

CodeMeaning
400Validation — unknown catalog agent, bad options, or a task_archive_url the platform will not fetch
401Missing or invalid API key / Bearer token
403Authenticated, but not permitted (wrong project, or provisioning without admin key)
413CodeEdit payload too large
429Project concurrency quota exhausted — honour Retry-After
502The run reached the platform and failed inside it
503Upstream or credential plane unavailable
504Sync run/datapoint outlasted its wait — the run keeps going; job id is in headers

Nothing on the platform answers a timeout with a 500. If you see one, that is a bug worth reporting rather than something to retry around.

Long-running requests and who times out first

POST /v2/run/datapoint holds the connection open while your run happens. Each layer waits longer than the one inside it:

LayerBudgetOn expiry
Harbor's own wait1500s504 naming the job id
Gateway1560s for run/datapoint, 120s elsewhere, unbounded for SSE504 (run not cancelled)
Load balancer1800sconnection closed with no body

If your runs take minutes, use POST /v2/jobs/execute-tasks. It returns a job id immediately. The synchronous route is a convenience for short runs only.

A 504 is never a signal to resubmit — that buys a second run competing for the same concurrency quota.

Quota is concurrency, not request rate

The limit is on how many jobs a project runs at once, not on how often you may submit. There is no requests-per-second throttle on the v2 routes, so do not expect one to protect you from a runaway loop.

The effective limit is resolved server-side for the authenticated tenant:project: exact project policy, then tenant default, then the environment default. A caller cannot raise its own limit with a request field. The operator also sets a maximum configurable ceiling so one project cannot be provisioned beyond the fleet and trial namespace capacity.

Read your current policy and usage:

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

The response includes max_concurrent, current_in_flight, available_slots, and whether the value came from a project, tenant default, or environment default. On shared dev the environment default and operator ceiling are both 8.

What shares one counter today

GET /sandbox/v1/quota reports the main tenant:project bucket. These surfaces bill that bucket while they are in flight:

SurfaceCounts toward GET /quota?
Harbor trials and standalone agent runsYes
Cloud IDE (ide-ubuntu) and desktop sandboxesYes — one slot per running session
Agent attach with sandbox_idNo extra slot (uses the session's slot)
CodeEdit (POST /codeedit/v1/executions)No — separate internal bucket (codeedit:{project}), not shown on GET /quota

So an open Cloud IDE and a Harbor trial compete for the same max_concurrent on the main bucket. Terminal CLIs inside the IDE (claude, codex, …) do not take additional slots — only the session pod does.

Catalog profiles declare a quota_class (harbor, agent, codeedit, sandboxes) for documentation; per-class limits and a multi-pool GET /quota response are not implemented yet.

Over the limit on the main bucket:

SurfaceBehaviour when over quota
POST /sandboxes/v1 (create/start IDE or desktop)429 when at cap
POST /harbor/v2/run/datapoint (sync)429 with Retry-After
POST /codeedit/v1/executions429 with Retry-After (CodeEdit bucket)
POST /harbor/v2/jobs/execute-tasks (async)Accepted with 202; the job waits in queued and the worker retries as capacity frees

The async route deliberately does not reject: being over the cap is a transient capacity condition, and failing a submitted job for it would report "your job failed" when the truth is "your project was busy".

Running the platform yourself? The cap is enforced when the quota scheduler is enabled (SANDBOX_L2_SCHEDULER=quota with SANDBOX_REDIS_URL set); SANDBOX_PROJECT_MAX_CONCURRENT changes the fallback default and SANDBOX_PROJECT_MAX_CONFIGURABLE bounds admin-provisioned policies. Raise the ceiling only together with the worker fleet and trial quota — see the worker elasticity runbook.

Bounds on a task archive

The platform fetches a URL you supply, so the fetch is bounded: 256 MiB downloaded, 1 GiB expanded, 20,000 entries. Full contract, including the presigned-URL rules: the task archive contract.

Harbor job failures

Poll GET /sandbox/harbor/v2/jobs/{job_id}status: failed includes error detail. Check worker logs for subprocess/Harbor CLI output.

A job can finish with status: succeeded and still contain a trial you must not score — for example one trial of several raised AgentTimeoutError. Gate scoring on outcome.valid_for_scoring, not on status. See Reading the result.

An agent run reports completion rather than outcome, because nothing grades an ad-hoc run: it tells you whether the agent finished, and makes no claim about scoring.

Desktop / viewer errors

StatusMeaning
409Sandbox not running yet — no viewer.url
401Invalid or missing viewer token on the stream
503Upstream stream (pod / Guacamole / Mac) not ready
502Viewer proxy cannot reach the substrate

Details: Viewer API.