Authentication

Every request to cella.latere.ai carries an Authorization: Bearer <jwt> header. The token is issued by auth.latere.ai, signed with RS256, and validated locally by sandboxd against the auth service's JWKS — no per-request round-trip.

Token shapes

There are three principal types, all produced by the same JWKS:

Principal Issuance flow Typical caller
user OAuth2 authorization-code (browser) or device-code (terminal, RFC 8628). Dashboard, latere CLI, an MCP client.
service OAuth2 client-credentials. Server-side agents, the Wallfacer trust plane.
agent RFC 8693 token exchange. An AI agent acting on behalf of a delegator.

The response from /token is a short-lived access token (15 min default) plus an optional refresh token. Refresh handling is the CLI's responsibility — sandboxd only validates the access token.

Device-code flow (RFC 8628)

For terminals where a browser redirect is awkward (CI runners, headless boxes, MCP hosts), the CLI uses the device-code grant:

POST https://auth.latere.ai/device/code
Content-Type: application/x-www-form-urlencoded

client_id=latere-cli&scope=openid+email+profile+read:sandbox+write:sandbox+exec:sandbox
{
  "device_code":               "T4Sx...long opaque...",
  "user_code":                 "ABCD-EFGH",
  "verification_uri":          "https://auth.latere.ai/device",
  "verification_uri_complete": "https://auth.latere.ai/device?user_code=ABCD-EFGH",
  "expires_in":                600,
  "interval":                  5
}

The CLI prints the URL + code; the user opens it in any browser, signs in if needed, and approves. The CLI polls /token every interval seconds with grant_type=urn:ietf:params:oauth:grant-type:device_code. On approval the response is a normal access_token envelope; while pending, the response is { "error": "authorization_pending" }. slow_down, expired_token, and access_denied follow the RFC verbatim.

Scopes

sandboxd recognises the following scopes; requesting more than you need is the right default:

Scope Grants
read:sandbox List, inspect, read logs.
write:sandbox Create, rename, start, stop, delete, file upload.
exec:sandbox POST /commands for synchronous and background runs.
attach:sandbox Open a PTY via the /sessions/{sid} WebSocket.
admin:sandbox Cross-tenant operations on /admin/*.

exec:sandbox does not imply attach:sandbox. The split lets MCP clients run background jobs without ever holding an interactive shell — and admins keep attach off by default for the same reason.

The client_id claim

Every sandboxd-bound token carries the OAuth client_id of the originating consumer. sandboxd uses it to resolve a per-OAuth-client policy row (sandbox_client_config) covering:

  • The credential-broker sidecar image (or none).
  • The audience and scopes of the Pod-scoped JWT minted for that sidecar.
  • Environment variables injected into the user container — typically ANTHROPIC_BASE_URL and friends pointing at the sidecar.

Two clients hitting the same sandboxd therefore get topologically-different Pods. A client without a row gets a no-sidecar Pod and brings its own credentials.

Owner scoping

All resources are scoped to the JWT's sub (the principal id). Listing, inspection, and deletion are silently filtered to the caller's owner; cross-owner reads return 404. Superadmin tokens bypass the filter.