Sandbox API

Authentication

Which credential to send, what it can reach, and how requests are scoped.

Every /sandbox/* route (except health) requires a recognised credential. No anonymous data.

Issuing credentials is a separate privilege: Provisioning credentials.

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

Which credential you have

CredentialHeaderScopeIssued to
Platform API keyX-Api-Key or Authorization: Bearer …Any project (via tenancy headers)First-party callers
Keycloak user tokenAuthorization: Bearer …Assigned tenant/project rolesPeople signed in through CTP or another approved application
Keycloak service tokenAuthorization: Bearer …Service-account role groupsApproved service-to-service callers
Admin keyX-Sandbox-Admin-KeyCompute-quota break-glass onlySandbox operators

They are not interchangeable. Prefer Keycloak for CTP/canvas. Keep platform API keys internal — they may act for any project when tenancy headers are set.

# Platform API key
curl -H "X-Api-Key: $SANDBOX_API_KEY" \
  -H "X-Sandbox-Tenant-Id: acme" \
  -H "X-Sandbox-Project-Id: proj1" \
  "$SANDBOX_GATEWAY_URL/sandbox/v1/catalog"

Choose your integration path

CallerHow it authenticatesWhat must be configured
CTP userSign in to CTP; CTP obtains a Keycloak user tokenSandbox role plus structural tenant/project group
CTP backendExisting ctp-app-{env} client credentialsService-account role/group and Sandbox audience
Another Keycloak application or serviceIts existing Keycloak client; do not create a sandbox-{consumer}-{env} clientApproved azp, Sandbox audience, role, and structural group
No Keycloak integrationInternal platform API key + tenancy headersOperator-issued API key (not LLM BYOK)

Sandbox migration to Keycloak does not require every consumer to implement a Keycloak login immediately. Platform API keys remain valid where the target environment enables them. They authenticate normal Catalog, Harbor, Agent, CodeEdit, and Sandboxes calls; they do not receive Keycloak's fine-grained role decisions.

Using Keycloak through CTP

sandbox-dev, sandbox-preprod, and sandbox-prod are resource-server audiences, not login applications. Their interactive login, password grant, and direct access grant are disabled. A person does not log in to a Sandbox client and must never be given a client secret.

A CTP user signs in through the existing CTP application. The resulting access token can call Sandbox directly, or CTP can forward it, when all of these are true:

  • aud contains the matching sandbox-{env} audience;
  • azp is an approved caller such as ctp-app-{env};
  • resource_access.sandbox-{env}.roles contains the Sandbox role; and
  • groups contains the full structural tenant/project role path.
curl -sS \
  -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" \
  -H "X-Sandbox-Tenant-Id: acme" \
  -H "X-Sandbox-Project-Id: proj1" \
  "$SANDBOX_GATEWAY_URL/sandbox/v1/catalog"

Headers select within the token's structural authority. Changing proj1 to a project absent from the token's groups returns 403; headers never grant access.

CTP backend jobs use the existing ctp-app-{env} client rather than a new sandbox-ctp-{env} client. The client-credentials token is subject to the same audience, role, structural-group, and ownership rules as a user token.

Using another Keycloak client

Sandbox is not CTP-only. An owning team can onboard an existing Keycloak client by requesting these Identity/Sandbox changes:

  1. Add its client ID to SERVICE_KC_ALLOWED_CLIENT_IDS for the target environment.
  2. Ensure its tokens contain the sandbox-{env} audience.
  3. Assign one of org_admin, project_admin, writer, or viewer on the sandbox-{env} client.
  4. Assign the same authority through an exact Sandbox structural group.

For a service account, assign the role and group to the service-account user, then use that service's existing client_credentials flow. Only an explicitly approved realm platform_admin is global.

Using Sandbox without Keycloak

External or transitional consumers may use a platform API key (X-Api-Key) with explicit X-Sandbox-Tenant-Id / X-Sandbox-Project-Id headers. Prefer Keycloak for CTP/canvas. When KeyHive is enabled, optional team tokens may authenticate; when SANDBOX_LLM_EGRESS=gateway, prefer Keycloak / platform API keys (ADR 0020).

Platform API keys and Keycloak tokens authenticate Catalog, Harbor, Agent, CodeEdit, and Sandboxes. LLM BYOK is provisioned on Sandbox Admin credential-plane when KeyHive is on, or on LLM Gateway when egress is gateway. See Provisioning credentials.

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 platform API key has no namespace of its own, so its headers are accepted as given.
  • A Keycloak token may select only a tenant/project present in its exact structural role groups. Its role must also permit the requested resource/scope.

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, 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
Platform key sending X-Sandbox-Tenant-Id: acme and X-Sandbox-Project-Id: proj1Yes
Platform key sending no tenancy headersNo — 404
Keycloak viewer/writer assigned to acme:proj1Yes
Keycloak role assigned only to another tenant/projectNo — 403 while resolving scope, or 404 for a scoped record lookup

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.

Migration note: a platform key used to read any job from its id alone. A dashboard that polls bare job ids will see 404 for jobs submitted with a tenancy — including its own, if it stamps tenancy on submit but not on poll. Send the same X-Sandbox-Tenant-Id and X-Sandbox-Project-Id on the poll as on the submit. Because the denial looks like "no such job", a sudden run of 404s on ids you believe exist usually means missing tenancy headers.

You never send a model provider key

Your Sandbox credential authenticates you to the compute platform. It is not the Anthropic/OpenAI/Google key the agent uses, and Sandbox will not read one out of your payload.

Workers obtain model credentials from SANDBOX_LLM_EGRESS:

  • gateway — LLM Gateway (Keycloak M2M). Sandbox /admin/credential-plane/* returns HTTP 410. Configure BYOK on LLM Gateway.
  • direct (KeyHive) — lease from KeyHive; provision via Sandbox Admin credential-plane (admin key).

How to set compute quotas vs BYOK: 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 API key (dev-local-key). Default compose uses SANDBOX_LLM_EGRESS=gateway. For KeyHive locally, enable the keyhive compose profile and set the KeyHive triad (see SECRETS). For Gateway mode, configure LLM Gateway M2M when exercising model runs.

Next