The module is one Go binary the runtime launches: the renderer (its instruction file held byte for byte to the TypeScript one it replaces), the credentials and identity files, the licence flow of ADR 0206 and the MCP servers in state. Keeps the TypeScript module's key files, so a node moving to it keeps its key. The npm package, its tests and its build go. Both binaries were run together under the real runtime on a test bus with postgres and a stub vendor: a login was adopted by one exchange, the node bound and handed an access token, its file left with no refresh token, and no token in either state.
56 lines
2.7 KiB
Markdown
56 lines
2.7 KiB
Markdown
# 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 ./...
|