The operator's agent and its licence manager are modules: ADR 0181–0183, to-be 36 and 39
The predecessor's agent module was retired and its six files stayed on both workstations telling every session to use tools that no longer exist. This is its successor's design, revised during review on the operator's directions: the host is module-agnostic, the controller has no part, and a real licence manager hands out the correct licence in every situation. - ADR 0181 (reconstructed): the operator account is a node fact stated by the operator; the home is derived unless stated; a resource may be placed under it owned by the account; a node with no account refuses one. What the controller shipped on 2026-09-27 without a record. - ADR 0182: inside a home the module owns the directory and the files it places, writes into the tool's own files for its few keys, never declares a credential's content, and holds everything else as found; a predecessor's leftovers are the operator's to remove once. - ADR 0183: claude-licence-manager holds the mesh seat anthropic-licence-manager and owns the Anthropic licences, grants (encrypted with a key the vault made for it), bindings per touchpoint, usage and audit; one rotation source under a lease; a token travels 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 host delivers package and state and knows nothing else. A bounded exception to ADR 0113; dated mechanism notes on ADR 0050 and 0113. - To-be 36 (claude-code): the mesh's part of the agent's configuration lives in the agent's machine-wide managed directory, owned whole by the module and written by its code; nothing under the home but the credentials file of a subscription licence; the API-key licence through the key-helper; the console as a node-scoped provision (to-be 34 amended); MCP servers as settings with an mcp_configure tool; one agent directory per machine shared by every session. - To-be 39 (claude-licence-manager): 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, the seat's verbs. Numbers taken across main and every open branch at the time of the merge; to-be 14, 29 and 34 carry dated notes; the glossary gains "operator account".
This commit is contained in:
@@ -4,9 +4,10 @@ status: in-progress
|
||||
code:
|
||||
- mesh-controller internal/licences
|
||||
- mesh-controller cmd/mesh-controller/licence.go
|
||||
updated: 2026-09-05
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0024-model-access-is-a-provision.md
|
||||
- 02-DECISIONS/0183-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/0054-model-usage-is-recorded-at-two-grains.md
|
||||
- 02-DECISIONS/0055-model-access-is-answered-by-a-licence-or-a-node.md
|
||||
@@ -96,6 +97,14 @@ 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
|
||||
given its own key, and releasing one leaves the other.
|
||||
|
||||
*2026-10-02:* the operator's own agent at a terminal is **not** a consumer of this provision: it is
|
||||
coupled to an Anthropic grant and nothing else, so it uses the `anthropic-licence-manager` seat, whose
|
||||
holder owns the Anthropic licences, their bindings and their rotation, and hands each node's agent its
|
||||
token over the bus
|
||||
([ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md),
|
||||
[36](36-the-operators-agent-on-a-machine.md), [39](39-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
|
||||
per machine — many can run on one, from one module — so `(node, module)` cannot name them apart
|
||||
and this reasoning does not extend to them. That belongs with
|
||||
|
||||
@@ -7,6 +7,8 @@ code:
|
||||
- mesh-controller cmd/mesh-controller
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
|
||||
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||
- 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
|
||||
- 02-DECISIONS/0051-shared-data-is-the-operators.md
|
||||
@@ -171,6 +173,15 @@ fact, the home as a placement root, and what the mesh may and may not do under a
|
||||
decision this document names but no record states. They are the next records to write, before the
|
||||
family of §2 modules is built.
|
||||
|
||||
*2026-10-02:* two of them are written. [ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
|
||||
records the account as a node fact and the home as a placement root, reconstructed from what shipped;
|
||||
[ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||
generalises §3's boundary to every directory under a home. The first member of the §2 family is
|
||||
designed in [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md). User-scoped
|
||||
units are [ADR 0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md),
|
||||
written the same day; still unwritten: several accounts per node, and the CA. On the same day every node of the
|
||||
live mesh still carried an empty account.
|
||||
|
||||
## Why now, and why not yet
|
||||
|
||||
**Why it matters:** when HAL retires, the generators that keep `~/.ssh`, shell config and the
|
||||
|
||||
@@ -30,6 +30,14 @@ module's credential and listens on loopback. It has no state, no provision, no s
|
||||
the bus, which it gets the way every module does: a credential the mesh minted for `<node>.mesh-console`,
|
||||
sealed to the machine, delivered as the module's own secret.
|
||||
|
||||
*2026-10-02:* it gains one provision, at node scope — the MCP endpoint on loopback, serving the port
|
||||
the machine gave it — so that a module whose software must be told where the console is requires that
|
||||
and is coupled to an endpoint rather than to a module's name
|
||||
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)). The first
|
||||
consumer is the operator's agent, [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md) §6;
|
||||
a machine without the console refuses such a module by name. Nothing else above changes: no seat, no
|
||||
state, no tools of its own.
|
||||
|
||||
Its manifest says three things nothing else in the catalogue says together:
|
||||
|
||||
- `invokes: ["*"]` — it calls every tool on the mesh, and the bus grants exactly that publish side;
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
|
||||
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.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/0152-the-operators-surface-is-a-module-the-console.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/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||
---
|
||||
|
||||
# 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, pointed
|
||||
at the console, and holding the licence the manager hands it.** It is a member of the family
|
||||
[to-be 29 §2](29-a-node-has-operator-accounts.md) names, the modules that touch a person's machine, and
|
||||
its counterpart is [39 — The Anthropic licence manager](39-the-anthropic-licence-manager.md).
|
||||
|
||||
What it replaces: the predecessor's module of the same name and a sibling, which placed six files under
|
||||
the operator's home. The predecessor is retired; the six files are still on both workstations telling
|
||||
every session to use tools that no longer exist.
|
||||
|
||||
**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.
|
||||
|
||||
## 1. Where the mesh's configuration lives: the agent's managed directory, not the home
|
||||
|
||||
The agent reads a machine-wide, administrator-owned configuration directory under `/etc`, documented
|
||||
by the vendor: a managed settings file that outranks every user and project setting; a key in it that
|
||||
adds HTTP tool servers *beside* a person's own without blocking them; and a managed instruction file every
|
||||
session reads before the user's and the project's. The agent has **no** machine-wide directory for
|
||||
rules, skills, slash commands or hooks; those exist only under a home or a project.
|
||||
|
||||
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:
|
||||
|
||||
| the predecessor placed | becomes |
|
||||
|---|---|
|
||||
| `~/.claude/CLAUDE.md` | the managed instruction file: how a session on this mesh works (§3) |
|
||||
| `~/.claude/rules/00-hal-mesh.md`, `~/.claude/rules/conventions.md` | sections of the same file: this node's identity, the repositories' conventions |
|
||||
| `~/.claude/settings.json`, merged | the managed settings file: the mesh's keys only, outranking nothing a person did not also set |
|
||||
| `~/.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 |
|
||||
| `~/.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 home.** Under [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||
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.
|
||||
|
||||
## 2. What the module declares and what its code writes
|
||||
|
||||
**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`.
|
||||
|
||||
**Written by the module's code**, from the facts file and the manager's hand-over, whenever either
|
||||
changes:
|
||||
|
||||
| path | content |
|
||||
|---|---|
|
||||
| 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 |
|
||||
| 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) |
|
||||
|
||||
Writing under `/etc` and as the operator under the home are two escalations the module's code performs
|
||||
for itself; the mesh does not run the module as root for everyone, and the caller does not know
|
||||
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||
|
||||
**Which settings keys are the mesh's.** A key is the mesh's when it encodes a rule of the mesh: the tool
|
||||
servers that reach the mesh, the attribution convention of its repositories, the key-helper a binding
|
||||
requires. The model, the spinner, the drafts and every other preference 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.
|
||||
|
||||
## 3. What the instruction file says
|
||||
|
||||
Prose, not a paste; the file is the module's.
|
||||
|
||||
**How a session on this mesh works.** The console is the only path to the mesh, and its tools are the
|
||||
vocabulary: the record is asked through the records module, symptom first — the literal error text before
|
||||
a hypothesis ([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; the forge through the forge module's
|
||||
tools; a licence through the `anthropic-licence-manager` seat's verbs, never by editing a file. The hard
|
||||
rules in new words: a file the mesh manages is changed through the verb that owns it or through the
|
||||
catalogue, never on disk; a store's database is never written by hand; main is never pushed; the mesh
|
||||
creates no symlinks and nobody else does; a package is declared, not installed by hand. The glossary's
|
||||
words, none of the predecessor's.
|
||||
|
||||
**Who this node is.** The node's name, from the facts file; the node's role, from the module's settings
|
||||
on the node's layer; and that the other nodes are asked of the controller's `nodes` verb rather than
|
||||
listed here, because a table is a copy that drifts.
|
||||
|
||||
**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.
|
||||
|
||||
## 4. The console
|
||||
|
||||
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.
|
||||
|
||||
**Other tool servers** a person wants on every machine, or on one, are a declared setting of this module
|
||||
— mesh layer or node layer — rendered into the same managed key. A module tool, `mcp_configure`,
|
||||
validates a server and sets the setting through the controller's settings verb, so the list stays
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
## 5. The licence: the consumer side
|
||||
|
||||
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||
decides it; to-be 39 is the manager's half. This module:
|
||||
|
||||
- **makes a keypair** in its state the first time it runs and registers the public half with the seat;
|
||||
- **serves `apply`**: the manager's hand-over, a token sealed to the module's key, with the licence's
|
||||
name and kind. A rotation of the same licence is applied only if newer within one lineage; a switch is
|
||||
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 0181](../../02-DECISIONS/0181-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
|
||||
|
||||
| Check | Defends |
|
||||
|---|---|
|
||||
| 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 0183 |
|
||||
| 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 0182, the host's agnosticism |
|
||||
| on a lab machine with no account, the assignment is refused naming the fact | ADR 0181 |
|
||||
| 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 0183 |
|
||||
| the API-key binding writes nothing under the home and the agent authenticates through the helper | ADR 0183 |
|
||||
| the console's provision resolves by co-location; a machine without the console refuses the module by name | ADR 0027, ADR 0152 |
|
||||
| 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
|
||||
|
||||
- **Several operator accounts on one node** (ADR 0181 decides one).
|
||||
- **A worker's own licence on a machine.** Every interactive session shares the node's one agent
|
||||
directory and its licence, however many run. A worker runs from a home of its own with an agent
|
||||
directory in it, bound to its own licence through the manager (to-be 39 §5); that is for when workers
|
||||
exist, and nothing here changes for it.
|
||||
- **The package repository seat** (§7).
|
||||
- **How the module's tools are run** is decided: the node's tool runtime, host-side
|
||||
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||
The managed files and the credential write are tools of this module that runtime serves. Until the
|
||||
runtime exists on every node, the module's code runs as a supervised process of its own
|
||||
([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)),
|
||||
which changes nothing in what it writes.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md),
|
||||
[ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md),
|
||||
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the decisions
|
||||
- [39 — The Anthropic licence manager](39-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)
|
||||
- [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md) — the operator's machine as modules, and where tools run
|
||||
- the vendor's documentation on managed settings, managed tool servers, the managed instruction file and the key-helper, read 2026-10-02
|
||||
- the predecessor's two modules and the six files on the workstations, read 2026-10-02
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||
- 02-DECISIONS/0024-model-access-is-a-provision.md
|
||||
- 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
|
||||
- 02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
|
||||
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
|
||||
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||
---
|
||||
|
||||
# 39 — The Anthropic licence manager
|
||||
|
||||
**One module knows every Anthropic licence the mesh has, keeps each alive, decides which consumer gets
|
||||
which, and hands every node's agent its token over the bus.**
|
||||
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||
decides it; this is the shape. It is the successor of the predecessor's manager module, built from what
|
||||
that module learned the hard way, and the counterpart of [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md),
|
||||
which is the consumer on every node.
|
||||
|
||||
## 1. What it is
|
||||
|
||||
A module, `claude-licence-manager`, holding the mesh-scoped seat **`anthropic-licence-manager`**. One
|
||||
holder, on the node the operator assigns it to — the control node is the natural one, and nothing in the
|
||||
definition says so. It requires a database for its own store and the bus; it claims the seat; it serves
|
||||
the seat's verbs. It has no port, no route, no file under anyone's home.
|
||||
|
||||
Its store holds four things:
|
||||
|
||||
| table | holds |
|
||||
|---|---|
|
||||
| **licences** | name, kind (`subscription` or `api-key`), the account's identity (id, address, organisation) once adopted, the grant encrypted at rest, when the access token expires, when the refresh token expires, consecutive failures, the refresh lease, when a person was last notified |
|
||||
| **bindings** | one row per consumer: kind (`node-agent`, `node-session`, `worker`), its key (the node, or the node and the worker), the licence, or *inherit* |
|
||||
| **usage** | the vendor's readings per licence per period, raw beside normalised ([ADR 0054](../../02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md)) |
|
||||
| **audit** | every switch, adoption, refusal and drift, with who asked |
|
||||
|
||||
**The grants are encrypted with a key the vault made for the manager** — its one `secret` requirement.
|
||||
The vault keeps that key; the manager keeps the grants. That is ADR 0050's carve-out, one module, one
|
||||
node, the long-lived grants only.
|
||||
|
||||
## 2. The licences it manages today
|
||||
|
||||
Two subscription accounts and one API key. They differ in kind and the manager treats them so:
|
||||
|
||||
| kind | what the grant is | refresh | what a node is handed | how the agent uses it |
|
||||
|---|---|---|---|---|
|
||||
| `subscription` | an OAuth grant: an access token that lives hours and a refresh token that lives weeks | the manager rotates it, alone | the access token only | written into the agent's credentials file by the agent module, as the operator |
|
||||
| `api-key` | a key the operator obtained from the vendor | none; a new key is a new adoption | the key | served to the agent through its key-helper setting; nothing is written under the home |
|
||||
|
||||
## 3. Keeping a grant alive
|
||||
|
||||
Carried from the predecessor, where each rule was earned by an incident:
|
||||
|
||||
- **One rotation source.** Only this module calls the vendor's token endpoint. An OAuth refresh is
|
||||
presumed to rotate the refresh token, so a second refresher presenting the old one would kill the
|
||||
grant; whether that presumption holds is to be measured in the lab, and the design is safe either way.
|
||||
- **A lease per licence**, taken in the store before the row is read. A duplicate run sees the token its
|
||||
predecessor just wrote, finds hours of life on it, and does nothing.
|
||||
- **An expiry floor and a cadence.** Within an hour of expiry a refresh must happen; otherwise a grant is
|
||||
rotated once it is older than a declared setting, so a node that misses one rotation still holds hours
|
||||
of life and a broken refresh surfaces in minutes rather than the next morning.
|
||||
- **Failure is counted and escalated once.** Consecutive failures are recorded; past a threshold a
|
||||
notification is emitted, and at most once a day while it stays broken — the predecessor sent one alarm
|
||||
411 times in 35 hours and the incident went unnoticed inside its own alarm.
|
||||
- **A refresh token's own expiry is warned about three days ahead**, because the only remedy is a person
|
||||
logging in again.
|
||||
- **The vendor's reason is logged**, never only the status code: a malformed request and a revoked grant
|
||||
both answer 400, and the predecessor built three concurrency fixes for a bug that was a wrong client id.
|
||||
|
||||
## 4. Handing a token to a node
|
||||
|
||||
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 |
|
||||
|
||||
**One agent directory per machine, shared by every interactive session**, so a node's binding is the
|
||||
licence of all its sessions at once. A worker is a consumer of its own because it runs from a home of its
|
||||
own, with its own agent directory and credentials file, which the agent module on that node writes for
|
||||
it as it writes the operator's — the predecessor ran its agents exactly so.
|
||||
|
||||
**Binding is a person's act through the seat's verbs**, listed by the console: `bind`, `switch`,
|
||||
`release`. **Exhaustion is observed, not acted on**: usage is read every few minutes, a crossing of a
|
||||
declared threshold in the five-hour window is notified once per crossing, and moving a consumer is the
|
||||
operator's call. Switching remains a reaction, not a declaration
|
||||
([ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md)), and an automated policy — move to the
|
||||
least-used licence, stay off a dying one — is designed later if wanted, on the readings this module
|
||||
already keeps.
|
||||
|
||||
## 6. Adopting a grant
|
||||
|
||||
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 0183, one rotation source |
|
||||
| every event the manager emits is free of any token; the hand-over opens only with the receiving module's key | ADR 0183, to-be 32 §10 |
|
||||
| a worker bound to a dead licence is refused, never answered with another licence's token | ADR 0183, the fallbacks |
|
||||
| a grant offered with a mismatching identity is refused and one notification emitted | ADR 0183, attribution |
|
||||
| a failing licence notifies once, and once a day after, not once per tick | §3 |
|
||||
| the console lists the seat's verbs and `switch` changes a workstation's token end to end | ADR 0132, the exit of the build |
|
||||
|
||||
## What this does not settle
|
||||
|
||||
- An automated switch on exhaustion (§5).
|
||||
- Whether an OAuth refresh token is single-use; the lab measures it, and §3 holds either way.
|
||||
- How the mesh's own session and a worker read their token on a node once those exist
|
||||
([to-be 15](15-the-agent-session.md), [ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)):
|
||||
the agent module on that node is their local source, and the reading is theirs to design.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the decision
|
||||
- [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md) — the consumer on every node
|
||||
- [14 — Model access](14-model-access.md) — the vendor-blind provision this sits beside
|
||||
- the predecessor's `claude-licences` module: the lease, the floor, the cadence, the cooldown, the identity guard — read 2026-10-02
|
||||
Reference in New Issue
Block a user