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.
159 lines
12 KiB
Markdown
159 lines
12 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
|
|
---
|
|
|
|
# 178. 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.
|
|
- **Not decided here:** an automated switch on exhaustion; a second concurrent session under another
|
|
licence on one machine; 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 |
|
|
|
|
## 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 37](../03-DESIGN/01-to-be/37-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
|