Files
hq/03-DESIGN/01-to-be/37-the-anthropic-licence-manager.md
T
jochen 479b8fe72d Revised: the Anthropic licence manager is a module holding a seat, and the agent's configuration lives in its managed directory
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.
2026-10-02 16:54:48 +02:00

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