The operator's directions, taken during review: the host is module-agnostic and never writes a vendor's file or anything under a home; the controller has no part; a real licence manager doles out the correct licence in every situation; it talks to the agent module on every node over the bus; the seat is named for the vendor, since the agent is coupled to an Anthropic grant, not to "a model". - ADR 0178 rewritten: `claude-licence-manager` holds the mesh seat `anthropic-licence-manager`, owns the licences, grants (encrypted with a key the vault made for it), bindings per touchpoint, usage and audit; one rotation source under a lease; tokens travel module to module sealed to each node's module key on request/reply, never as an event; the agent module alone writes what the agent reads; the exception to ADR 0113 stated and bounded. Dated mechanism notes on ADR 0050 and 0113. - To-be 37 (new): the manager — its store, the two licence kinds, keeping a grant alive, the hand-over, who gets which licence with the predecessor's fallbacks, adoption with the identity guard, verbs. - To-be 36 rewritten: the mesh's part of the agent's configuration lives in the agent's machine-wide managed directory (settings, tool servers, instruction file), owned whole by the module and written by its code; the home is found except the credentials file; the API-key licence through the key-helper writes nothing under the home; the console as a node-scoped provision; MCP servers as settings with an `mcp_configure` tool; the six predecessor files removed by the operator. - Records 0169–0171 renumbered to 0176–0178 after main gained 0169–0175 today.
166 lines
10 KiB
Markdown
166 lines
10 KiB
Markdown
---
|
|
layer: to-be
|
|
status: designed
|
|
code: []
|
|
updated: 2026-10-02
|
|
decisions:
|
|
- 02-DECISIONS/0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
|
- 02-DECISIONS/0024-model-access-is-a-provision.md
|
|
- 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
|
|
- 02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
|
|
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
|
|
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
|
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
|
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
|
---
|
|
|
|
# 37 — The Anthropic licence manager
|
|
|
|
**One module knows every Anthropic licence the mesh has, keeps each alive, decides which consumer gets
|
|
which, and hands every node's agent its token over the bus.**
|
|
[ADR 0178](../../02-DECISIONS/0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
|
decides it; this is the shape. It is the successor of the predecessor's manager module, built from what
|
|
that module learned the hard way, and the counterpart of [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md),
|
|
which is the consumer on every node.
|
|
|
|
## 1. What it is
|
|
|
|
A module, `claude-licence-manager`, holding the mesh-scoped seat **`anthropic-licence-manager`**. One
|
|
holder, on the node the operator assigns it to — the control node is the natural one, and nothing in the
|
|
definition says so. It requires a database for its own store and the bus; it claims the seat; it serves
|
|
the seat's verbs. It has no port, no route, no file under anyone's home.
|
|
|
|
Its store holds four things:
|
|
|
|
| table | holds |
|
|
|---|---|
|
|
| **licences** | name, kind (`subscription` or `api-key`), the account's identity (id, address, organisation) once adopted, the grant encrypted at rest, when the access token expires, when the refresh token expires, consecutive failures, the refresh lease, when a person was last notified |
|
|
| **bindings** | one row per consumer: kind (`node-agent`, `node-session`, `worker`), its key (the node, or the node and the worker), the licence, or *inherit* |
|
|
| **usage** | the vendor's readings per licence per period, raw beside normalised ([ADR 0054](../../02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md)) |
|
|
| **audit** | every switch, adoption, refusal and drift, with who asked |
|
|
|
|
**The grants are encrypted with a key the vault made for the manager** — its one `secret` requirement.
|
|
The vault keeps that key; the manager keeps the grants. That is ADR 0050's carve-out, one module, one
|
|
node, the long-lived grants only.
|
|
|
|
## 2. The licences it manages today
|
|
|
|
Two subscription accounts and one API key. They differ in kind and the manager treats them so:
|
|
|
|
| kind | what the grant is | refresh | what a node is handed | how the agent uses it |
|
|
|---|---|---|---|---|
|
|
| `subscription` | an OAuth grant: an access token that lives hours and a refresh token that lives weeks | the manager rotates it, alone | the access token only | written into the agent's credentials file by the agent module, as the operator |
|
|
| `api-key` | a key the operator obtained from the vendor | none; a new key is a new adoption | the key | served to the agent through its key-helper setting; nothing is written under the home |
|
|
|
|
## 3. Keeping a grant alive
|
|
|
|
Carried from the predecessor, where each rule was earned by an incident:
|
|
|
|
- **One rotation source.** Only this module calls the vendor's token endpoint. An OAuth refresh is
|
|
presumed to rotate the refresh token, so a second refresher presenting the old one would kill the
|
|
grant; whether that presumption holds is to be measured in the lab, and the design is safe either way.
|
|
- **A lease per licence**, taken in the store before the row is read. A duplicate run sees the token its
|
|
predecessor just wrote, finds hours of life on it, and does nothing.
|
|
- **An expiry floor and a cadence.** Within an hour of expiry a refresh must happen; otherwise a grant is
|
|
rotated once it is older than a declared setting, so a node that misses one rotation still holds hours
|
|
of life and a broken refresh surfaces in minutes rather than the next morning.
|
|
- **Failure is counted and escalated once.** Consecutive failures are recorded; past a threshold a
|
|
notification is emitted, and at most once a day while it stays broken — the predecessor sent one alarm
|
|
411 times in 35 hours and the incident went unnoticed inside its own alarm.
|
|
- **A refresh token's own expiry is warned about three days ahead**, because the only remedy is a person
|
|
logging in again.
|
|
- **The vendor's reason is logged**, never only the status code: a malformed request and a revoked grant
|
|
both answer 400, and the predecessor built three concurrency fixes for a bug that was a wrong client id.
|
|
|
|
## 4. Handing a token to a node
|
|
|
|
Every node that runs the agent module registers that module's public key with the seat when it first
|
|
runs. From then on:
|
|
|
|
- **On rotation**, the manager calls `claude-code.apply@<node>` on every node bound to the rotated
|
|
licence, with the new token sealed to that node's module key. The module answers *applied*, or
|
|
*refused* and why, and the manager records it.
|
|
- **On a switch**, the same call with the other licence's token, and the binding is the authority: the
|
|
module applies a bind without comparing expiries, because across two licences the numbers are
|
|
unrelated.
|
|
- **On a pull** — the module starting, or finding its token near expiry — the module calls the seat's
|
|
`current` verb for its binding and is answered sealed the same way.
|
|
- **Never as an event.** What the manager emits names the licence and the outcome and carries no token.
|
|
|
|
A node whose module has not registered a key cannot be handed a token, and the manager says so by name
|
|
rather than falling silent. A node whose module refuses — a wrong identity, a stale grant within one
|
|
lineage — is recorded as drift and reported.
|
|
|
|
## 5. Who gets which licence
|
|
|
|
Three consumer kinds, the predecessor's touchpoints with their fallbacks:
|
|
|
|
| consumer | bound by | falls back to | if the bound licence cannot be served |
|
|
|---|---|---|---|
|
|
| **the node's interactive agent** | the node | nothing: an unbound node has no licence and the agent says so | keeps the last token, which expires within hours; a notification is emitted |
|
|
| **the mesh's session on a node** | the node, for that session | the node's agent licence | refused |
|
|
| **a worker** | the worker | the node's session licence, then the node's | refused: a worker never borrows a person's account |
|
|
|
|
**Binding is a person's act through the seat's verbs**, listed by the console: `bind`, `switch`,
|
|
`release`. **Exhaustion is observed, not acted on**: usage is read every few minutes, a crossing of a
|
|
declared threshold in the five-hour window is notified once per crossing, and moving a consumer is the
|
|
operator's call. Switching remains a reaction, not a declaration
|
|
([ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md)), and an automated policy — move to the
|
|
least-used licence, stay off a dying one — is designed later if wanted, on the readings this module
|
|
already keeps.
|
|
|
|
## 6. Adopting a grant
|
|
|
|
A licence enters the mesh one of two ways, and the token never passes through a prompt, a terminal or an
|
|
argument:
|
|
|
|
- **From a node's login.** A person logs in on a node, as they always have. The agent module there reads
|
|
the account's identity from the agent's own state file, and offers the full grant to the seat sealed
|
|
to the manager's key. The manager adopts it into the licence the node is bound to **only if the
|
|
identity matches** that licence's recorded account; a licence not yet identified is identified by its
|
|
first adoption; a mismatch is refused and notified, because the predecessor once filed one account's
|
|
grant into another's row this way.
|
|
- **An API key** is delivered to the manager by the operator through the seat's `adopt` verb from a file
|
|
on the manager's node, never as an argument.
|
|
|
|
## 7. What it emits and serves
|
|
|
|
**Events**, no secret in any: `licence.rotated`, `licence.switched`, `licence.adopted`,
|
|
`licence.failing`, `licence.refused`, `usage.read` — the audit logger records them all.
|
|
|
|
**The seat's verbs**, the contract every future holder must serve: `licences` (each with kind,
|
|
identity, expiry, failures, who is bound), `bindings`, `bind`, `switch`, `release`, `refresh` (now, one
|
|
or all), `usage` (current and history), `adopt`, `register` (a node's module key), `current` (a
|
|
consumer's token, sealed, asked by the consumer's module).
|
|
|
|
## 8. Settings
|
|
|
|
The refresh cadence; the usage threshold; the notification cooldown. Each declared with a default, so
|
|
one definition serves and one mesh may differ.
|
|
|
|
## How it is checked
|
|
|
|
| Check | Defends |
|
|
|---|---|
|
|
| two refresh runs started together rotate one grant once; the second does nothing and says so | ADR 0178, one rotation source |
|
|
| every event the manager emits is free of any token; the hand-over opens only with the receiving module's key | ADR 0178, to-be 32 §10 |
|
|
| a worker bound to a dead licence is refused, never answered with another licence's token | ADR 0178, the fallbacks |
|
|
| a grant offered with a mismatching identity is refused and one notification emitted | ADR 0178, attribution |
|
|
| a failing licence notifies once, and once a day after, not once per tick | §3 |
|
|
| the console lists the seat's verbs and `switch` changes a workstation's token end to end | ADR 0132, the exit of the build |
|
|
|
|
## What this does not settle
|
|
|
|
- An automated switch on exhaustion (§5).
|
|
- Whether an OAuth refresh token is single-use; the lab measures it, and §3 holds either way.
|
|
- How the mesh's own session and a worker read their token on a node once those exist
|
|
([to-be 15](15-the-agent-session.md), [ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)):
|
|
the agent module on that node is their local source, and the reading is theirs to design.
|
|
|
|
## References
|
|
|
|
- [ADR 0178](../../02-DECISIONS/0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the decision
|
|
- [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md) — the consumer on every node
|
|
- [14 — Model access](14-model-access.md) — the vendor-blind provision this sits beside
|
|
- the predecessor's `claude-licences` module: the lease, the floor, the cadence, the cooldown, the identity guard — read 2026-10-02
|