Files
hq/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
T
jochen 8a5dbaeb98 To-be 40 revised for the tools refactor; the manager starts every exchange (ADR 0183 dated note, designs 36 and 39)
Design 38's WP1-WP4b ran: the node's tool runtime is live on all four machines as the operator
account, tools are bundles given only their declared words, and a bundle has no bus credential.
So the wait on design 38 WP3 is over, the agent module calls nothing and the manager starts every
exchange (key, hand-over, waiting login, reconcile), and the manager's daemon now waits on WP4c's
record instead. Accounts are stated on all four, sudo -n works for each, the agent is installed on
all four; the plan's WP0 shrinks and WP2 gets a configuration-only live proof before any licence.
2026-10-03 16:01:37 +02:00

177 lines
14 KiB
Markdown

---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0024-model-access-is-a-provision.md
---
# 183. The Anthropic licence manager is a module holding a seat; it hands each node's agent its token over the bus, sealed; the controller and the host have no part
## Context
**The operator's stance, set on 2026-10-02 and sharpened during the day.** The controller has no part in
the agent module. The host is module-agnostic: it knows no vendor, no agent, no path under a home. The
agent module owns its own files. And there must be a *real* licence manager — a module that doles out
the correct licence in every situation the mesh has: two subscription accounts and one API key today,
used by a person's interactive agent on each workstation, by the mesh's own sessions, and by workers.
**What the predecessor built, read from its code the same day.** Two modules, split after an incident.
A *manager* on exactly one node held every account's full OAuth grant encrypted, rotated each grant
under a per-licence lease on a cadence and an expiry floor, published each rotation over its bus with
the tokens encrypted, collected the vendor's usage figures per licence, and alerted once a day on
repeated failure or on a refresh token within three days of its own expiry. A *consumer* on every node
was the single writer of the agent's credentials file: it applied a published rotation, stripped the
refresh token so a node could never rotate, pulled when stale, refused a stale grant by comparing
expiries within one lineage, and mirrored a local login back to the manager only after checking the
account's identity against the licence's record — because an unchecked mirror had once written one
account's grant into another's row and published it mesh-wide. Three **touchpoints** with fallbacks: the
node's interactive agent; the mesh's own sessions on the node, falling back to the node's licence; a
worker's own account, falling back to the node's, and refusing to spawn when assigned a licence that
could not be served. The split exists because four nodes refreshing one grant destroyed it: an OAuth
refresh rotates the refresh token, and the predecessor's own code records both that a reused token
killed a licence and that a malformed client id was once misdiagnosed as the same fault. **Whether a
refresh token is single-use is not documented by the vendor**; the predecessor treated it as so, and
this record keeps one rotation source for that reason while leaving the fact to be measured.
**What the mesh has.** [ADR 0050](0050-model-access-is-vendor-agnostic.md) put a per-vendor adapter
inside the controller's licences context, with the carve-out that the manager node holds the refresh
token readably; the catalogue has a manager and a consumer module built on it, assigned to nothing. The
controller's licence commands are not seat verbs and cannot be asked for through the console
([to-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)). `model-access` is a vendor-blind
provision ([ADR 0024](0024-model-access-is-a-provision.md)), and the operator's judgement is that the
agent is not a vendor-blind consumer: it is coupled to an Anthropic subscription grant and nothing else,
so a name that hides the vendor misdescribes the coupling
([ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md)).
**The bus's rule for a secret** ([to-be 32 §10](../03-DESIGN/01-to-be/32-what-a-module-declares.md)): the
bus is not trusted with one; a secret travels sealed to its recipient, on core request/reply, never
through a stream that persists it.
## Considered Options
1. **Keep the lifecycle in the controller** ([ADR 0050](0050-model-access-is-vendor-agnostic.md) as
built), and make the agent module a consumer of `model-access` delivered by the host as a sealed
file. Rejected by the operator: the controller and the host would both carry a part of an
Anthropic-specific mechanism, and the agent's coupling is misnamed.
2. **The manager delivers each short-lived token through the vault**, as a backend-issued secret the
vault provides to each consumer ([ADR 0113](0113-the-vault-makes-every-secret.md)). Rejected: every
hourly rotation becomes a vault delivery, a composition and a push to every node, and the host
ends up writing a vendor's credential as a file — the module-agnostic host, carrying a vendor's
traffic.
3. **A seat-holding manager module that talks to the agent module on every node over the bus.**
Chosen.
## Decision
**The Anthropic licence manager is a module, `claude-licence-manager`, holding the mesh-scoped seat
`anthropic-licence-manager`.** The seat's contract is the licence verbs: list the licences and their
health, list the bindings, bind or switch a consumer, release one, refresh now, read usage, adopt a
grant, register a node's key, answer a consumer's current token. One holder, on a node the operator
assigns, is what makes rotation happen once ([ADR 0126](0126-a-module-declares-its-own-seats.md),
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)). The seat is named for the vendor,
because what it manages is one vendor's grants and nothing else is coupled to it. The vendor-blind
`model-access` provision stands for the consumers that do not care which vendor answers; the agent is
not among them.
**The manager owns the licences.** The records, the grants, the bindings per touchpoint, the usage
readings and the audit of every switch live in the manager's own store, not in the controller's
licences context, which keeps only what it already serves to vendor-blind consumers. The manager is the
one rotation source: it alone calls the vendor's token endpoint, under a lease per licence, on an expiry
floor and a cadence it declares as a setting.
**The long-lived grants are encrypted at rest with a key the vault made for the manager.** The vault
keeps custody of that one key as the manager's own secret ([ADR 0113](0113-the-vault-makes-every-secret.md));
the grants themselves — a refresh token per subscription account, the API key — are the manager's
rows, readable only by it. This is [ADR 0050](0050-model-access-is-vendor-agnostic.md)'s carve-out,
moved with the manager: *one module, one node, the long-lived grants only.*
**The short-lived tokens travel module to module, sealed, on request/reply.** The agent module on each
node makes a keypair of its own when it first runs — a private key made where it is used, never leaving
([ADR 0113](0113-the-vault-makes-every-secret.md)) — and registers its public half with the seat. The
manager hands a node its token by calling that node's agent module (`<module>.<tool>@<node>`,
[ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)) with the token
sealed to that key, and the module answers *applied* or *refused* and why. An agent module that starts,
or finds its token near expiry, asks the seat for its current token the same way. **A token is never
published as an event**: what the manager emits — rotated, switched, failing, usage read — names the
licence and nothing secret, and the audit logger records it. This is a second channel for a secret
beside the vault's, and it is bounded as 0050's carve-out is: this vendor, tokens that live hours, sealed
to one recipient, request/reply only.
**The agent module alone writes what the agent reads.** For a subscription licence it writes the
agent's credentials file under the operator's home, as the operator, access-token-only, atomically. For
the API-key licence it serves the key through the agent's own key-helper setting, so nothing is written
under the home at all. For the mesh's own sessions and workers on that node, it is the local source of
their token. **The host delivers the module's package and its state directory and knows nothing else**:
no path under the home, no vendor, no file shape.
**A binding is explicit, and a switch is a reaction.** Every consumer — a node's interactive agent, the
mesh's session on a node, a worker — is bound to a licence by the operator through the seat's verb, with
the predecessor's fallbacks: a session inherits its node's licence, a worker inherits its node's, and a
worker assigned a licence that cannot be served is refused rather than lent another. Exhaustion is
observed and warned about once per crossing of a declared threshold; moving a consumer to another
licence is a person's act through the seat's verb, as [ADR 0024](0024-model-access-is-a-provision.md)
says, and the declaration language grows no conditional. An automated policy is not decided here.
**A login is attributed only to the account it belongs to.** When a person logs in on a node, the
agent module reads the account's identity from the agent's own state and offers the grant to the
manager sealed to the manager's key; the manager adopts it only when the identity matches the licence
the node is bound to, and refuses with a notification otherwise.
## Consequences
- One module decides which licence every consumer gets, one module writes what each agent reads, and
neither the controller nor the host carries a word of the vendor.
- **A second sealed channel exists** beside the vault's, bounded as stated. A record that widens it to
another vendor or a longer-lived secret is a new decision, not an application of this one.
- The catalogue's `anthropic-manager` and `anthropic-consumer` modules, built on ADR 0050's placement,
are retired once the manager runs; the controller's licences context stops holding Anthropic licences.
- The console lists the seat's verbs, so a person switches a licence in a sentence, and the controller
gains no `licence` verb.
- **What got harder:** a manager that is down leaves every node on its last token until it expires;
the agent module keeps the last token and says so. And a node whose agent module has not registered
its key cannot be handed a token, which the manager reports by name.
- Every interactive session on a machine shares the node's one agent directory, and so its licence;
twenty sessions share it as one does. A consumer with a licence of its own on the same machine is a
worker running from a home of its own with its own agent directory — the worker touchpoint above, for
when workers exist ([ADR 0003](0003-agents-are-persistent-employees.md)); the predecessor ran its
agents that way.
- **Not decided here:** an automated switch on exhaustion; whether a refresh token is single-use, to be
measured in the lab.
## How it is checked
| Rule | Checked by |
|---|---|
| Only the seat's holder calls the vendor's token endpoint | a catalogue test: no module but the manager names it; the manager's refresh runs under a lease per licence, tested with two concurrent runs |
| A token crosses the bus only sealed, only on request/reply | a bus test: every message the manager publishes as an event carries no token; the hand-over is a request whose payload opens only with the receiving module's key |
| The agent module's private key never leaves the node | the per-key test of ADR 0113, extended to this module's key |
| The host writes nothing under a home and names no vendor | a catalogue test on the agent module's definition: no file resource under a home, no vendor word in anything the host applies |
| A grant is attributed only to a matching identity | a manager test: a grant whose account identity differs from the bound licence's is refused and a notification emitted |
| An unservable binding refuses rather than lends | a manager test: a worker bound to a dead licence is answered with a refusal, never another licence's token |
| A switch through the console changes the token on the node and nothing in the answer is a token | a live check on one workstation |
> **The mechanism changed — 2026-10-03, by [ADR 0192](0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md).**
> What stands: one manager holding the seat, one rotation source, a token sealed to the receiving
> module's key on request/reply and never an event, the agent module alone writing what the agent
> reads, the identity guard, the host knowing nothing. What moved: the agent module's code is now a
> tools bundle the node's runtime serves ([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
> [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)),
> and a bundle has no bus credential of its own and answers calls rather than making them (ADR 0192's
> consequences). So **the manager starts every exchange**: it asks each bound node's agent module for
> its public key, hands it a token, asks it for a login waiting to be adopted, and reconciles every node
> on a schedule — which is what "the agent module asks the seat for its current token" and "offers the
> grant to the manager" in the decision above now mean in practice. The manager's own process needs a
> bus credential to make those calls, which is the question design 38's WP4c leaves to a record.
## References
- [ADR 0024](0024-model-access-is-a-provision.md), [ADR 0050](0050-model-access-is-vendor-agnostic.md) — the licence as a named thing, the carve-out this moves with the manager
- [ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) — why the seat is named for the vendor
- [ADR 0113](0113-the-vault-makes-every-secret.md) — the vault's custody of the manager's key, and the exception stated here
- [ADR 0126](0126-a-module-declares-its-own-seats.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) — a module's seat, its verbs, a call addressed to one machine
- [to-be 32 §10](../03-DESIGN/01-to-be/32-what-a-module-declares.md) — a secret on the bus
- [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md), [to-be 39](../03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md) — the two modules
- the predecessor's `claude-licences` and `claude-code` modules, read 2026-10-02: the lease, the floor, the lineage comparison, the identity guard, the touchpoints