The licence manager, in Go, and claude-code's half of ADR 0206
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.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# 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 ./...
|
||||
Reference in New Issue
Block a user