mesh/delivery merged: source.main moved to novox/mesh-catalog@6d000647de2d
mesh/check being carried to cff1005422c1 on 6d000647de2d | from f9dcbc295723
The one bundle that did everything becomes the manager bundle, which runs long and answers the seat's ten verbs and the three acting tools, and a tools bundle of its own that answers the four reading tools and writes nothing, so they answer while the manager fails to load or stops. Both import the module's one package for the layouts of the kept data and the log entries, the key form, the entries' names and the settings with their validity; the settings file and the account with its directory stay as they are. Builds: design 39 parts §1, §2, §3, §4, §5, §6, §7, §8
116 lines
7.9 KiB
Markdown
116 lines
7.9 KiB
Markdown
# claude-licence-manager
|
|
|
|
Holds the `anthropic-licence-manager` seat: every Anthropic licence the mesh has, kept alive by one
|
|
rotation source, its consumers moved off a licence near its limit by the manager itself (novox/hq ADR 0183,
|
|
ADR 0206, design 39 and its milestone bus-and-switch). Every grant and access token lives in the vault; this module
|
|
keeps none.
|
|
|
|
It is one manifest of four parts (design 39 parts §1): the manager bundle (`manager`, `cmd/manager`), which runs long
|
|
(the conditions judged at its start, the usage timer, the switch rule) and answers the seat's verbs and every act; the
|
|
tools bundle (`tools`, `cmd/tools`), which answers the reading tools and writes nothing, so they answer while the
|
|
manager fails; the settings file; and the module's account with its directory. Both bundles run as an account of its
|
|
own (`runs-as claude-licence-manager`, design 39 bus-and-switch C1), in a runtime of its own, never in the machine's
|
|
tool runner, so every state, log and secret below is issued to its own account. Its
|
|
account is issued what its manifest names: the asks under `invokes` (the vault, the operator channel, mesh-config and
|
|
a node's `claude_code_grant`), the events under `emits` and `consumes`.
|
|
|
|
## How a licence comes to exist
|
|
|
|
Nothing is configured. Every node running `claude-code` says what it holds as the event
|
|
`claude-code.holdings-changed` (the account, fingerprints and expiries, never a token), at its start, on every change,
|
|
and again when this module says `holdings.asked` (at every start, after its conditions are judged, and on `licences_resync`):
|
|
|
|
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**, under the account's lease. If the vendor exchanges the token, the licence is new, named by its
|
|
account's address and kept under that name's key (`@` as `_at_`, any other character outside letters, digits, `-`
|
|
and `.` as `_`); its grant and access token go to the vault (`licence-put`), and from then on this module is the
|
|
only refresher. If not, the candidate is recorded dead and nothing is adopted. An adoption whose key another licence
|
|
has is refused.
|
|
4. Several nodes logged in to one account: newest login first; the rest are never exchanged.
|
|
5. The node it came from is bound to it; a node reporting that account and bound to nothing is bound to it.
|
|
|
|
From its next reading a new subscription has a room and is a target of the switch. An API key enters through `adopt`,
|
|
from a file on this module's node or sealed by a node's `claude_code_add_api_key`; it is never a target.
|
|
|
|
## What it keeps on the bus
|
|
|
|
Each state asked for in `requires` by its template (ADR 0595), and the two logs declared in the `logs` section, the
|
|
form the runtime issues today (design 39 bus-and-switch C3); each read by `licences_kept`:
|
|
|
|
| name | template | what |
|
|
|---|---|---|
|
|
| `licences` | state, history 1 | one key per licence: account, address, organisation, expiries, failures, adoption, the vault's generation, the latest reading; never a token |
|
|
| `bindings` | state, history 8 | one key per consumer (a node's name): licence, kind, generation, since, why and by |
|
|
| `leases` | state, a lifetime per key | `licence.<key>`, `account.<id>`: one refresh or adoption at a time, two minutes |
|
|
| `offered` | state, a lifetime per key | each grant a node offered and its outcome, 90 days |
|
|
| `usage` | log, 35 days, 256 MiB | every reading, one subject per licence key, with the vendor's answer whole |
|
|
| `audit` | log, 64 MiB | every act, one subject per kind (`switched`, `carried`, `settings` among them) |
|
|
| `carried` | state | `postgres`: the record of the carry-over from the store database, which ran once on 2026-11-12; nothing writes it again |
|
|
| `conditions` | state | each condition said raised, and since when |
|
|
|
|
The carry-over from the store database is done, and its code has left the module with its requirement of the
|
|
database and of the secret `grant-key` (design 39 parts §9, §10); at its start the manager waits for nothing and
|
|
clears a `carry-over-failed` it finds kept.
|
|
|
|
## Switching on usage
|
|
|
|
Every `usage_minutes` the manager reads each subscription's usage with its access token from the vault: the session,
|
|
weekly and sonnet windows, each with its reset time. A licence's room is 100 less its weekly percentage alone (the session
|
|
window takes no part in room, the switch, the warning or the conditions: the vendor retired the session limit), known only when it was read within `usage_stale_minutes` and its refresh is healthy. When a reading puts
|
|
a licence with consumers at or above `switch_at_pct` in its weekly window, every consumer bound to it moves, in one act, to the licence
|
|
with the most room whose weekly window is below `room_below_pct`: one `binding.changed` per consumer, one
|
|
`licence.switched`, and one `operator-channel.tell`. A consumer is never moved back by the rule.
|
|
|
|
Its conditions are said on the operator channel's condition subject, the router deciding where they go:
|
|
`licence.<key>.no-room` (urgent; no licence has room), `licence.<key>.near-limit` (at `warn_at_pct`, once per
|
|
crossing), `licence.<key>.usage-unread` and `settings.<setting>.invalid`.
|
|
|
|
## The seat's verbs
|
|
|
|
`licences`, `bindings` (with `consumer` for one), `bind`, `switch`, `release`, `refresh`, `usage` (`licence`,
|
|
`since`, `limit` up to 2000, `raw`), `adopt`, `public-key`, `current` (the licence, its key, kind, generation and
|
|
health, never the token) — through the console as `anthropic-licence-manager.<verb>`. No answer carries a token.
|
|
|
|
## Its tools
|
|
|
|
Read by the tools bundle: `licences_switches` (every switch and why), `licences_settings` (each value, its default
|
|
and where it was set), `licences_kept` (every entry it keeps, how full against its cap) and `licences_conditions`.
|
|
Acting, by the manager bundle: `licences_read_usage` (read now, `dry`), `licences_switch_now` (apply the rule now, even
|
|
with `switch_auto` off) and `licences_resync` (say every binding, holdings.asked and each condition again, and hand
|
|
each access token to the vault again).
|
|
|
|
## Settings
|
|
|
|
Through mesh-config, each `trusted`, read again at every reading: `cadence_minutes` (240), `floor_minutes` (60),
|
|
`failures_to_notify` (3), `cooldown_hours` (24), `refresh_warn_days` (3), `switch_auto` (on), `switch_at_pct` (99,
|
|
1 to 100), `room_below_pct` (90, 1 to 100), `warn_at_pct` (90, 1 to 100), `usage_minutes` (5, 1 to 60),
|
|
`usage_stale_minutes` (15, 5 to 180). A value outside its bound keeps its default and raises its condition.
|
|
|
|
## Events
|
|
|
|
`licence.adopted`, `licence.refused`, `licence.switched`, `licence.failing`, `usage.read`, `binding.changed`,
|
|
`holdings.asked` — none carries a secret. It consumes `claude-code.holdings-changed`.
|
|
|
|
## Code and tests
|
|
|
|
Go, one Go module of two binaries: `cmd/manager` (`claude-licence-manager`: the seat's verbs, the acting tools and the
|
|
daemon) and `cmd/tools` (`claude-licence-manager-tools`: the reading tools), sharing the package `internal/licences`
|
|
(the layouts of the kept data and the log entries, the key form, the entries' names and the settings). 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 whole module on a real bus:
|
|
go test -tags integration ./...
|
|
|
|
The whole tests (`cmd/manager/whole_integration_test.go`) start the built binaries as their runtime does, its kept data in a real
|
|
nats-server. The vault, the agent modules, the
|
|
operator channel and the vendor are played beside it; a condition is read through the operator channel's client.
|
|
|
|
## Files it does not know
|
|
|
|
Each directory or archive this module declares says how it treats a file it did not write (ADR 0422, ADR 0416).
|
|
|
|
- `${dir:state}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
|