Sandbox API

Authentication

Which credential to send, what it is allowed to reach, and how requests are scoped to a project.

Authentication is deny-by-default: a request without a recognised credential gets 401, and no route under /sandbox/ serves data anonymously.

Issuing credentials is a separate job with a separate privilege — see Provisioning credentials.

export SANDBOX_GATEWAY_URL="${SANDBOX_GATEWAY_URL:-http://localhost:8780}"

Which credential you have

CredentialHeaderScopeIssued to
Team tokenAuthorization: Bearer …One tenant:projectExternal consumers
Platform API keyX-Api-Key or Authorization: Bearer …Any projectFirst-party callers
Keycloak JWTAuthorization: Bearer …Per clientService-to-service
Admin keyX-Sandbox-Admin-KeyProvisioning onlyOperators

They are not interchangeable. The distinction that matters: a team token is confined to its own namespace, which is why it is the credential to hand to a consumer. A platform API key carries no namespace and may act for any project, which is why it stays internal — never issue one externally.

# Team token
curl -H "Authorization: Bearer $TEAM_TOKEN" "$SANDBOX_GATEWAY_URL/sandbox/v1/catalog"

# Platform API key
curl -H "X-Api-Key: $SANDBOX_API_KEY" "$SANDBOX_GATEWAY_URL/sandbox/v1/catalog"

For Keycloak, the gateway must be configured with the matching client settings, and the token needs an allowed audience and client id.

Project scoping

X-Sandbox-Tenant-Id and X-Sandbox-Project-Id select the project a request is billed and rate-limited against. They are validated against your credential rather than taken at face value:

  • A team token may only name its own namespace. Naming another returns 403. Omit the headers and your own namespace is used.
  • A platform API key has no namespace of its own, so its headers are accepted as given.

Who can see a job

Authenticating gets you in; it does not get you someone else's run. A job is visible only to a request whose resolved tenancy equals the tenancy it was submitted under. That covers every job-keyed route — status, events, cancel, selective retry-errored-trials, artifact listing, archive download, verifier stdout.

A job you may not see returns 404, identically to a job that does not exist. A 403 would confirm the id is real, which is the thing being protected, so the two are deliberately indistinguishable.

CredentialCan read a job belonging to acme:proj1
Team token for acme:proj1Yes
Team token for any other namespaceNo — 404
Platform key sending X-Sandbox-Tenant-Id: acme and X-Sandbox-Project-Id: proj1Yes
Platform key sending no tenancy headersNo — 404

So a platform key keeps its privilege of acting for any tenant, but it has to say which one — it cannot reach a run by knowing the job id alone. Internal tooling that polls across tenants must send the tenancy headers for the job it is asking about.

Jobs submitted with no tenancy at all are visible only to requests that also send none, which is what keeps single-tenant deployments and the local stack working.

The last row is a change in behaviour, not just a rule. A platform key used to read any job from its id alone, so a dashboard or script that polls bare job ids will start seeing 404 for jobs that were submitted with a tenancy — including its own, if it stamps tenancy on submit but not on poll. The fix is to send the same X-Sandbox-Tenant-Id and X-Sandbox-Project-Id on the poll as on the submit.

Because the denial is deliberately identical to "no such job", a sudden run of 404s on ids you believe exist means missing tenancy headers rather than missing jobs.

You never send a model provider key

Your credential authenticates you to the platform. It is not the key the agent uses to call Anthropic, OpenAI or Google, and the platform will not read one out of your payload. The worker leases one per job, scoped to your tenant, and passes it to the agent.

Three consequences worth knowing:

  • Spend is attributable to your tenant, because the lease is per tenant rather than one shared key.
  • Revocation is a credential-plane operation, not a redeploy.
  • A model with no key provisioned is not rejected up front. The job runs and the agent fails when it tries to call the provider — only the agent knows whether it needs a key at all, and some (an oracle agent, an air-gapped one) must not have one. Look for model_credential_absent in the job logs.

Which providers map to which models, and how to provision the keys: Provisioning credentials.

What is not authenticated

  • /healthz, /readyz, /metrics — liveness and scrape endpoints
  • /docs — this documentation site
  • /openapi/sandbox-platform.yaml — the API specification

Everything under /sandbox/ needs a credential, including /sandbox/healthz. A 401 from that path is the gate working, not an outage; use /healthz for unauthenticated health checks.

Local development

The local stack ships a placeholder key (dev-local-key) and static team tokens (for example dev-token-aacme:proj-a). Static tokens bypass the credential database and never expire, so they are ignored unless the stack explicitly enables them, and they are refused outright on deployed environments.

Next