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.
This commit is contained in:
@@ -204,3 +204,14 @@ only, the refresh token only, the manager node only.**
|
|||||||
record extends, amended to describe the adapter generalisation.
|
record extends, amended to describe the adapter generalisation.
|
||||||
- The read-only vendor-agnostic analysis, 2026-09-05 (code workspace) — the inventory and the decisions
|
- The read-only vendor-agnostic analysis, 2026-09-05 (code workspace) — the inventory and the decisions
|
||||||
taken on the open questions this record encodes.
|
taken on the open questions this record encodes.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0178](0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md).**
|
||||||
|
> What stands: `model-access` is one vendor-blind provision for the consumers that do not care which
|
||||||
|
> vendor answers; a vendor's lifecycle is an adapter's; the carve-out that one node holds a refreshable
|
||||||
|
> grant's refresh token readably. What moved: the Anthropic adapter is no longer a part of the
|
||||||
|
> controller's licences context but a module, `claude-licence-manager`, holding the seat
|
||||||
|
> `anthropic-licence-manager`, with the grants in its own store encrypted with a key the vault made for
|
||||||
|
> it; and the agent at a terminal is not a consumer of `model-access` — it is coupled to an Anthropic
|
||||||
|
> grant and uses the seat. The consequence above that the three binding columns *become three ordinary
|
||||||
|
> consumers of `model-access`* therefore no longer describes the agent's bindings; they are the
|
||||||
|
> manager's. The static-key adapters and the vendor-blind records stay where this record put them.
|
||||||
|
|||||||
@@ -283,3 +283,13 @@ modules in the catalogue require it — so a shared secret is a requirement answ
|
|||||||
which is what this record asks for. Private keys are still made where they are used and never
|
which is what this record asks for. Private keys are still made where they are used and never
|
||||||
travel, which is the other half and was never in question.
|
travel, which is the other half and was never in question.
|
||||||
|
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0178](0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md).**
|
||||||
|
> What stands: every shared secret the mesh makes is the vault's, a private key is made where it is
|
||||||
|
> used, and a long-lived value a backend issues enters the vault's custody — here as the key the vault
|
||||||
|
> makes for the licence manager, which encrypts the vendor's grants at rest with it. What this record
|
||||||
|
> did not foresee: a credential that lives hours, issued by a vendor to the one module that holds its
|
||||||
|
> grant, and handed by that module to the agent on each node sealed to that node's module key, on
|
||||||
|
> request/reply over the bus, never through the vault and never as a file the host writes. ADR 0178
|
||||||
|
> states that as a bounded exception — one vendor, tokens that live hours, one recipient per message —
|
||||||
|
> and a second such channel is a decision of its own.
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ declared**, not inferred from what happened to be on disk:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **owned** | a file with content, or a roster fact | written whole, regenerated, removed when undeclared; a file found there with no record of the mesh making it is kept once before it is written over ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)) |
|
| **owned** | a file with content, or a roster fact | written whole, regenerated, removed when undeclared; a file found there with no record of the mesh making it is kept once before it is written over ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)) |
|
||||||
| **written into** | a file written *into* a structured document | only the keys the definition names are set, every other key is kept, and each set key is given back when undeclared (ADR 0102). The key list is the module's and is short |
|
| **written into** | a file written *into* a structured document | only the keys the definition names are set, every other key is kept, and each set key is given back when undeclared (ADR 0102). The key list is the module's and is short |
|
||||||
| **written by the module's own process** | a secret the mesh delivers to the module, and a step that writes the file from it | the file's content is never a declared file's content, because a declaration travels in the clear on the bus and the host records it; the module's process writes it, owned by the account, atomically. [ADR 0178](0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md) says how for a credential |
|
| **written by the module's own process** | nothing the host applies: the module's code writes it from what it was handed | the file's content is never a declared file's content, because a declaration travels in the clear on the bus and the host records it; the module's code writes it, owned by the account, atomically. [ADR 0178](0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) says how for a credential |
|
||||||
| **found** | nothing | never read, never rewritten, never removed. The person's memory, history, projects, local settings, their own rules and skills |
|
| **found** | nothing | never read, never rewritten, never removed. The person's memory, history, projects, local settings, their own rules and skills |
|
||||||
|
|
||||||
**A file the tool writes for itself is written into, never over.** The agent's settings file and its
|
**A file the tool writes for itself is written into, never over.** The agent's settings file and its
|
||||||
|
|||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
-142
@@ -1,142 +0,0 @@
|
|||||||
---
|
|
||||||
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 mesh binds and delivers a licence; the module alone writes the tool's credential; a switch is the binding changed, asked for through the console
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
**The operator's stance, set on 2026-10-02:** the controller has no part in the agent module. The
|
|
||||||
module owns the agent's directory under the operator's home and every related file, handles them
|
|
||||||
itself, and carries a licence-switching function as the predecessor's did.
|
|
||||||
|
|
||||||
**What the predecessor's switching actually was.** A registry of accounts held server-side; a tool,
|
|
||||||
callable from a session, that decrypted the chosen account's token on the server, refreshed it if near
|
|
||||||
expiry, and wrote the agent's credentials file on the target node — never returning the token. Beside
|
|
||||||
it, a shell helper that ran the agent with a token read from a plaintext file in the operator's own
|
|
||||||
configuration directory, one token per account, on every workstation; and an enrolment helper that
|
|
||||||
logged in once in a throwaway home and registered what came out. So the central half did the
|
|
||||||
refreshing and the writing; the node held nothing it could refresh with; and the convenience path kept
|
|
||||||
every account's token readable on disk wherever it was wanted.
|
|
||||||
|
|
||||||
**What the mesh has.** [To-be 14](../03-DESIGN/01-to-be/14-model-access.md) is built as far as it goes:
|
|
||||||
a licence is a named record with a vendor; a consumer is a module on a node and is put on one licence;
|
|
||||||
the access token is sealed per holder and delivered to the holder's machine; for a refreshable grant
|
|
||||||
the manager node alone holds the refresh token, encrypted, and refreshes centrally — the one stated
|
|
||||||
carve-out of [ADR 0050](0050-model-access-is-vendor-agnostic.md). The controller has commands to add a
|
|
||||||
licence, put a consumer on it, release it, accept a key, set a manager, set and refresh a grant. **None
|
|
||||||
of them is a verb on the `mesh-controller` seat**, so none can be asked for through the console
|
|
||||||
([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md) exposes eighteen commands and
|
|
||||||
not these). The catalogue has the manager module and a consumer module that already writes an
|
|
||||||
access-token-only credentials file at a path it is told; both are assigned to nothing.
|
|
||||||
|
|
||||||
**Why refresh is central and must stay so.** A refreshable grant rotates its refresh token on use. Two
|
|
||||||
machines each refreshing one account's grant race: the second refresh presents a token the first
|
|
||||||
retired. The predecessor refreshed centrally for this reason, and ADR 0024 kept that half on purpose
|
|
||||||
(*the hard half of this already — and it works*). ADR 0050 narrowed the consequence to one node.
|
|
||||||
|
|
||||||
**The two designs are not in conflict, and the line has to be drawn in a record.** The controller
|
|
||||||
resolving *whose* home a file lands in ([ADR 0176](0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md))
|
|
||||||
and *which* licence a consumer holds (to-be 14) is what the controller does for every module. "No
|
|
||||||
part" cannot mean that, or the module could not be assigned. It can mean — and this record says it
|
|
||||||
means — that **the controller learns nothing about the agent**: no file shape, no path, no key, no
|
|
||||||
word beyond the vendor adapter it already has.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **The module keeps its own registry of accounts and tokens**, the stance read literally. Rejected: a
|
|
||||||
second secret store outside the vault ([ADR 0113](0113-the-vault-makes-every-secret.md)); a refresh
|
|
||||||
token on every workstation, widening ADR 0050's one-node carve-out to every machine a person sits
|
|
||||||
at; and two records of one licence, which drift.
|
|
||||||
2. **The agent refreshes itself**: the mesh delivers a full grant once at a switch and the agent's own
|
|
||||||
refresh keeps it alive. Rejected: the refresh race above, between the agent and the manager and
|
|
||||||
between two machines on one account; and every node then holds a refresh token, which ADR 0050
|
|
||||||
decided no node does.
|
|
||||||
3. **The mesh binds and delivers; the module writes; a switch is the binding changed, asked for through
|
|
||||||
the console.** Chosen.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**The consumer is the module on the machine: the operator's interactive sessions on that node, under
|
|
||||||
that account, hold one licence at a time.** That is to-be 15's `(node, module)` identity, with the
|
|
||||||
agent module as the module. Two machines may hold different licences, the ordinary case. The mesh's own
|
|
||||||
sessions on a machine are other modules and hold theirs in their own right.
|
|
||||||
|
|
||||||
**The mesh delivers; the module writes.** The module requires `model-access`. The mesh resolves the
|
|
||||||
licence the consumer is on, delivers the access token sealed to the machine as a secret in the module's
|
|
||||||
own state, and delivers the non-secret facts — the licence's name, what it serves — beside it. **The
|
|
||||||
module's own process writes the agent's credentials file** from the delivered secret: under the
|
|
||||||
account's home, owned by the account, readable by nobody else, written atomically, and access-token-only
|
|
||||||
— a refresh token found there is removed, because a node never holds one (ADR 0050). The file's content
|
|
||||||
is never a declared file's content ([ADR 0177](0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)).
|
|
||||||
The step runs when the delivered secret changes and on a schedule as a backstop, so a refreshed token
|
|
||||||
reaches the file without anyone asking.
|
|
||||||
|
|
||||||
**The controller learns nothing about the agent.** Where the file is, what shape it has, what the
|
|
||||||
agent calls its keys, how it is told about the console — all of that is the module's definition and
|
|
||||||
code. The controller contributes the facts it contributes to every module: the account, the home, the
|
|
||||||
licence, the delivery.
|
|
||||||
|
|
||||||
**A switch is the binding changed.** Putting the consumer on another licence is the mesh's existing
|
|
||||||
act — *use this licence, for this consumer* — and it becomes a verb on the `mesh-controller` seat the
|
|
||||||
way the other verbs did (ADR 0154): the command it already has, served on the bus, listed by the
|
|
||||||
console. The module serves two tools of its own: one that reports which licence the machine holds and
|
|
||||||
when its token expires, and one that invokes the seat's verb for a named licence and then waits until
|
|
||||||
the credentials file carries the new licence's token, answering with the licence's name — **never the
|
|
||||||
token, in any answer, log or event**. A skill in the agent's directory wraps the second so a person
|
|
||||||
asks in a sentence. Switching remains a reaction, not a declaration (ADR 0024): a person asks for it,
|
|
||||||
and nothing in the declaration language grows a conditional.
|
|
||||||
|
|
||||||
**Enrolling an account is the mesh's act on the manager node.** A new licence is added by name, its
|
|
||||||
grant obtained by a login in a throwaway home on the manager node and adopted sealed to that node's
|
|
||||||
key, as the manager module already does. No token is pasted into a prompt, printed, or passed as an
|
|
||||||
argument (to-be 14's rule for keys).
|
|
||||||
|
|
||||||
**The shell helper that read tokens from a file is retired, not replaced.** A second concurrent
|
|
||||||
session on the same machine under a different licence would need a second consumer identity — the
|
|
||||||
unbuilt half of to-be 14's gap — and is not provided here. Stated so it is not rediscovered as a bug.
|
|
||||||
|
|
||||||
**A licence the mesh no longer grants is withdrawn** at the binding (ADR 0024). The credentials file
|
|
||||||
the module wrote is the module's own output: unassigning the module leaves it, like the agent's other
|
|
||||||
files, and the access token in it expires within hours. Releasing the consumer from the licence is the
|
|
||||||
act that ends its access.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- The agent on a workstation authenticates with a token the mesh delivered and refreshes centrally,
|
|
||||||
and no workstation holds a refresh token or any other account's token.
|
|
||||||
- **The manager must run.** The refresh path exists in the catalogue and is assigned to nothing; it is
|
|
||||||
a prerequisite of this record, on the control node, and the first thing the build proves.
|
|
||||||
- **The controller gains a verb, not knowledge.** The licence commands become seat verbs, each
|
|
||||||
running the command it names, as ADR 0154 did for the others; nothing in them is about the agent.
|
|
||||||
- A switch is a round trip — binding, composition, push, apply — rather than the predecessor's direct
|
|
||||||
write: seconds to a minute, and reported when done rather than assumed.
|
|
||||||
- **What got harder:** running two sessions on one machine under two accounts at once, which the
|
|
||||||
retired helper allowed by keeping tokens readable. The price of not keeping them so.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
| Rule | Checked by |
|
|
||||||
|---|---|
|
|
||||||
| The module's definition declares no secret in a file's content, and requires `model-access` | a catalogue test on the module's definition |
|
|
||||||
| The credentials file is access-token-only, owned by the account, atomic | the consumer module's existing unit tests on the strip and the write, carried into this module; a live check that the file names no refresh token |
|
|
||||||
| A switch through the console changes the licence and the token, and no answer carries a token | a live check: the bound facts name the new licence, the file's fingerprint changes, the tool's answer and the module's log contain neither token |
|
|
||||||
| The controller's licence verbs run the commands they name and carry no agent vocabulary | the seat verb's test, as for the eighteen before it |
|
|
||||||
| The refresh path is live before the module is | the manager assigned on the control node and a refresh observed in the licence's record, before the module's first assignment |
|
|
||||||
| No workstation holds a refresh token | the live check above, on every machine the module is assigned to |
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0024](0024-model-access-is-a-provision.md), [ADR 0050](0050-model-access-is-vendor-agnostic.md),
|
|
||||||
[ADR 0055](0055-model-access-is-answered-by-a-licence-or-a-node.md) — what a licence is, who refreshes, what answers
|
|
||||||
- [to-be 14](../03-DESIGN/01-to-be/14-model-access.md), [to-be 15](../03-DESIGN/01-to-be/15-the-agent-session.md) — the consumer identity and the gap this leaves where it is
|
|
||||||
- [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — how a verb reaches a person
|
|
||||||
- [ADR 0113](0113-the-vault-makes-every-secret.md) — why there is no second registry
|
|
||||||
- [ADR 0177](0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md) — the class the credentials file is in
|
|
||||||
- the predecessor's `claude-code` module: its switch tool, its shell helpers and the rules of its skill
|
|
||||||
- mesh-catalog `modules/anthropic-manager`, `modules/anthropic-consumer` — the refresh and the write, as built
|
|
||||||
@@ -275,7 +275,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0155** — [A definition names no installation: how that is checked, and the three ways a value that did gets out](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
- **0155** — [A definition names no installation: how that is checked, and the three ways a value that did gets out](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
||||||
- **0176** — [The operator account is a node fact, and a home is a placement root](0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
|
- **0176** — [The operator account is a node fact, and a home is a placement root](0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
|
||||||
- **0177** — [Inside a home, the mesh owns the directory and the files it places, writes into the tool's own files, and holds everything else as found](0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
- **0177** — [Inside a home, the mesh owns the directory and the files it places, writes into the tool's own files, and holds everything else as found](0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||||
- **0178** — [The mesh binds and delivers a licence; the module alone writes the tool's credential; a switch is the binding changed, asked for through the console](0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md)
|
- **0178** — [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](0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||||
|
|
||||||
### How it is built
|
### How it is built
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ code:
|
|||||||
updated: 2026-10-02
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0024-model-access-is-a-provision.md
|
- 02-DECISIONS/0024-model-access-is-a-provision.md
|
||||||
- 02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md
|
- 02-DECISIONS/0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||||
- 02-DECISIONS/0009-modules-and-the-graph.md
|
- 02-DECISIONS/0009-modules-and-the-graph.md
|
||||||
- 02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
|
- 02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
|
||||||
- 02-DECISIONS/0055-model-access-is-answered-by-a-licence-or-a-node.md
|
- 02-DECISIONS/0055-model-access-is-answered-by-a-licence-or-a-node.md
|
||||||
@@ -97,12 +97,13 @@ So `(node, module)` tells them apart, and asking for a licence per session neede
|
|||||||
identity. Checked rather than argued: two sessions on one machine hold different licences, each is
|
identity. Checked rather than argued: two sessions on one machine hold different licences, each is
|
||||||
given its own key, and releasing one leaves the other.
|
given its own key, and releasing one leaves the other.
|
||||||
|
|
||||||
*2026-10-02:* the operator's own interactive agent on a workstation is a consumer the same way —
|
*2026-10-02:* the operator's own agent at a terminal is **not** a consumer of this provision: it is
|
||||||
`(node, claude-code)`, one licence at a time per machine, delivered by the mesh and written by the
|
coupled to an Anthropic grant and nothing else, so it uses the `anthropic-licence-manager` seat, whose
|
||||||
module, switched through the console
|
holder owns the Anthropic licences, their bindings and their rotation, and hands each node's agent its
|
||||||
([ADR 0178](../../02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md),
|
token over the bus
|
||||||
[36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md)). The licence commands
|
([ADR 0178](../../02-DECISIONS/0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md),
|
||||||
become verbs on the controller's seat for it; none was one before.
|
[36](36-the-operators-agent-on-a-machine.md), [37](37-the-anthropic-licence-manager.md)). This
|
||||||
|
provision stays for the consumers that do not care which vendor answers.
|
||||||
|
|
||||||
**What is still open is the rest of the gap, and it is the harder half.** A *worker* is not one
|
**What is still open is the rest of the gap, and it is the harder half.** A *worker* is not one
|
||||||
per machine — many can run on one, from one module — so `(node, module)` cannot name them apart
|
per machine — many can run on one, from one module — so `(node, module)` cannot name them apart
|
||||||
|
|||||||
@@ -6,252 +6,200 @@ updated: 2026-10-02
|
|||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
|
- 02-DECISIONS/0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
|
||||||
- 02-DECISIONS/0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
- 02-DECISIONS/0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||||
- 02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md
|
- 02-DECISIONS/0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||||
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
|
|
||||||
- 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md
|
|
||||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
|
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
|
||||||
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||||
- 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
|
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
|
||||||
- 02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md
|
- 02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 36 — The operator's agent on a machine: the `claude-code` module
|
# 36 — The operator's agent on a machine: the `claude-code` module
|
||||||
|
|
||||||
**The agent a person runs at a terminal, put on the machine by the mesh, instructed by the mesh,
|
**The agent a person runs at a terminal, put on the machine by the mesh, instructed by the mesh, pointed
|
||||||
pointed at the console, and authenticated with a licence the mesh delivers.** It is the first member of
|
at the console, and holding the licence the manager hands it.** It is a member of the family
|
||||||
the family [to-be 29 §2](29-a-node-has-operator-accounts.md) names — the modules that place files under
|
[to-be 29 §2](29-a-node-has-operator-accounts.md) names, the modules that touch a person's machine, and
|
||||||
an operator's home — and the smallest, so it is where the pattern is proven before the shell, the
|
its counterpart is [37 — The Anthropic licence manager](37-the-anthropic-licence-manager.md).
|
||||||
terminal and the desktop follow.
|
|
||||||
|
|
||||||
What it replaces: the predecessor's module of the same name, which installed the agent's package and
|
What it replaces: the predecessor's module of the same name and a sibling, which placed six files under
|
||||||
placed five files under the operator's home, and a sibling that placed a sixth. The predecessor is
|
the operator's home. The predecessor is retired; the six files are still on both workstations telling
|
||||||
retired; those six files are still on both workstations telling every session to use tools that no
|
every session to use tools that no longer exist.
|
||||||
longer exist. That is the symptom this design answers, and it answers it by making the files a module's
|
|
||||||
again rather than by editing them.
|
|
||||||
|
|
||||||
## 1. What it is
|
**Three rules shape everything below.** The host is module-agnostic: it installs the package and gives
|
||||||
|
the module a state directory, and knows no vendor, no agent, no path under a home. The controller has no
|
||||||
|
part beyond resolving what it resolves for every module. And the module handles its own files: the
|
||||||
|
mesh's part of the agent's configuration is written by the module's own code, from what the mesh
|
||||||
|
delivered it and what the manager handed it.
|
||||||
|
|
||||||
A module, `claude-code`, universal tier: assigned to every node a person logs into, which is every node
|
## 1. Where the mesh's configuration lives: the agent's managed directory, not the home
|
||||||
with an operator account ([ADR 0176](../../02-DECISIONS/0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
|
|
||||||
It declares the agent's package, owns the agent's configuration directory under the account's home, and
|
|
||||||
requires two things: `model-access`, for the licence
|
|
||||||
([ADR 0178](../../02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md)),
|
|
||||||
and the console on the same machine, for the tools
|
|
||||||
([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)). It names no
|
|
||||||
node, no path and no login: the account and its home are machine facts, the node's name is a machine
|
|
||||||
fact, the node's role is a setting on the assignment, and the console's address is what the console
|
|
||||||
serves.
|
|
||||||
|
|
||||||
**The controller has no part in it beyond what it has in every module.** It resolves the account, the
|
The agent reads a machine-wide, administrator-owned configuration directory under `/etc`, documented
|
||||||
home, the licence and the console's port, and delivers them. It holds nothing about the agent: no file
|
by the vendor: a managed settings file that outranks every user and project setting; a key in it that
|
||||||
shape, no key name, no path. The one controller change this design asks for is not about the agent at
|
adds HTTP tool servers *beside* a person's own without blocking them; and a managed instruction file every
|
||||||
all — the licence commands become verbs on the controller's seat, as the other commands did
|
session reads before the user's and the project's. The agent has **no** machine-wide directory for
|
||||||
([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).
|
rules, skills, slash commands or hooks; those exist only under a home or a project.
|
||||||
|
|
||||||
## 2. What it owns under the home, and what it leaves alone
|
So the mesh's part of the agent's configuration lives there, **owned whole by the module**, and the home
|
||||||
|
is left alone. What the predecessor shipped as two rule files and two skills folds into the managed
|
||||||
|
instruction file and the manager's tools:
|
||||||
|
|
||||||
Every path the module touches is in one of the four classes
|
| the predecessor placed | becomes |
|
||||||
[ADR 0177](../../02-DECISIONS/0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
|
||||||
draws, and the class is visible from the shape the definition declares. The agent's directory is
|
|
||||||
`~/.claude`; its own state file is `~/.claude.json` beside it.
|
|
||||||
|
|
||||||
| path | class | declared as |
|
|
||||||
|---|---|---|
|
|
||||||
| `~/.claude/` | owned directory | a directory, owner the account, readable by the account alone |
|
|
||||||
| `~/.claude/CLAUDE.md` | owned | a file: how a session on this mesh works (§4) |
|
|
||||||
| `~/.claude/rules/00-mesh.md` | owned | a file: this node's identity (§4) |
|
|
||||||
| `~/.claude/rules/conventions.md` | owned | a file: the rules of the repositories (§4) |
|
|
||||||
| `~/.claude/skills/mesh-licence/SKILL.md` | owned | a file: the licence skill (§5) |
|
|
||||||
| `~/.claude/settings.json` | written into | the agent's settings; the mesh's key is `attribution`, and only that (below) |
|
|
||||||
| `~/.claude.json` | written into | the agent's own state; the mesh's key is the console's entry under the servers the agent speaks to (§3) |
|
|
||||||
| `~/.claude/.credentials.json` | written by the module's process | a delivered secret and a step (§5) |
|
|
||||||
| everything else | found | nothing — the person's memory, history, projects, local settings, plugins, their own rules and skills |
|
|
||||||
|
|
||||||
**Which keys of the settings file are the mesh's.** A key is the mesh's when it encodes a rule of the
|
|
||||||
mesh, and the person's when it is a preference. `attribution` — the trailers the agent adds to commits
|
|
||||||
and pull requests — encodes the repositories' convention and is the mesh's. The model, the spinner, the
|
|
||||||
drafts, the automation mode and everything else are the person's, and the predecessor's experience with
|
|
||||||
the model key is the evidence: a mesh that sets a preference reverts a person's choice on every push. A
|
|
||||||
preference the operator wants on every machine belongs to the family's dotfiles module, not here.
|
|
||||||
|
|
||||||
**The agent's own state file is written into for one key.** The agent is told about the console as one
|
|
||||||
entry among the servers it speaks to, in the file where it keeps that list. Everything else in that
|
|
||||||
file — the account it is logged in as, its caches, its history of projects — is the agent's, and
|
|
||||||
ADR 0102's rule is exactly what keeps it: the mesh sets one key and gives it back on undeclare.
|
|
||||||
|
|
||||||
## 3. The predecessor's six files
|
|
||||||
|
|
||||||
They were placed by a generator that no longer exists; to the mesh they are found. ADR 0177 says what
|
|
||||||
happens to each kind, and this is the list:
|
|
||||||
|
|
||||||
| file | fate |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| `CLAUDE.md`, `rules/conventions.md` | **adopted.** The module declares the same paths; the host keeps the found original once and writes the mesh's content ([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)) |
|
| `~/.claude/CLAUDE.md` | the managed instruction file: how a session on this mesh works (§3) |
|
||||||
| `settings.json` | **written into.** The values the predecessor merged and the person changed since — the model among them — stay; the mesh sets its one key |
|
| `~/.claude/rules/00-hal-mesh.md`, `~/.claude/rules/conventions.md` | sections of the same file: this node's identity, the repositories' conventions |
|
||||||
| `rules/00-hal-mesh.md` | **removed by the operator, once.** Its successor is `rules/00-mesh.md`; the old name carries the predecessor's and stays otherwise |
|
| `~/.claude/settings.json`, merged | the managed settings file: the mesh's keys only, outranking nothing a person did not also set |
|
||||||
| `skills/hal-switch-license/SKILL.md` | **removed by the operator, once.** Its successor is `skills/mesh-licence/SKILL.md` |
|
| `~/.claude/skills/hal-switch-license/SKILL.md` | the manager seat's `switch` verb, listed by the console, and a sentence in the instruction file saying to use it |
|
||||||
| `skills/cleanup/SKILL.md` | **removed by the operator, once.** A repository hygiene skill naming the predecessor's forge and repository; not the mesh's |
|
| `~/.claude/skills/cleanup/SKILL.md` | nothing; it named the predecessor's forge |
|
||||||
|
| the console's entry in the agent's user-scope state | the managed settings' tool-server key, from the console's provision (§4) |
|
||||||
|
|
||||||
The module's documentation names the three removals, so a person assigning it on a workstation that
|
**The home.** Under [ADR 0177](../../02-DECISIONS/0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||||
carried the predecessor knows the step. On a fresh machine there is nothing to remove.
|
every path under `~/.claude` is *found*, with one exception: the agent's credentials file, which the
|
||||||
|
module's own code writes for a subscription licence (§5). The person's memory, history, projects, local
|
||||||
|
settings, their own rules, skills and tool servers are never read or written by the mesh. **The six
|
||||||
|
predecessor files are the operator's to remove, once, on each workstation**; the module's documentation
|
||||||
|
lists them, and until they go the agent reads stale instructions beside the mesh's.
|
||||||
|
|
||||||
**The console's entry changes name.** The agent on both workstations today reaches the console under
|
## 2. What the module declares and what its code writes
|
||||||
an entry named after this installation. A definition names no installation
|
|
||||||
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)), so
|
|
||||||
the module writes the entry as `mesh`, and every tool an agent sees is prefixed accordingly. The
|
|
||||||
hand-made entry is the person's to remove; until they do, the agent sees the mesh's tools twice.
|
|
||||||
|
|
||||||
## 4. What the three documents say
|
**Declared, applied by the host:** the agent's package (§7); the module's state directory; a facts file
|
||||||
|
in that directory carrying the node's name, the operator account, the console's endpoint, the module's
|
||||||
|
settings; the bus, the console's provision, and that it uses the `anthropic-licence-manager` seat.
|
||||||
|
Nothing under the home, nothing under `/etc`.
|
||||||
|
|
||||||
**Prose, not a paste** — the files are the module's; this is what they are for.
|
**Written by the module's code**, from the facts file and the manager's hand-over, whenever either
|
||||||
|
changes:
|
||||||
|
|
||||||
**`CLAUDE.md` — how a session on this mesh works.** The console is the only path to the mesh, and its
|
| path | content |
|
||||||
tools are the vocabulary: the record is asked through the records module's tools, symptom first — the
|
|
||||||
literal error text before a hypothesis — and that is the *search before you dig* rule rewritten for a
|
|
||||||
knowledge base that is now the record itself ([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md));
|
|
||||||
the mesh is asked and changed through the controller seat's verbs — status, plan, assign, push,
|
|
||||||
settings, and licence once it exists; the forge through the forge module's tools. The hard rules are the
|
|
||||||
same rules in new words: a file the mesh manages is changed through the verb that owns it or through
|
|
||||||
the catalogue, never on disk, and `plan` says what the mesh would write; a store's database is never
|
|
||||||
written by hand; main is never pushed; the mesh creates no symlinks and nobody else does either; a
|
|
||||||
package is declared, not installed by hand. It uses the glossary's words — controller, foundation,
|
|
||||||
node, seat, console — and none of the predecessor's.
|
|
||||||
|
|
||||||
**`rules/00-mesh.md` — who this node is.** Two facts and one pointer: the node's name, from the
|
|
||||||
machine; the node's role, from the assignment's settings on this node; and that the other nodes are
|
|
||||||
asked of the controller's `nodes` verb rather than listed here. The predecessor's rule carried a table
|
|
||||||
of every node with its public domain and role; a table is a copy that drifts, and the live answer is
|
|
||||||
one tool call away. No address, no public domain.
|
|
||||||
|
|
||||||
**`rules/conventions.md` — the rules of the repositories.** Concise commit messages in the imperative,
|
|
||||||
focused on why; a branch, a pull request and a human approval for every merge; test before pushing,
|
|
||||||
because nodes update unattended; follow the playbooks in the record; shared logic in the SDK; the
|
|
||||||
module repository's rules on manifests. Nothing that names a tool of the predecessor's.
|
|
||||||
|
|
||||||
**Where the module gets the name and the role.** The name is a machine fact the controller already
|
|
||||||
offers a definition. The role is a value a person chooses per node — *the laptop*, *the home-server* —
|
|
||||||
and is an operator value on the assignment's node layer, refused by name when unset
|
|
||||||
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
|
|
||||||
So assigning the module to a node is two acts: the assignment, and the node's role in its settings.
|
|
||||||
|
|
||||||
## 5. The licence
|
|
||||||
|
|
||||||
[ADR 0178](../../02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md)
|
|
||||||
decides it; this is the shape.
|
|
||||||
|
|
||||||
**The consumer** is `(node, claude-code)`: the operator's interactive sessions on that machine, under
|
|
||||||
that account, on one licence at a time. The module requires `model-access` and is put on a licence like
|
|
||||||
the consumer module already in the catalogue.
|
|
||||||
|
|
||||||
**Delivery and the write.** The mesh delivers the access token sealed to the machine, as a secret in
|
|
||||||
the module's own state directory, and the bound facts beside it. A step in the module's own process —
|
|
||||||
the consumer module's existing write, carried over — reads the secret and writes
|
|
||||||
`~/.claude/.credentials.json`: owned by the account, readable by the account alone, atomically,
|
|
||||||
access-token-only. The step names the secret file as what it reads and runs again when it changes
|
|
||||||
([ADR 0099](../../02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md)), and on a schedule as
|
|
||||||
a backstop, so a refreshed token reaches the file unasked. It runs in the module's own context and
|
|
||||||
never as the person.
|
|
||||||
|
|
||||||
**Refresh** is the manager module's on the control node, as [ADR 0050](../../02-DECISIONS/0050-model-access-is-vendor-agnostic.md)
|
|
||||||
built it. It is assigned to nothing today and is the first prerequisite of the build.
|
|
||||||
|
|
||||||
**The two tools** the module serves, listed by the console under the module's name:
|
|
||||||
|
|
||||||
| tool | answers |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| `licence_status` | which licence this machine's agent holds, from the bound facts; when its access token expires; whether the file on disk matches what was delivered — by fingerprint, never by value |
|
| the managed settings file | the mesh's keys: the tool servers (the console, plus any the operator declared as settings), the attribution trailers, and — for an API-key binding only — the key-helper that serves the key |
|
||||||
| `licence_switch` | asks the controller seat's `licence` verb to put this consumer on the named licence, waits until the credentials file carries the new licence's token, and answers with the licence's name and expiry. Refuses with the mesh's own words when the licence does not exist or the consumer cannot be put on it |
|
| the managed instruction file | §3 |
|
||||||
|
| the agent's credentials file under the operator's home | for a subscription binding only: the access token the manager handed over, as the operator, readable by the operator alone, atomic, no refresh token |
|
||||||
|
| the module's keypair in its state | made once, the private half never leaves (§5) |
|
||||||
|
|
||||||
Neither tool, nor the module's log, nor any event it emits, ever carries a token. The module declares
|
Writing under `/etc` and as the operator under the home are two escalations the module's code performs
|
||||||
that it invokes the controller seat's `licence` verb, and nothing else.
|
for itself; the mesh does not run the module as root for everyone, and the caller does not know
|
||||||
|
([research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)).
|
||||||
|
|
||||||
**The skill** — `skills/mesh-licence/SKILL.md` — wraps `licence_switch` so a person asks in a sentence,
|
**Which settings keys are the mesh's.** A key is the mesh's when it encodes a rule of the mesh: the tool
|
||||||
and carries the predecessor's rules unchanged in substance: never ask for or print a token; never edit
|
servers that reach the mesh, the attribution convention of its repositories, the key-helper a binding
|
||||||
the credentials file by hand; the tool writes the file and the record together; with no licence named,
|
requires. The model, the spinner, the drafts and every other preference are the person's, and the
|
||||||
ask rather than guess.
|
predecessor's experience with the model key is the evidence: a mesh that sets a preference reverts a
|
||||||
|
person's choice on every push.
|
||||||
|
|
||||||
**Enrolling an account** happens on the manager node: a licence added by name, its grant obtained by a
|
## 3. What the instruction file says
|
||||||
login in a throwaway home and adopted sealed to that node's key, as the manager module does. **The
|
|
||||||
shell helper** that ran the agent with a token from a plaintext file is retired and not replaced
|
|
||||||
([ADR 0178](../../02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md)
|
|
||||||
says why).
|
|
||||||
|
|
||||||
## 6. The console
|
Prose, not a paste; the file is the module's.
|
||||||
|
|
||||||
The agent reaches the mesh through the console on the machine's loopback
|
**How a session on this mesh works.** The console is the only path to the mesh, and its tools are the
|
||||||
([to-be 34](34-the-console.md)). The module must tell the agent the console's address, and the port is
|
vocabulary: the record is asked through the records module, symptom first — the literal error text before
|
||||||
the console's to say: today the console's manifest declares it and the host assigns it, and nothing but
|
a hypothesis ([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md));
|
||||||
the console knows what was assigned. So **the console provides a node-scoped provision** — the MCP
|
the mesh is asked and changed through the controller seat's verbs; the forge through the forge module's
|
||||||
endpoint on loopback — serving its port, and the module requires it. A requirement names what the
|
tools; a licence through the `anthropic-licence-manager` seat's verbs, never by editing a file. The hard
|
||||||
consumer is coupled to ([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)):
|
rules in new words: a file the mesh manages is changed through the verb that owns it or through the
|
||||||
the agent is coupled to an MCP endpoint on its own machine, not to a module name. Co-location resolves
|
catalogue, never on disk; a store's database is never written by hand; main is never pushed; the mesh
|
||||||
it, and a machine without the console refuses the agent module by name — which is right, because an
|
creates no symlinks and nobody else does; a package is declared, not installed by hand. The glossary's
|
||||||
agent without the console is the predecessor's situation again.
|
words, none of the predecessor's.
|
||||||
|
|
||||||
This is a change to the console's definition, not to the controller. [To-be 34 §1](34-the-console.md)
|
**Who this node is.** The node's name, from the facts file; the node's role, from the module's settings
|
||||||
says the console has *no provision*; this is the one it gains, at node scope, and the design is amended
|
on the node's layer; and that the other nodes are asked of the controller's `nodes` verb rather than
|
||||||
in the same change.
|
listed here, because a table is a copy that drifts.
|
||||||
|
|
||||||
## 7. Scope, settings and the order of assignment
|
**The repositories' conventions.** Concise commit messages in the imperative, about why; a branch, a
|
||||||
|
pull request and a human approval for every merge; test before pushing, because nodes update unattended;
|
||||||
|
the playbooks in the record.
|
||||||
|
|
||||||
**Every node with an operator account.** None has one today; the operator states them first. A node
|
## 4. The console
|
||||||
with no account refuses the module, naming the fact.
|
|
||||||
|
|
||||||
**Per node:** the role, in the module's settings on the node layer. **Per mesh:** nothing.
|
The module tells the agent where the console is, and the port is the console's to say. **The console
|
||||||
|
provides a node-scoped provision** — its MCP endpoint on loopback — serving the port the machine gave
|
||||||
|
it, and the module requires it. A requirement names what the consumer is coupled to
|
||||||
|
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)); co-location
|
||||||
|
resolves it; a machine without the console refuses the module by name. [To-be 34](34-the-console.md) is
|
||||||
|
amended in the same change; issue 192 (open) found the gap.
|
||||||
|
|
||||||
**Order:** the manager on the control node and a refresh observed; the licences the operator uses,
|
**Other tool servers** a person wants on every machine, or on one, are a declared setting of this module
|
||||||
enrolled; the console's provision and the module in the catalogue; one workstation assigned, the three
|
— mesh layer or node layer — rendered into the same managed key. A module tool, `mcp_configure`,
|
||||||
predecessor files removed there, and a new session read to confirm it sees the mesh's instructions and
|
validates a server and sets the setting through the controller's settings verb, so the list stays
|
||||||
the console's tools; then the rest.
|
declared state. The agent's own HTTP-only constraint for managed servers applies; a person's local
|
||||||
|
command-based servers stay their own, in their own file.
|
||||||
|
|
||||||
## 8. The package
|
**The entry's name is `mesh`.** The hand-made entry both workstations carry today is named after this
|
||||||
|
installation, which a definition may not be ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md));
|
||||||
|
it is the person's to remove, and until then the agent sees the mesh's tools twice.
|
||||||
|
|
||||||
The module declares the agent's package. The distribution every node of the live mesh runs does not
|
## 5. The licence: the consumer side
|
||||||
carry it in its repositories: the two workstations have it from a build the predecessor's helper made
|
|
||||||
from the community repository, and nothing updates it since the predecessor retired. On those two the
|
|
||||||
declaration is satisfied — the package is present. **On a fresh machine the host's package manager
|
|
||||||
refuses it, in its own words, and the module is not applied there.** That is correct and is a gap.
|
|
||||||
|
|
||||||
The answer the mesh already has a shape for is a package repository for this ecosystem as a seat
|
[ADR 0178](../../02-DECISIONS/0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||||
([ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md)), fed by the
|
decides it; to-be 37 is the manager's half. This module:
|
||||||
builder with a package it builds from the vendor's release, and trusted by every node's package manager.
|
|
||||||
Then `package: claude-code` is answered the way every package is, and updates arrive the way every
|
|
||||||
update does. It is not built, and it is not this module's to build: it is a seat and a provider module
|
|
||||||
of its own, needed by every package the distribution does not carry.
|
|
||||||
|
|
||||||
Rejected as the answer: the vendor's own installer, which puts a self-updating binary under the
|
- **makes a keypair** in its state the first time it runs and registers the public half with the seat;
|
||||||
person's home. It is a hand-installed package the mesh cannot see, reproduce or roll back, and it
|
- **serves `apply`**: the manager's hand-over, a token sealed to the module's key, with the licence's
|
||||||
updates itself outside the mesh — the arrangement the manifest rule *never install a package by hand*
|
name and kind. A rotation of the same licence is applied only if newer within one lineage; a switch is
|
||||||
exists to end.
|
applied regardless, because across licences the expiries are unrelated. The answer says applied or
|
||||||
|
refused and why, and never echoes a token;
|
||||||
|
- **pulls** at start and when its token nears expiry, by the seat's `current` verb, and keeps the last
|
||||||
|
token when the manager does not answer, saying so;
|
||||||
|
- **writes** for a subscription licence the credentials file as the operator, access-token-only; for the
|
||||||
|
API-key licence sets the key-helper in the managed settings to a small program that prints the key
|
||||||
|
from the module's state, so no file under the home is touched;
|
||||||
|
- **offers a login to the manager**: when the credentials file changes by a person's login, it reads the
|
||||||
|
account's identity from the agent's state file and offers the grant to the seat, sealed to the manager's
|
||||||
|
key, for adoption; the manager decides;
|
||||||
|
- **serves `licence_status`**: which licence and kind this node holds, when the token expires, whether
|
||||||
|
the file matches what was handed over — by fingerprint, never by value.
|
||||||
|
|
||||||
|
Switching is the seat's `switch` verb, asked through the console; this module only applies what it is
|
||||||
|
handed.
|
||||||
|
|
||||||
|
## 6. Scope, settings and the order of assignment
|
||||||
|
|
||||||
|
**Every node with an operator account** ([ADR 0176](../../02-DECISIONS/0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
|
||||||
|
None has one today; the operator states them first. **Per node:** the role. **Per mesh or per node:**
|
||||||
|
extra tool servers. **Prerequisite:** the manager holds its seat and has adopted the licences.
|
||||||
|
|
||||||
|
**Order:** the manager assigned and a refresh observed; the console's provision in the catalogue; this
|
||||||
|
module on one workstation; the six predecessor files and the hand-made console entry removed there; a
|
||||||
|
new session read to confirm it sees the mesh's instruction file, the console's tools under `mesh`, and
|
||||||
|
its licence; then the rest.
|
||||||
|
|
||||||
|
## 7. The package
|
||||||
|
|
||||||
|
The module declares the agent's package. The distribution every node runs does not carry it in its
|
||||||
|
repositories: the two workstations have it from a build the predecessor's helper made from the community
|
||||||
|
repository, and nothing updates it since. On those two the declaration is satisfied. **On a fresh machine
|
||||||
|
the host's package manager refuses it, in its own words, and the module is not applied there.** The
|
||||||
|
answer is a package repository for this ecosystem as a seat
|
||||||
|
([ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md)), fed by the builder
|
||||||
|
and trusted by every node's package manager; not built, and not this module's to build. The vendor's own
|
||||||
|
installer is rejected: it puts a self-updating binary under the person's home, invisible to the mesh.
|
||||||
|
|
||||||
## How it is checked
|
## How it is checked
|
||||||
|
|
||||||
| Check | Defends |
|
| Check | Defends |
|
||||||
|---|---|
|
|---|---|
|
||||||
| the module's definition names no node, path or login, and declares no secret in a file's content | ADR 0112, ADR 0155, ADR 0178 |
|
| the module's definition names no node, path or login, declares nothing under a home or `/etc`, and no file resource carries a secret | ADR 0112, ADR 0155, ADR 0178 |
|
||||||
| on a lab machine with an account, a seeded home holding a person's rule file, the predecessor's three leftovers and a settings file with the person's model: after assign, the mesh's files are present and owned by the account, the person's file and model are byte-identical, the leftovers are untouched, the console's entry is set; after unassign, the mesh's files are gone, the two keys are given back, the directory and everything else stand | ADR 0177 |
|
| on a lab machine with an account and a seeded home holding a person's rule file and the predecessor's leftovers: after assign, the managed directory holds the mesh's files, the home is byte-identical except the credentials file, which is owned by the operator and names no refresh token; after unassign, the managed directory's files are gone and the home is untouched | ADR 0177, the host's agnosticism |
|
||||||
| on a lab machine with no account, the assignment is refused naming the fact | ADR 0176 |
|
| on a lab machine with no account, the assignment is refused naming the fact | ADR 0176 |
|
||||||
| the credentials file is owned by the account, readable by it alone, and names no refresh token; a switch through the console changes the licence named in the bound facts and the file's fingerprint; neither the tool's answer nor the module's log holds a token | ADR 0178, ADR 0050 |
|
| a switch asked of the seat through the console changes the licence and the token on the node; no tool answer and no log line holds a token | ADR 0178 |
|
||||||
| the console's provision resolves by co-location and a machine without the console refuses the module by name | ADR 0027, ADR 0152 |
|
| the API-key binding writes nothing under the home and the agent authenticates through the helper | ADR 0178 |
|
||||||
| a new session on the assigned workstation lists the console's tools under the `mesh` prefix and answers "which node am I" from the identity rule | the exit of the build |
|
| the console's provision resolves by co-location; a machine without the console refuses the module by name | ADR 0027, ADR 0152 |
|
||||||
| the package is reported present on the workstations and refused in the package manager's words on a machine without it | §8, honestly |
|
| a new session on the assigned workstation lists the console's tools under `mesh` and answers "which node am I" from the instruction file | the exit of the build |
|
||||||
|
|
||||||
## What this does not settle
|
## What this does not settle
|
||||||
|
|
||||||
- **Several operator accounts on one node.** ADR 0176 decides one; the module follows.
|
- **Several operator accounts on one node** (ADR 0176 decides one).
|
||||||
- **A parallel session under another licence on the same machine.** The retired helper allowed it by
|
- **A parallel session under another licence on one machine.** The retired shell helper allowed it by
|
||||||
keeping tokens readable; a clean form needs a second consumer identity (to-be 14's open half).
|
keeping tokens readable; not provided.
|
||||||
- **The package repository seat.** §8 names it and leaves it to its own design.
|
- **The package repository seat** (§7).
|
||||||
- **The rest of the family** — shell, terminal, desktop, user-scoped services — each a module of the
|
- **How the module's code is run** — a supervised process per module ([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md))
|
||||||
same shape, each proving nothing new about ownership and something new about its own tool.
|
today, one executor per node when research 018 graduates. Nothing here depends on which.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [ADR 0176](../../02-DECISIONS/0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md),
|
- [ADR 0176](../../02-DECISIONS/0176-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md),
|
||||||
[ADR 0177](../../02-DECISIONS/0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md),
|
[ADR 0177](../../02-DECISIONS/0177-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md),
|
||||||
[ADR 0178](../../02-DECISIONS/0178-the-mesh-binds-and-delivers-a-licence-and-the-module-writes-the-tools-credential.md) — the three decisions this rests on
|
[ADR 0178](../../02-DECISIONS/0178-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the decisions
|
||||||
- [to-be 29](29-a-node-has-operator-accounts.md) — the family; [to-be 14](14-model-access.md),
|
- [37 — The Anthropic licence manager](37-the-anthropic-licence-manager.md), [34 — The console](34-the-console.md), [29 — A node has operator accounts](29-a-node-has-operator-accounts.md)
|
||||||
[to-be 15](15-the-agent-session.md) — the licence and the consumer; [to-be 34](34-the-console.md) — the console
|
- [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md) — the operator's machine as modules, and where tools run
|
||||||
- the predecessor's `claude-code` module and its sibling's identity rule — what is replaced, read from the workstations on 2026-10-02
|
- the vendor's documentation on managed settings, managed tool servers, the managed instruction file and the key-helper, read 2026-10-02
|
||||||
- mesh-catalog `modules/anthropic-consumer` — the write this module carries over; `modules/anthropic-manager` — the refresh it depends on
|
- the predecessor's two modules and the six files on the workstations, read 2026-10-02
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user