claude-licence-manager holds the anthropic-licence-manager seat: it reads every node's holdings state, adopts a login it does not hold by refreshing it (newest first, once per account), keeps each grant alive under a lease, publishes what each consumer should hold as its bindings state with a generation, and answers current sealed to the consumer's key. Postgres store prepared by a run-once step; grants encrypted with the vault's key. claude-code reports what its node holds (fingerprints and account, never a token), hands its grant over only when the manager asks, watches its binding and fetches the token on a newer generation, and writes access-token-only. Its ask now reads the runtime's answer as a value and addresses seats as seats.
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 the agent module's own format, byte for
|
|
byte, and a test opens one the TypeScript sealed (and has TypeScript open one Go sealed, where `node` and
|
|
a built `claude-code` are beside it).
|
|
|
|
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 ./...
|