Errors and limits
HTTP status codes, quota, admission, and the bounds on request input.
| Code | Meaning |
|---|---|
| 400 | Validation — 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) |
| 401 | Missing or invalid X-Api-Key / Bearer |
| 403 | Authenticated, but not permitted — a team token naming another project, or provisioning without a valid admin key |
| 413 | Submitted files exceed the CodeEdit payload limit |
| 429 | Project concurrency quota exhausted. Honour Retry-After |
| 502 | The run reached the platform and failed inside it |
| 503 | An upstream service or the credential plane is unavailable |
| 504 | A 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:
| Layer | Budget | On expiry |
|---|---|---|
| Harbor's own wait | 1500s | 504 naming the job id, so you can keep following it |
| Gateway | 1560s for run/datapoint, 120s elsewhere, unbounded for SSE | 504 telling you the run was not cancelled |
| Load balancer | 1800s | connection 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:
| Surface | Behaviour when over quota |
|---|---|
POST /harbor/v2/run/datapoint (sync) | 429 with Retry-After |
POST /codeedit/v1/executions | 429 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.