The operator's flow: clients publish what their credentials file holds, the manager takes in a licence it does not own and rotates it from then on. The token itself cannot be published (design 32 §10, ADR 0201), so a node reports fingerprints and identity as state and hands the grant over only when the manager asks; adopting is refreshing, newest login first; bindings with a generation replace the rotated/switched events. Designs 36 and 39 and to-be 40 amended; a pointer note on ADR 0183.
191 lines
13 KiB
Markdown
191 lines
13 KiB
Markdown
---
|
|
layer: to-be
|
|
status: designed
|
|
code: []
|
|
updated: 2026-10-04
|
|
decisions:
|
|
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
|
- 02-DECISIONS/0183-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
|
|
---
|
|
|
|
# 39 — 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 0183](../../02-DECISIONS/0183-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
|
|
|
|
*Amended 2026-10-04 by [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)*, replacing the manager's visits: **what each consumer
|
|
should hold is the manager's state, and the token is fetched when it changes.**
|
|
|
|
- **The manager keeps a `bindings` state**, one key per consumer: the licence, its kind, and a
|
|
**generation** that increases with every rotation and every switch. Nothing in it is secret.
|
|
- **The agent module on each node watches its own key.** When the generation is newer than the one it
|
|
applied, it asks the seat's `current` verb, sending its public key, and is answered with the token
|
|
sealed to it — request/reply, never an event. A node that was away reads its key when it is back and
|
|
asks once; a manager that is down leaves every node on its last token, which lives hours.
|
|
- **On a switch** the agent applies the new licence's token without comparing expiries, because across
|
|
two licences the numbers are unrelated; within one licence it applies only a newer grant.
|
|
- **No event announces a rotation or a switch.** What they announced is the state itself, and a node
|
|
needs the latest, not the history. What the manager still emits names an outcome and carries no token.
|
|
|
|
A consumer that never asks is visible: its own report (§6) names the licence and generation it holds,
|
|
and a node behind its binding is drift the manager reports.
|
|
|
|
## 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 |
|
|
|
|
**One agent directory per machine, shared by every interactive session**, so a node's binding is the
|
|
licence of all its sessions at once. A worker is a consumer of its own because it runs from a home of its
|
|
own, with its own agent directory and credentials file, which the agent module on that node writes for
|
|
it as it writes the operator's — the predecessor ran its agents exactly so.
|
|
|
|
**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
|
|
|
|
*Amended 2026-10-04 by [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)*: a licence is an account, learned from what the nodes
|
|
report, and adopted by refreshing it.
|
|
|
|
- **Every node reports what it holds**, as the agent module's `holdings` state, one key per node: the
|
|
account's identity read from the agent's own state file, the kind, the refresh token's fingerprint and
|
|
whether one is present, the access token's fingerprint and expiry, the licence and generation it was
|
|
last handed, when the credentials file last changed. Written when the module starts — a node already
|
|
logged in reports at once — and on every change. Never a token.
|
|
- **The manager reads every report at start and watches them.** A report with a refresh token whose
|
|
fingerprint the manager does not hold is a candidate: a new licence for an account it has none for, a
|
|
login made since for one it has. A manager launched for the first time holds no licence and takes every
|
|
report as a candidate.
|
|
- **The secret is asked for, never published.** For a candidate the manager calls that node's agent
|
|
module, giving its own public key, and is answered with the grant sealed to it.
|
|
- **Adopting is refreshing.** The manager exchanges the candidate's refresh token under its lease for
|
|
that account; success makes the returned grant the licence's and the manager its only rotation source;
|
|
failure records the candidate dead and adopts nothing. Candidates for one account are tried newest login
|
|
first, and the first that refreshes ends the search — the others are never exchanged.
|
|
- **The latest login wins.** A bound node is handed an access token only and its file holds no refresh
|
|
token, so a refresh token appearing there later is a person's login; its report makes it a candidate,
|
|
and if it refreshes it replaces the licence's grant.
|
|
- **A first binding follows the login**: a node with no binding whose report names the adopted account
|
|
is bound to it. Every later change is `bind`, `switch` or `release`.
|
|
- **The identity guard** files a grant under the identity the node read; where the vendor's refresh
|
|
answer names the account too, a mismatch is refused and notified. Which source decided is audited.
|
|
- **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.adopted`, `licence.failing`, `licence.refused`, `usage.read` —
|
|
the audit logger records them all. *2026-10-04 (ADR 0206):* `licence.rotated` and `licence.switched` are
|
|
gone; a rotation or a switch is a new generation in the `bindings` state.
|
|
|
|
**State**: `bindings`, which it keeps; the agent module's `holdings`, which it reads.
|
|
|
|
**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`, and `current` (a consumer's token, sealed to the key the
|
|
consumer sends — ADR 0206). The manager asks a node for a candidate grant by the agent module's own tool.
|
|
|
|
## 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 0183, 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 0183, to-be 32 §10 |
|
|
| a worker bound to a dead licence is refused, never answered with another licence's token | ADR 0183, the fallbacks |
|
|
| a grant offered with a mismatching identity is refused and one notification emitted | ADR 0183, 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 0183](../../02-DECISIONS/0183-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
|