Files
mesh-catalog/modules/claude-licence-manager
jochen c8fb0016f2
mesh/merge-gate pass: builds claude-code, claude-licence-manager, restic → ace, g14, novox, shanks; no bus step; every machine composes with the change as …
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery stopped: the pull request closed unmerged
Test that a sealed grant hides its words, not two letters
The check looked for "rt" in a random base64 ciphertext, which holds it
about one time in seventy, and failed mesh-catalog #141's repo check by
chance.
2026-10-09 10:14:18 +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 ./...