X-Gateway-Auth-Url header.
This is the same contract that the Lager CLI speaks.
The crate handles gated boxes transparently, in two modes.
CLI session reuse (zero config)
If you’ve runlager login <auth_url> on the machine, the crate picks up
that session automatically:
- Reads the CLI’s token store (
~/.lager_gateway_auth, overridable viaLAGER_GATEWAY_AUTH_FILE). - Attaches
Authorization: Bearerto every request — including debug-service traffic and UART Socket.IO handshakes. - Refreshes expired access tokens transparently.
- Learns which auth server fronts a box from the gateway’s discovery header on first contact, and retries the denied request within the same call.
LagerBox::from_env() just works:
Pinned token (CI)
On machines with no CLI login (CI runners), supply a token directly:LAGER_GATEWAY_TOKEN environment variable — no code change:
Errors
When a gateway asks for auth and no usable credential exists, calls fail withError::AuthRequired, which names the auth server to log into:
Environment variables
The full client/gateway contract (discovery header, auth server endpoints,
store schema, retry semantics) is specified in the monorepo at
docs/reference/gateway-auth-contract.md.
Credential precedence
Three sources, checked in this order. The first that yields a token wins.LagerBoxBuilder::bearer_token()- The
LAGER_GATEWAY_TOKENenvironment variable (empty or whitespace-only is ignored) - The CLI’s token store
The token store
The crate reads the same file thatlager login writes, so a developer who signed in
needs no additional setup.
Its shape:
How discovery works
A gated box answers unauthenticated traffic with a denial status and anX-Gateway-Auth-Url header naming its auth server.
- On first contact, the crate learns the box-to-auth-server mapping from that header
and records it under
boxes. - It resolves a credential, retries the request once, and thereafter attaches the token proactively. A box already known to be gated gets its header on the very first request of the next run.
- An expired access token is refreshed against
POST <auth_url>/api/auth/refresh, replaying the stored cookies. Rotated cookies from the response are merged back in.
exp claim, decoded but not verified, with a
60-second refresh margin. An opaque non-JWT token reads as already expired, so the
crate attempts a refresh. When the crate has no refresh token, it sends the original
token as-is anyway.
A plain 401 with no
X-Gateway-Auth-Url header is not treated as a gateway
denial. It surfaces as an ordinary Error::Box { status: 401, .. }, because it came
from the application rather than from the gateway.Where the token is attached
Everywhere, not just the port-9000 API:- The box HTTP API on port 9000
- The debug service on port 8765
- The UART Socket.IO handshake
- The RTT Socket.IO handshake
Denial mapping
The 401 message distinguishes the two cases:
box <host> requires sign-in when no
token was sent, and your session was rejected by box <host> when one was.

