Files
mesh-catalog/modules/claude-licence-manager
jochen af63f12129 The licence manager binds a node reporting an account it already holds
Found going live: the other nodes report the adopted account with older
logins, which are never candidates, and the first binding was only made at
adoption — so a node reporting afterwards was never bound (ADR 0206 §7).
2026-10-04 12:34:39 +02:00
..

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:

  1. A report with a refresh token it does not hold is a candidate.
  2. It asks that node's claude_code_grant, giving its public key, and receives the grant sealed to it.
  3. 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.
  4. Several nodes logged in to one account: newest login first; the rest are never exchanged.
  5. 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 ./...