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_URLand 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.