On the control node agents are to run as an account that cannot become root (hq ADR 0266). The runtime keeps running as the operator, so the token, the identity and the home-scope items go to the agent's home through a helper started as that account; the licence and the account file follow the move, what was placed in the operator's home goes once the agent holds the licence, and the guard refuses every tool of a session running as anyone else, recording to the journal. An API key is taken only from the agent's home and only when it is one, here and in the licence manager, so the tool cannot carry off the runtime's credential. Unchanged where no agent account is named.
claude-licence-manager
Holds the anthropic-licence-manager seat: every Anthropic licence the mesh has, kept alive by one
rotation source, and handed to each consumer sealed (novox/hq ADR 0183, ADR 0206, design 39).
How a licence comes to exist
Nothing is configured. Every node running claude-code reports what it holds as that module's
holdings state — the account, fingerprints and expiries, never a token. This module reads every report
when it starts and watches them:
- A report with a refresh token it does not hold is a candidate.
- It asks that node's
claude_code_grant, giving its public key, and receives the grant sealed to it. - It refreshes it. If the vendor exchanges the token, the grant is this module's — encrypted at rest with the key the vault made for it — and from then on it is the only refresher. If not, the candidate is recorded dead and nothing is adopted.
- Several nodes logged in to one account: newest login first; the rest are never exchanged.
- A node reporting that account and bound to nothing is bound to it.
Each node is then handed an access token only, so the agent there never refreshes, and a refresh token appearing on a node later can only be a person's login — which wins if it refreshes.
An API key enters through adopt, from a file on this module's node.
What each consumer holds
This module's bindings state: one key per consumer (a node's name) with the licence, its kind and a
generation that grows with every rotation and switch. claude-code watches its own key and, on a newer
generation, asks current with its public key.
The seat's verbs
licences, bindings, bind, switch, release, refresh, usage, adopt, current — through
the console as anthropic-licence-manager.<verb>. No answer carries a token.
Settings
settings.json in the state directory: cadence_minutes (240), floor_minutes (60),
failures_to_notify (3), cooldown_hours (24), refresh_warn_days (3).
Events
licence.adopted, licence.refused, licence.failing, usage.read — none carries a secret.
Code and tests
Go, one binary (cmd/claude-licence-manager): the seat's verbs and the daemon in one launched bundle,
prepare as the run-once preparation step. The sealed box is claude-code's own format, byte for byte —
the two modules carry the same seal.go — and a test opens one sealed by the TypeScript agent module the
Go one replaced, so the format is the one already on the machines.
go test ./...
# the store against a real postgres:
docker run -d --rm --name licmgr-pg -e POSTGRES_PASSWORD=t -p 15498:5432 postgres:16-alpine
MESH_TEST_POSTGRES=postgres://postgres:t@127.0.0.1:15498/postgres go test ./...