Sandbox API

Errors and limits

HTTP status codes, quota, admission, and the bounds on request input.

CodeMeaning
400Validation — unknown catalog agent, bad agent_options / harbor_extensions key, invalid opencode_config (see OpenCode flags), or a task_archive_url that cannot be fetched or is not a zip (wrong scheme, private host, tarball)
401Missing or invalid X-Api-Key / Bearer
403Authenticated, but not permitted — a team token naming another project, or provisioning without a valid admin key
413Submitted files exceed the CodeEdit payload limit
429Project concurrency quota exhausted. Honour Retry-After
502The run reached the platform and failed inside it
503An upstream service or the credential plane is unavailable
504A synchronous run/datapoint outlasted its wait. The run is still going — the job id is in the response headers, so poll it rather than resubmitting

A 400 is worth acting on and a 429 is worth waiting on, so the split matters: a request refused with 400 will never succeed unchanged.

Nothing on the platform answers a timeout with a 500. If you ever 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 deliberately holds the connection open while your run happens, so it needs a wider budget than anything else. Each layer waits longer than the one inside it, so the layer that knows what happened is the one that answers:

LayerBudgetOn expiry
Harbor's own wait1500s504 naming the job id, so you can keep following it
Gateway1560s for run/datapoint, 120s elsewhere, unbounded for SSE504 telling you the run was not cancelled
Load balancer1800sconnection closed with no body

The practical consequence: if your runs take minutes, use POST /v2/jobs/execute-tasks. It returns a job id immediately, and you follow the job with GET /v2/jobs/{job_id} or its event stream. The synchronous route is a convenience for short runs, not the primary path.

A 504 is never a signal to resubmit. The work is still going, so resubmitting buys you 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.

Both Harbor routes and CodeEdit draw from the same per-project bucket, keyed by the tenancy resolved from your credential (not from the headers you send). Over the limit:

SurfaceBehaviour when over quota
POST /harbor/v2/run/datapoint (sync)429 with Retry-After
POST /codeedit/v1/executions429 with Retry-After
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.