From d227ed12d2daf22435aa78194ac9a1c6647db3e3 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 2 Oct 2026 17:26:48 +0200 Subject: [PATCH 1/6] To-be 40: building the operator's agent and its licence manager as work packages Designs 36 and 39 say what is built; this says in which order and what proves each step, in the shape to-be 38 gave the operator's machine. Seven packages: the operator states the facts (accounts, roles, licences); the console provides its endpoint; the licence manager and the agent module are built and unit-tested in parallel; the manager goes live on the control node; the agent on one workstation, with the switch and the predecessor's files removed as the proof of the whole; then the rest of the nodes and the retirement of the two catalogue modules built on the old placement. The live proofs wait for to-be 38's WP3, because both modules' tools run in the node's tool runtime (ADR 0175) and a per-module tool container would rebuild what that record retires. --- ...operators-agent-and-its-licence-manager.md | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md diff --git a/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md new file mode 100644 index 0000000..6a8bc6f --- /dev/null +++ b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md @@ -0,0 +1,226 @@ +--- +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/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/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md + - 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md + - 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md +--- + +# 40. Building the operator's agent and its licence manager + +**The work of [design 36](36-the-operators-agent-on-a-machine.md) and [design 39](39-the-anthropic-licence-manager.md), +broken into packages that each end at something a person can see run, in the order their +dependencies allow.** The two designs are the authority on *what* is built; this document holds the +packages, their order, their sizes and their proofs, and is wrong the moment it disagrees with them. +It is the shape [design 38](38-building-the-operators-machine.md) gave the operator's machine, applied +to the two modules that make its agent work. + +## How this is built, and where it is run + +**On the live mesh, by the operator's decision** ([ADR 0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md), +and design 38's words on the same day). Every package is written with unit tests, committed on one +branch per repository ([playbook 07](../../00-META/process/07-feature-branches.md)), and proven on the +machines: the control node first for the manager, one workstation first for the agent, then the rest. +The cost accepted: a broken agent module leaves a workstation's agent without the mesh's instructions +or with a stale token until the next push; the person's own files under the home are never in reach of +the failure, by [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md). + +Each package names what proves it. A package that cannot name its proof is divided until it can. + +## What exists already, measured + +Counted 2026-10-02 in the repositories and on the machines. Nothing here is new ground; every package +reshapes something standing. + +| Piece | Today | Becomes | +|---|---|---| +| the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalogue: a token-endpoint client, a sealed-box primitive, adoption of a grant sealed to a node's key; assigned to nothing | the manager's refresh and adoption, with the lease, the floor and the cadence the predecessor's manager had | +| the credentials write, the refresh-token strip, the identity read | `anthropic-consumer` in the catalogue: tested; assigned to nothing | the agent module's write, unchanged in shape | +| the predecessor's manager and consumer | two modules in the retired system: the lease per licence, the expiry floor, the lineage comparison, the identity guard, three touchpoints with fallbacks, cooldowns on alarms | ported as logic with its tests; nothing of its registry or its bus | +| the host's `process` and `archive` shapes | fetch a bundle by digest and run it supervised; fetch and unpack an artifact | **unchanged** — the manager's daemon is one process; both modules' tools are bundles | +| the manifest's `uses`, `claims`, `invokes seat:.`, node-scoped `provides` with `serves: {port}` | all four exist and are used by other modules | **unchanged** — the agent uses the seat and invokes its verbs; the console provides its endpoint | +| the console | a container per node, MCP on loopback, no provision | gains one provision; becomes the node-tools runtime's serving mode under design 38's WP3 | +| the agent's package | present on both workstations from a build the predecessor's helper made; the distribution's repositories do not carry it | declared; satisfied where present, refused where not, until a package repository seat exists | +| the operator account | a column on every node record, **empty on all four** | stated by the operator, before anything home-scoped lands | + +**One dependency decides the order.** Both modules serve tools and the agent module's tools write +under `/etc` and, as the operator, under the home. Under [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) +tools run in the node's tool runtime, host-side, which design 38 builds in its WP1–WP3. Writing a +per-module tool container for these two modules would be building the pattern that record retires, so +**the live proofs of WP3 to WP5 below wait for design 38's WP3.** Everything before a live proof — +manifests, code, tests — does not, and is written now. + +## The order the work allows + +``` +WP0 the operator states the facts (the live mesh) ── accounts, roles, licences to adopt +WP1 the console provides its endpoint (mesh-catalog) ── small, independent +WP2 the licence manager, built and tested (mesh-catalog) ──┐ independent of each other; +WP3 the agent module, built and tested (mesh-catalog) ──┘ both wait on design 38 WP3 to run + │ +WP4 the manager live on the control node (the live mesh) ── three licences adopted, a refresh seen +WP5 the agent live on one workstation (the live mesh) ── the hand-over, the switch, the instructions +WP6 the rest of the nodes, and the predecessor's remains ── adoption from a login, the retirements +``` + +WP1, WP2 and WP3 touch different directories of one repository and meet only at the seat's name and +the provision's name; they are built in parallel. WP4 is the first time anything on a machine changes. +WP5 is the proof of the whole. + +## WP0 — The operator states the facts + +*The live mesh. An hour, and it is the operator's.* + +The account on each node record, through the controller's node command — none is stated today, and +[ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) +refuses a home-scoped module without one. The role of each node, as the agent module's setting on the +node layer, once the module exists. Which three licences exist and what each is called. + +**Proof.** The controller's node command lists an account for every node. + +## WP1 — The console provides its endpoint + +*mesh-catalog. Half a day.* + +**What changes.** The console's manifest gains a node-scoped provision — working name +`console-endpoint`, fixed when the manifest is written — serving the port the machine gave it, as the +local model server already does for its API. Co-location resolves it +([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md), +[to-be 34](34-the-console.md) as amended). When design 38's WP3 moves the console into the node-tools +module, the provision moves with it; it is a line in a manifest either way. + +**Proof.** The controller's plan for a workstation shows a consumer of the provision bound to the +console's port; the same consumer on a machine without the console is refused naming the provision; +the catalogue's tests pass. + +## WP2 — The licence manager, built and tested + +*mesh-catalog. Two to three days; the largest package.* + +**What is written**, as design 39 says: + +1. **The manifest.** Claims the mesh-scoped seat `anthropic-licence-manager` with its verbs; requires a + database, a `secret` for the key its grants are encrypted with, and the bus; a `bundle` of tools; a + `process` for the daemon that refreshes, collects usage and notifies, on a schedule; declared + settings for the cadence, the usage threshold and the cooldown, each with a default; `invokes` the + agent module's `apply`. +2. **The store.** Migrations for licences, bindings, usage and audit, with the lease and the + notification slot as columns, numbered and idempotent. +3. **The refresh.** The token-endpoint client and the sealed box from `anthropic-manager`; the plan + (floor, cadence, forced, cannot) and the lease from the predecessor, as pure functions with their + tests; the vendor's reason logged on failure; counted failures, one notification per cooldown. +4. **Adoption.** From a file on the manager's node for the API key; from a sealed grant a node offers; + the identity guard that refuses a mismatch and notifies. +5. **Usage.** The vendor's reading per licence on a schedule, stored raw and normalised + ([ADR 0054](../../02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md)), one notification + per threshold crossing. +6. **The verbs**: `licences`, `bindings`, `bind`, `switch`, `release`, `refresh`, `usage`, `adopt`, + `register`, `current` — the last answering a consumer's token sealed to the key that consumer + registered. +7. **The hand-over**: on rotation or switch, one call to `claude-code.apply@` per bound node, + the token sealed to that node's key, the answer recorded. + +**Proof, before anything runs live.** Unit tests: two refresh runs started together rotate one grant +once; a grant with a mismatching identity is refused; a worker bound to a dead licence is refused and +never lent another; every event the daemon emits is free of a token; the hand-over payload opens only +with the registered key. The catalogue's checks: no installation named, no secret in a declared file. + +## WP3 — The agent module, built and tested + +*mesh-catalog. Two days.* + +**What is written**, as design 36 says: + +1. **The manifest.** The agent's package; the state directory; the facts file carrying the node's + name, the operator account and its home, the console's bound port, the role and the extra tool + servers from settings; requires the console's endpoint and the bus; `uses` the seat and `invokes` + its `register`, `current` and `adopt`; a `bundle` of tools. **No file resource under a home or + under `/etc`.** +2. **The renderer.** From the facts file and the current binding, the managed settings file (the tool + servers under the entry `mesh`, the attribution trailers, and the key-helper for an API-key binding) + and the managed instruction file (§3 of design 36), written under the agent's managed directory + with the escalation the tool performs for itself; idempotent; re-run when the facts file changes. +3. **The keypair**, made once in the state directory, the public half registered with the seat at + start and at every start. +4. **The consumer side**: `apply` (a rotation applied only if newer within one lineage, a switch applied + regardless, the answer naming the outcome and never a token); the pull at start and near expiry; + the credentials write as the operator, access-token-only, from `anthropic-consumer` with its tests; + the key-helper program for the API key; the offer of a login to the seat, sealed, after reading the + account's identity. +5. **The tools**: `apply`, `licence_status`, `mcp_configure` (validates a server, sets the module's + setting through the controller's settings verb), `render` (re-render now, for a person). +6. **The documentation**: the six predecessor files and the hand-made console entry a person removes + on a workstation that carried the predecessor. + +**Proof, before anything runs live.** Unit tests: the renderer writes only the mesh's keys and leaves +every other key of a seeded settings file; the credentials write strips a refresh token and is atomic; +the lineage comparison from the predecessor, with its cases; a login offer carries the identity it read. +The catalogue's checks pass. + +## WP4 — The manager live on the control node + +*The live mesh. Half a day, after design 38's WP3.* + +**Order.** Assign the manager on the control node; push. Adopt the API key from a file there. Adopt +the two subscription grants: a login in a throwaway home on the control node, offered to the seat the +way a node's agent module will. Bind each node's agent to a licence. + +**Proof.** Through the console: `anthropic-licence-manager.licences` lists three licences with +identity and expiry; within the cadence, the audit shows a rotation and `licences` shows a later +expiry; a forced `refresh` on one licence is logged with the vendor's answer; the two retired catalogue +modules are still assigned to nothing. + +## WP5 — The agent live on one workstation + +*The live mesh. Half a day. The proof of the whole.* + +**Order.** Set the workstation's role in the module's settings. Record the checksums of everything +under the person's agent directory. Assign the module; push. Remove the six predecessor files and the +hand-made console entry. Start a new session. + +**Proof.** The managed directory holds the settings and instruction files, owned by root. Everything +under the person's agent directory is byte-identical to before except the credentials file, which is +owned by the operator, readable by nobody else, and names no refresh token. The new session lists the +mesh's tools under `mesh` once, answers *which node am I* from the instruction file, and makes a model +request. `anthropic-licence-manager.switch` to the second subscription licence changes the token on +the workstation within a minute, and neither the verb's answer nor either module's log holds a token. +Switched to the API-key licence, the credentials file is left as it was and the agent authenticates +through the key-helper. Switched back. + +## WP6 — The rest of the nodes, and the predecessor's remains + +*The live mesh and mesh-catalog. One day.* + +**Order.** Assign the module on the second workstation and on the servers whose account is stated; +remove the predecessor's files on the second workstation. Log in on a workstation under a licence's +account and watch the offer be adopted — and under the wrong account, and watch it refused and +notified. Retire `anthropic-manager` and `anthropic-consumer` from the catalogue. Set designs 36 and +39 to `implemented` for what runs, with the as-is written +([playbook 02](../../00-META/process/02-graduation.md)). + +**Proof.** Every node with an account runs the module and `licence_status` answers on each; the +refused login's notification arrived; the catalogue has no module built on the old placement. + +## What is deliberately not here + +- **The package repository seat** for a distribution that does not carry the agent's package + (design 36 §7). A fresh node refuses the module in the package manager's words until it exists. +- **An automated switch on exhaustion.** The readings are kept from WP4; the policy is a later record. +- **Workers and the mesh's own sessions as consumers.** The manager's bindings and fallbacks know them + from WP2; the consumers themselves do not exist yet ([to-be 15](15-the-agent-session.md), + [ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)). +- **Whether a refresh token is single-use.** WP4 may measure it on a licence deliberately refreshed + twice; the design holds either way. + +## How this list is kept true + +Each package's proof is run when the package is finished and its line here gains the date and the +commit, as design 38 does. A package whose proof fails is not reworded; the failure is recorded under +it and the package stays open. When WP5 is proven, designs 36 and 39 move to `in-progress` with their +owning repository, and when WP6 is proven to `implemented`, with the as-is written. From 2eba399e1e8094901001c302d914c82c120ceed2 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 16:01:37 +0200 Subject: [PATCH 2/6] To-be 40 revised for the tools refactor; the manager starts every exchange (ADR 0183 dated note, designs 36 and 39) Design 38's WP1-WP4b ran: the node's tool runtime is live on all four machines as the operator account, tools are bundles given only their declared words, and a bundle has no bus credential. So the wait on design 38 WP3 is over, the agent module calls nothing and the manager starts every exchange (key, hand-over, waiting login, reconcile), and the manager's daemon now waits on WP4c's record instead. Accounts are stated on all four, sudo -n works for each, the agent is installed on all four; the plan's WP0 shrinks and WP2 gets a configuration-only live proof before any licence. --- ...-hands-tokens-to-the-agent-over-the-bus.md | 13 + .../36-the-operators-agent-on-a-machine.md | 34 +- .../39-the-anthropic-licence-manager.md | 24 +- ...operators-agent-and-its-licence-manager.md | 290 ++++++++---------- 4 files changed, 182 insertions(+), 179 deletions(-) diff --git a/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md b/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md index 64cd620..63fff50 100644 --- a/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md +++ b/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md @@ -152,6 +152,19 @@ the node is bound to, and refuses with a notification otherwise. | An unservable binding refuses rather than lends | a manager test: a worker bound to a dead licence is answered with a refusal, never another licence's token | | A switch through the console changes the token on the node and nothing in the answer is a token | a live check on one workstation | +> **The mechanism changed — 2026-10-03, by [ADR 0192](0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md).** +> What stands: one manager holding the seat, one rotation source, a token sealed to the receiving +> module's key on request/reply and never an event, the agent module alone writing what the agent +> reads, the identity guard, the host knowing nothing. What moved: the agent module's code is now a +> tools bundle the node's runtime serves ([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), +> [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)), +> and a bundle has no bus credential of its own and answers calls rather than making them (ADR 0192's +> consequences). So **the manager starts every exchange**: it asks each bound node's agent module for +> its public key, hands it a token, asks it for a login waiting to be adopted, and reconciles every node +> on a schedule — which is what "the agent module asks the seat for its current token" and "offers the +> grant to the manager" in the decision above now mean in practice. The manager's own process needs a +> bus credential to make those calls, which is the question design 38's WP4c leaves to a record. + ## References - [ADR 0024](0024-model-access-is-a-provision.md), [ADR 0050](0050-model-access-is-vendor-agnostic.md) — the licence as a named thing, the carve-out this moves with the manager diff --git a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md index 00dea1c..fcb278e 100644 --- a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md +++ b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md @@ -2,7 +2,7 @@ layer: to-be status: designed code: [] -updated: 2026-10-02 +updated: 2026-10-03 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 @@ -63,8 +63,9 @@ lists them, and until they go the agent reads stale instructions beside the mesh ## 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. +in that directory carrying the node's name and the console's endpoint, and a settings file carrying the +role and the extra tool servers, merged from the module's settings layers — the bundle is told the two +files' paths, because a bundle's words are paths and constants only (ADR 0192); 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 @@ -119,9 +120,9 @@ resolves it; a machine without the console refuses the module by name. [To-be 34 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 +— mesh layer or node layer — rendered into the same managed key. The person sets them with the +controller's `settings` verb on this module, so the list stays declared state; a tool of this module +cannot set it, because a bundle calls nothing (ADR 0192). 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 @@ -133,29 +134,34 @@ it is the person's to remove, and until then the agent sees the mesh's tools twi [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; +- **makes a keypair** in its state the first time it runs, and answers `claude_code_public_key` with the + public half when the manager asks; - **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; +- **is reconciled, never pulls**: the manager asks every bound node on a schedule and after every + rotation, so a node that was away receives its token when it is back; between visits it keeps the last + token, and `licence_status` says how long it has left; - **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; +- **holds a login for the manager to collect**: when the credentials file holds a full grant it did not + write — a person logged in — it answers `claude_code_pending_login`, when the manager asks, with the + grant sealed to the key the manager gives in its request and the account's identity read from the + agent's state file; the manager decides, and the next hand-over strips the refresh token; - **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. +handed. *2026-10-03:* every exchange is started by the manager, because a tools bundle answers calls and +has no bus credential to make them ([ADR 0192](../../02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md), +ADR 0183's dated note). The tool names follow the catalogue's `_` form. ## 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:** +All four nodes carry one since 2026-10-03. **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 diff --git a/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md b/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md index d7c42f0..9347d85 100644 --- a/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md +++ b/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md @@ -2,7 +2,7 @@ layer: to-be status: designed code: [] -updated: 2026-10-02 +updated: 2026-10-03 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 @@ -74,8 +74,9 @@ Carried from the predecessor, where each rule was earned by an incident: ## 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: +**The manager starts every exchange** (ADR 0183's dated note of 2026-10-03): the agent module is a +tools bundle, which answers and calls nothing. The manager asks each bound node's module for its public +key the first time and keeps it. From then on: - **On rotation**, the manager calls `claude-code.apply@` on every node bound to the rotated licence, with the new token sealed to that node's module key. The module answers *applied*, or @@ -83,11 +84,12 @@ runs. From then on: - **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. +- **On a schedule**, every few minutes, the manager visits each bound node: a node whose token is near + expiry, or that did not answer last time, is handed its current token. A node that was away is + served when it is back, with nothing for it to ask. - **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 +A node whose module does not answer for its 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. @@ -119,9 +121,9 @@ already keeps. 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 +- **From a node's login.** A person logs in on a node, as they always have. On its next visit the manager + asks that node's module for a waiting login, giving its own public key; the module answers with the + full grant sealed to it and the account's identity read from the agent's own state file. 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. @@ -135,8 +137,8 @@ argument: **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). +or all), `usage` (current and history), `adopt`, and `visit` (reconcile one node now). A node's key and +a consumer's token are not seat verbs: the manager asks the node, by the agent module's own tools. ## 8. Settings diff --git a/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md index 6a8bc6f..e2d8d64 100644 --- a/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md +++ b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md @@ -2,12 +2,14 @@ layer: to-be status: designed code: [] -updated: 2026-10-02 +updated: 2026-10-03 decisions: - 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md - 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/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md + - 02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md + - 02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md - 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md - 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md --- @@ -18,209 +20,189 @@ decisions: broken into packages that each end at something a person can see run, in the order their dependencies allow.** The two designs are the authority on *what* is built; this document holds the packages, their order, their sizes and their proofs, and is wrong the moment it disagrees with them. -It is the shape [design 38](38-building-the-operators-machine.md) gave the operator's machine, applied -to the two modules that make its agent work. +It is the shape [design 38](38-building-the-operators-machine.md) gives the operator's machine. + +*Revised 2026-10-03, after design 38's WP1–WP4b ran:* the node's tool runtime is live on all four +machines, tools are bundles it serves and each is given only the words its artifact declares, and a +bundle has no bus credential of its own. Three things in the first version of this plan changed with +that: the wait on design 38's WP3 is over; the agent module calls nothing, so the manager starts every +exchange (ADR 0183's dated note); and the manager's own process now waits on a different question, +named under WP4. ## How this is built, and where it is run -**On the live mesh, by the operator's decision** ([ADR 0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md), -and design 38's words on the same day). Every package is written with unit tests, committed on one -branch per repository ([playbook 07](../../00-META/process/07-feature-branches.md)), and proven on the -machines: the control node first for the manager, one workstation first for the agent, then the rest. -The cost accepted: a broken agent module leaves a workstation's agent without the mesh's instructions -or with a stale token until the next push; the person's own files under the home are never in reach of -the failure, by [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md). +**On the live mesh, by the operator's decision** ([ADR 0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md)). +Every package is written with unit tests, committed on one branch per repository +([playbook 07](../../00-META/process/07-feature-branches.md)), and proven on the machines: one +workstation first for the agent, the control node first for the manager, then the rest. A broken agent +module leaves a workstation's agent without the mesh's instructions or with a stale token until the next +push; the person's own files under the home are out of the failure's reach, by +[ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md). Each package names what proves it. A package that cannot name its proof is divided until it can. ## What exists already, measured -Counted 2026-10-02 in the repositories and on the machines. Nothing here is new ground; every package -reshapes something standing. +Measured 2026-10-03 on the four machines and in the repositories. | Piece | Today | Becomes | |---|---|---| -| the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalogue: a token-endpoint client, a sealed-box primitive, adoption of a grant sealed to a node's key; assigned to nothing | the manager's refresh and adoption, with the lease, the floor and the cadence the predecessor's manager had | -| the credentials write, the refresh-token strip, the identity read | `anthropic-consumer` in the catalogue: tested; assigned to nothing | the agent module's write, unchanged in shape | -| the predecessor's manager and consumer | two modules in the retired system: the lease per licence, the expiry floor, the lineage comparison, the identity guard, three touchpoints with fallbacks, cooldowns on alarms | ported as logic with its tests; nothing of its registry or its bus | -| the host's `process` and `archive` shapes | fetch a bundle by digest and run it supervised; fetch and unpack an artifact | **unchanged** — the manager's daemon is one process; both modules' tools are bundles | -| the manifest's `uses`, `claims`, `invokes seat:.`, node-scoped `provides` with `serves: {port}` | all four exist and are used by other modules | **unchanged** — the agent uses the seat and invokes its verbs; the console provides its endpoint | -| the console | a container per node, MCP on loopback, no provision | gains one provision; becomes the node-tools runtime's serving mode under design 38's WP3 | -| the agent's package | present on both workstations from a build the predecessor's helper made; the distribution's repositories do not carry it | declared; satisfied where present, refused where not, until a package repository seat exists | -| the operator account | a column on every node record, **empty on all four** | stated by the operator, before anything home-scoped lands | - -**One dependency decides the order.** Both modules serve tools and the agent module's tools write -under `/etc` and, as the operator, under the home. Under [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) -tools run in the node's tool runtime, host-side, which design 38 builds in its WP1–WP3. Writing a -per-module tool container for these two modules would be building the pattern that record retires, so -**the live proofs of WP3 to WP5 below wait for design 38's WP3.** Everything before a live proof — -manifests, code, tests — does not, and is written now. +| the node's tool runtime | live on all four, a host process; **runs as the operator account**; listens for the console on loopback at a port its own code fixes; launches or imports every assigned module's tools bundle and hands each its declared words | serves the agent module's tools; gains one provision for its endpoint (WP1) | +| the operator account | **stated on all four** — the runtime runs as it | read by the agent module from the runtime's own words | +| escalation | passwordless `sudo` for the operator account on all four — a fact about the machines, checked by nobody | how the agent module writes its managed directory under `/etc` | +| the agent itself | installed on all four, at four different versions, all above the one the managed tool-server key needs | declared as the module's package | +| a bundle's words | paths and constants written with `${dir:…}` and `${port:…}` only; a fact the mesh knows reaches a bundle as a file whose path is a word | the agent module's facts file and settings file | +| a bundle calling a tool | **not possible**: a bundle answers calls; it holds no bus credential | the manager starts every exchange | +| a module's own long-running process with a bus credential | **undecided** — design 38's WP4c names it as the question its next record answers | the manager's daemon (WP4) | +| the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalogue, built on the controller placement ADR 0183 moved away from; assigned to nothing | its client ported into the manager; the module retired (WP6) | +| the credentials write, the strip, the identity read | `anthropic-consumer` in the catalogue; tested; assigned to nothing | ported into the agent module with its tests; the module retired (WP6) | +| the predecessor's manager and consumer | the lease per licence, the expiry floor, the lineage comparison, the identity guard, three touchpoints, cooldowns | ported as logic with its tests | ## The order the work allows ``` -WP0 the operator states the facts (the live mesh) ── accounts, roles, licences to adopt -WP1 the console provides its endpoint (mesh-catalog) ── small, independent -WP2 the licence manager, built and tested (mesh-catalog) ──┐ independent of each other; -WP3 the agent module, built and tested (mesh-catalog) ──┘ both wait on design 38 WP3 to run - │ -WP4 the manager live on the control node (the live mesh) ── three licences adopted, a refresh seen -WP5 the agent live on one workstation (the live mesh) ── the hand-over, the switch, the instructions -WP6 the rest of the nodes, and the predecessor's remains ── adoption from a login, the retirements +WP0 the operator names the licences and each node's role (the live mesh) ── an hour +WP1 the runtime provides its endpoint (mesh-tools) ── small +WP2 the agent module (mesh-catalog) ──┐ WP2 needs WP1; +WP3 the manager's code, built and tested (mesh-catalog) ──┘ WP3 is independent + │ +WP2 live: one workstation, configuration only — no licence yet ── the first live proof + │ +WP4 the manager live on the control node ── waits on design 38 WP4c's record (a process's bus credential) +WP5 the licence end to end on one workstation +WP6 the rest of the nodes, and the predecessor's remains ``` -WP1, WP2 and WP3 touch different directories of one repository and meet only at the seat's name and -the provision's name; they are built in parallel. WP4 is the first time anything on a machine changes. -WP5 is the proof of the whole. +## WP0 — The operator names the licences and each node's role -## WP0 — The operator states the facts +*The live mesh. An hour, and it is the operator's.* The accounts are stated already. What remains: the +names of the two subscription licences and the API key; each node's role, as the agent module's setting +on the node layer once the module is registered. -*The live mesh. An hour, and it is the operator's.* +**Proof.** The module's settings list a role for every node; the licences have names. -The account on each node record, through the controller's node command — none is stated today, and -[ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) -refuses a home-scoped module without one. The role of each node, as the agent module's setting on the -node layer, once the module exists. Which three licences exist and what each is called. +## WP1 — The runtime provides its endpoint -**Proof.** The controller's node command lists an account for every node. +*mesh-tools. An hour.* -## WP1 — The console provides its endpoint +**What changes.** The `node-tools` manifest provides a node-scoped provision, `mcp-endpoint`, serving +the port its code listens on, the way the local model server serves its API +([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)). Co-location +resolves it. The runtime's port stays what its code fixes; assigning it is +[issue 192](../../04-ISSUES/192-the-meshs-tools-reach-a-person-only-by-a-registration-made-by-hand/00-report.md)'s +second question and not this package's. -*mesh-catalog. Half a day.* +**Proof.** The plan for a workstation carrying a consumer of `mcp-endpoint` shows it bound to the +runtime's port; the controller's tests and the catalogue's checks pass. -**What changes.** The console's manifest gains a node-scoped provision — working name -`console-endpoint`, fixed when the manifest is written — serving the port the machine gave it, as the -local model server already does for its API. Co-location resolves it -([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md), -[to-be 34](34-the-console.md) as amended). When design 38's WP3 moves the console into the node-tools -module, the provision moves with it; it is a line in a manifest either way. +## WP2 — The agent module -**Proof.** The controller's plan for a workstation shows a consumer of the provision bound to the -console's port; the same consumer on a machine without the console is refused naming the provision; -the catalogue's tests pass. - -## WP2 — The licence manager, built and tested - -*mesh-catalog. Two to three days; the largest package.* - -**What is written**, as design 39 says: - -1. **The manifest.** Claims the mesh-scoped seat `anthropic-licence-manager` with its verbs; requires a - database, a `secret` for the key its grants are encrypted with, and the bus; a `bundle` of tools; a - `process` for the daemon that refreshes, collects usage and notifies, on a schedule; declared - settings for the cadence, the usage threshold and the cooldown, each with a default; `invokes` the - agent module's `apply`. -2. **The store.** Migrations for licences, bindings, usage and audit, with the lease and the - notification slot as columns, numbered and idempotent. -3. **The refresh.** The token-endpoint client and the sealed box from `anthropic-manager`; the plan - (floor, cadence, forced, cannot) and the lease from the predecessor, as pure functions with their - tests; the vendor's reason logged on failure; counted failures, one notification per cooldown. -4. **Adoption.** From a file on the manager's node for the API key; from a sealed grant a node offers; - the identity guard that refuses a mismatch and notifies. -5. **Usage.** The vendor's reading per licence on a schedule, stored raw and normalised - ([ADR 0054](../../02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md)), one notification - per threshold crossing. -6. **The verbs**: `licences`, `bindings`, `bind`, `switch`, `release`, `refresh`, `usage`, `adopt`, - `register`, `current` — the last answering a consumer's token sealed to the key that consumer - registered. -7. **The hand-over**: on rotation or switch, one call to `claude-code.apply@` per bound node, - the token sealed to that node's key, the answer recorded. - -**Proof, before anything runs live.** Unit tests: two refresh runs started together rotate one grant -once; a grant with a mismatching identity is refused; a worker bound to a dead licence is refused and -never lent another; every event the daemon emits is free of a token; the hand-over payload opens only -with the registered key. The catalogue's checks: no installation named, no secret in a declared file. - -## WP3 — The agent module, built and tested - -*mesh-catalog. Two days.* +*mesh-catalog. A day and a half.* **What is written**, as design 36 says: -1. **The manifest.** The agent's package; the state directory; the facts file carrying the node's - name, the operator account and its home, the console's bound port, the role and the extra tool - servers from settings; requires the console's endpoint and the bus; `uses` the seat and `invokes` - its `register`, `current` and `adopt`; a `bundle` of tools. **No file resource under a home or +1. **The manifest.** The agent's package. A state directory. A facts file in it, rendered by the mesh: + the node's name and the console's address from `mcp-endpoint`. A settings file in it, merged from + the module's settings layers: the node's role and the extra tool servers. A tools bundle whose + words name the two files, the state directory and nothing else. **No file resource under a home or under `/etc`.** -2. **The renderer.** From the facts file and the current binding, the managed settings file (the tool - servers under the entry `mesh`, the attribution trailers, and the key-helper for an API-key binding) - and the managed instruction file (§3 of design 36), written under the agent's managed directory - with the escalation the tool performs for itself; idempotent; re-run when the facts file changes. -3. **The keypair**, made once in the state directory, the public half registered with the seat at - start and at every start. -4. **The consumer side**: `apply` (a rotation applied only if newer within one lineage, a switch applied - regardless, the answer naming the outcome and never a token); the pull at start and near expiry; - the credentials write as the operator, access-token-only, from `anthropic-consumer` with its tests; - the key-helper program for the API key; the offer of a login to the seat, sealed, after reading the - account's identity. -5. **The tools**: `apply`, `licence_status`, `mcp_configure` (validates a server, sets the module's - setting through the controller's settings verb), `render` (re-render now, for a person). -6. **The documentation**: the six predecessor files and the hand-made console entry a person removes - on a workstation that carried the predecessor. +2. **The renderer**, run whenever the runtime collects the module's tools: from the two files, the + managed settings file (the tool servers under the entry `mesh`, the attribution trailers, the + key-helper for an API-key binding) and the managed instruction file, written under the agent's + managed directory through the account's escalation, only when their content changed. +3. **The keypair**, made once in the state directory; X25519 and an authenticated cipher from the + language's own library, so the bundle carries no dependency. +4. **The tools**: `claude_code_status` (what is rendered, what licence is held, when its token expires, + fingerprints only); `claude_code_render` (render now); `claude_code_public_key`; + `claude_code_apply` (a sealed token, applied only if newer within one lineage unless it is a switch; + the credentials write as the operator, access-token-only, atomic; the key-helper program for an API + key); `claude_code_pending_login` (a full grant found in the credentials file, sealed to the key the + caller gives, with the account's identity). +5. **The documentation**: the six predecessor files and the hand-made console entry a person removes. -**Proof, before anything runs live.** Unit tests: the renderer writes only the mesh's keys and leaves -every other key of a seeded settings file; the credentials write strips a refresh token and is atomic; -the lineage comparison from the predecessor, with its cases; a login offer carries the identity it read. -The catalogue's checks pass. +**Proof, before anything runs live.** Unit tests: the renderer writes the mesh's keys and nothing else; +it writes nothing when nothing changed; the credentials write strips a refresh token and is atomic; the +lineage cases from the predecessor; a sealed hand-over opens only with the module's key; no tool's answer +contains a token. The catalogue's checks pass. + +**Proof, live, on one workstation, configuration only.** Assign the module; set the node's role; push. +The agent's managed directory holds the two files; everything under the person's agent directory is +byte-identical to before; a new session lists the mesh's tools under `mesh` and answers *which node am +I* from the managed instruction file. `claude_code_status` answers through the console. No licence is +touched: the module writes the credentials file only when it is handed a token. + +## WP3 — The manager's code, built and tested + +*mesh-catalog. Two to three days.* + +**What is written**, as design 39 says: the manifest (the seat and its verbs, a database, a `secret` +for the key the grants are encrypted with, a tools bundle, a process bundle for the daemon, settings +with defaults); the store's migrations; the refresh with its plan, lease, floor and cadence as pure +functions; the vendor client from `anthropic-manager`; adoption from a file and from a node's waiting +login with the identity guard; usage and its threshold; the visit — key, hand-over, waiting login — per +bound node; the seat's verbs. + +**Proof, before anything runs live.** Unit tests: two refresh runs started together rotate one grant +once; a mismatching identity is refused; a worker bound to a dead licence is refused and never lent +another; nothing the daemon emits carries a token; a hand-over sealed for one node opens with no other +node's key. ## WP4 — The manager live on the control node -*The live mesh. Half a day, after design 38's WP3.* +*The live mesh. Half a day.* **Waits on design 38 WP4c's record** — how a module's own long-running +process is given a bus credential and its subscriptions — because the daemon must call the agent module +on every node. Until that record exists, nothing in this plan works around it: no tool container, no +credential copied by hand. -**Order.** Assign the manager on the control node; push. Adopt the API key from a file there. Adopt -the two subscription grants: a login in a throwaway home on the control node, offered to the seat the -way a node's agent module will. Bind each node's agent to a licence. +**Order.** Assign the manager on the control node; push. Adopt the API key from a file there. Adopt the +two subscription grants: a login on a workstation carrying the agent module, collected by the manager's +visit. Bind each node's agent to a licence. -**Proof.** Through the console: `anthropic-licence-manager.licences` lists three licences with -identity and expiry; within the cadence, the audit shows a rotation and `licences` shows a later -expiry; a forced `refresh` on one licence is logged with the vendor's answer; the two retired catalogue -modules are still assigned to nothing. +**Proof.** Through the console: `anthropic-licence-manager.licences` lists three licences with identity +and expiry; within the cadence the audit shows a rotation and a later expiry; a forced `refresh` is +logged with the vendor's answer. -## WP5 — The agent live on one workstation +## WP5 — The licence end to end on one workstation *The live mesh. Half a day. The proof of the whole.* -**Order.** Set the workstation's role in the module's settings. Record the checksums of everything -under the person's agent directory. Assign the module; push. Remove the six predecessor files and the -hand-made console entry. Start a new session. +**Order.** Record the checksums under the person's agent directory. Bind the workstation to a +subscription licence. Remove the six predecessor files and the hand-made console entry. Start a session. -**Proof.** The managed directory holds the settings and instruction files, owned by root. Everything -under the person's agent directory is byte-identical to before except the credentials file, which is -owned by the operator, readable by nobody else, and names no refresh token. The new session lists the -mesh's tools under `mesh` once, answers *which node am I* from the instruction file, and makes a model -request. `anthropic-licence-manager.switch` to the second subscription licence changes the token on -the workstation within a minute, and neither the verb's answer nor either module's log holds a token. -Switched to the API-key licence, the credentials file is left as it was and the agent authenticates -through the key-helper. Switched back. +**Proof.** Everything under the person's agent directory is byte-identical but the credentials file, +which is owned by the operator, readable by nobody else, and names no refresh token. A session makes a +model request. `switch` to the second subscription licence changes the token on the workstation within a +minute, and neither the verb's answer nor either module's log holds a token. Switched to the API key, the +credentials file is left as it was and the agent authenticates through the key-helper. Switched back. ## WP6 — The rest of the nodes, and the predecessor's remains -*The live mesh and mesh-catalog. One day.* - -**Order.** Assign the module on the second workstation and on the servers whose account is stated; -remove the predecessor's files on the second workstation. Log in on a workstation under a licence's -account and watch the offer be adopted — and under the wrong account, and watch it refused and -notified. Retire `anthropic-manager` and `anthropic-consumer` from the catalogue. Set designs 36 and -39 to `implemented` for what runs, with the as-is written +*The live mesh and mesh-catalog. One day.* The module on every node, the predecessor's files removed on +the second workstation; a login under a licence's account collected and adopted, and one under the wrong +account refused and notified; `anthropic-manager` and `anthropic-consumer` retired from the catalogue; +designs 36 and 39 set to `implemented` with the as-is written ([playbook 02](../../00-META/process/02-graduation.md)). -**Proof.** Every node with an account runs the module and `licence_status` answers on each; the -refused login's notification arrived; the catalogue has no module built on the old placement. +**Proof.** `claude_code_status` answers on every node; the refusal's notification arrived; the catalogue +has no module built on the old placement. ## What is deliberately not here -- **The package repository seat** for a distribution that does not carry the agent's package - (design 36 §7). A fresh node refuses the module in the package manager's words until it exists. +- **The package repository seat** for a distribution that does not carry the agent's package (design 36 + §7). The four machines have the package; a fifth would refuse the module in its package manager's + words. +- **Escalation as a checked fact.** The agent module's write under `/etc` relies on the operator + account's passwordless `sudo`, true on all four and checked by nothing. A machine without it refuses + the render in the tool's own words; making escalation a reported capability is design 38's to decide. - **An automated switch on exhaustion.** The readings are kept from WP4; the policy is a later record. -- **Workers and the mesh's own sessions as consumers.** The manager's bindings and fallbacks know them - from WP2; the consumers themselves do not exist yet ([to-be 15](15-the-agent-session.md), - [ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)). -- **Whether a refresh token is single-use.** WP4 may measure it on a licence deliberately refreshed - twice; the design holds either way. +- **Workers and the mesh's own sessions as consumers.** The manager knows them from WP3; the consumers do + not exist yet ([to-be 15](15-the-agent-session.md)). +- **Whether a refresh token is single-use.** WP4 may measure it; the design holds either way. ## How this list is kept true Each package's proof is run when the package is finished and its line here gains the date and the -commit, as design 38 does. A package whose proof fails is not reworded; the failure is recorded under -it and the package stays open. When WP5 is proven, designs 36 and 39 move to `in-progress` with their -owning repository, and when WP6 is proven to `implemented`, with the as-is written. +commit, as design 38 does. A package whose proof fails is not reworded; the failure is recorded under it +and the package stays open. When WP2's live proof runs, design 36 moves to `in-progress` with its owning +repository; when WP5's does, design 39 does too; and when WP6's does, both move to `implemented`, with +the as-is written. From bcf010886d124c072d17b617df3f5b879bab3cca Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 16:13:21 +0200 Subject: [PATCH 3/6] =?UTF-8?q?Design=2036=20=C2=A74:=20the=20console=20is?= =?UTF-8?q?=20registered=20in=20the=20exclusive=20managed=20tool-server=20?= =?UTF-8?q?file,=20because=20the=20managed-settings=20key=20refuses=20a=20?= =?UTF-8?q?non-https=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../36-the-operators-agent-on-a-machine.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md index fcb278e..81a113e 100644 --- a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md +++ b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md @@ -112,15 +112,18 @@ 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. +> **Revised 2026-10-03, building it.** The vendor's managed-settings key for tool servers refuses any +> URL that is not `https://`, including one on loopback, so it cannot carry the console. The module +> owns the vendor's **exclusive** managed tool-server file instead (operator's choice): the console as +> `mesh`, over HTTP on loopback, and every server in the module's `mcp_servers` setting — and no other. +> A server added by hand, a project's own file and a plugin's servers stop loading; claude.ai's +> connectors are kept by a managed setting. A person's own servers move into the setting, for the mesh +> or for one node. Stdio straight to the bus, through the runtime's own client, was weighed and left +> for later: it needs a verb the delivered runtime does not have, and a session holding its own bus +> connection breaks on a credential rotation. **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. The person sets them with the +— mesh layer or node layer — rendered into the same managed file. The person sets them with the controller's `settings` verb on this module, so the list stays declared state; a tool of this module cannot set it, because a bundle calls nothing (ADR 0192). 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. From f5d54db7aa75b8518e1b13e1ebc4d6e204d271e8 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 23:41:38 +0200 Subject: [PATCH 4/6] Plan and designs after ADR 0193, 0195 and 0198: bundles are launched and the runtime is their bus; the manager's daemon is a long-running bundle; the console's five tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dated note on ADR 0183 now rests on ADR 0193 and 0198 rather than on a bundle having no way to call: the manager starts every exchange by the operator's direction, through mesh/ask. To-be 40's WP4 no longer waits on a record — ADR 0198 is it — and the live proofs count the console's five tools (ADR 0195). --- ...-hands-tokens-to-the-agent-over-the-bus.md | 21 ++++++------- .../36-the-operators-agent-on-a-machine.md | 14 ++++----- .../39-the-anthropic-licence-manager.md | 4 +-- ...operators-agent-and-its-licence-manager.md | 30 +++++++++++-------- 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md b/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md index 63fff50..1b49091 100644 --- a/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md +++ b/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md @@ -152,18 +152,19 @@ the node is bound to, and refuses with a notification otherwise. | An unservable binding refuses rather than lends | a manager test: a worker bound to a dead licence is answered with a refusal, never another licence's token | | A switch through the console changes the token on the node and nothing in the answer is a token | a live check on one workstation | -> **The mechanism changed — 2026-10-03, by [ADR 0192](0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md).** +> **The mechanism changed — 2026-10-03, by [ADR 0193](0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md) +> and [ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md).** > What stands: one manager holding the seat, one rotation source, a token sealed to the receiving > module's key on request/reply and never an event, the agent module alone writing what the agent -> reads, the identity guard, the host knowing nothing. What moved: the agent module's code is now a -> tools bundle the node's runtime serves ([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), -> [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)), -> and a bundle has no bus credential of its own and answers calls rather than making them (ADR 0192's -> consequences). So **the manager starts every exchange**: it asks each bound node's agent module for -> its public key, hands it a token, asks it for a login waiting to be adopted, and reconciles every node -> on a schedule — which is what "the agent module asks the seat for its current token" and "offers the -> grant to the manager" in the decision above now mean in practice. The manager's own process needs a -> bus credential to make those calls, which is the question design 38's WP4c leaves to a record. +> reads, the identity guard, the host knowing nothing. What moved: both modules' code is bundles the +> node's runtime launches over stdio and is the bus for — `mesh/ask` for a call made on the module's +> behalf, `mesh/publish` and `mesh/subscribe` beside it — so neither holds a bus credential of its own. +> The manager's refresh and visits are a long-running bundle the control node's runtime launches. And, +> by the operator's direction, **the manager starts every exchange**: it asks each bound node's agent +> module for its public key, hands it a token, asks it for a login waiting to be adopted, and reconciles +> every node on a schedule — which is what "the agent module asks the seat for its current token" and +> "offers the grant to the manager" in the decision above now mean in practice. The agent module could +> ask through its runtime; it does not need to. ## References diff --git a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md index 81a113e..d6527c2 100644 --- a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md +++ b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md @@ -124,8 +124,8 @@ the playbooks in the record. **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 file. The person sets them with the -controller's `settings` verb on this module, so the list stays declared state; a tool of this module -cannot set it, because a bundle calls nothing (ADR 0192). The agent's own HTTP-only constraint for managed servers applies; a person's local +controller's `settings` verb on this module, so the list stays declared state; the list is the operator's +choice, set where every setting is set. 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 @@ -157,9 +157,9 @@ decides it; to-be 39 is the manager's half. This module: 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. *2026-10-03:* every exchange is started by the manager, because a tools bundle answers calls and -has no bus credential to make them ([ADR 0192](../../02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md), -ADR 0183's dated note). The tool names follow the catalogue's `_` form. +handed. *2026-10-03:* every exchange is started by the manager, by the operator's direction (ADR 0183's dated +note); this module is a bundle the node's runtime launches over stdio and answers what it is asked +([ADR 0193](../../02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md)). The tool names follow the catalogue's `_` form. ## 6. Scope, settings and the order of assignment @@ -169,7 +169,7 @@ extra tool servers. **Prerequisite:** the manager holds its seat and has adopted **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 +new session read to confirm it sees the mesh's instruction file, the console's five tools under `mesh`, and its licence; then the rest. ## 7. The package @@ -193,7 +193,7 @@ installer is rejected: it puts a self-updating binary under the person's home, i | 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 | +| a new session on the assigned workstation lists the console's five tools under `mesh` ([ADR 0195](../../02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)) and answers "which node am I" from the instruction file | the exit of the build | ## What this does not settle diff --git a/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md b/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md index 9347d85..f52667c 100644 --- a/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md +++ b/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md @@ -74,8 +74,8 @@ Carried from the predecessor, where each rule was earned by an incident: ## 4. Handing a token to a node -**The manager starts every exchange** (ADR 0183's dated note of 2026-10-03): the agent module is a -tools bundle, which answers and calls nothing. The manager asks each bound node's module for its public +**The manager starts every exchange** (ADR 0183's dated note of 2026-10-03), by `mesh/ask` through the +runtime that launched it ([ADR 0198](../../02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)). The manager asks each bound node's module for its public key the first time and keeps it. From then on: - **On rotation**, the manager calls `claude-code.apply@` on every node bound to the rotated diff --git a/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md index e2d8d64..0806e71 100644 --- a/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md +++ b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md @@ -23,11 +23,15 @@ packages, their order, their sizes and their proofs, and is wrong the moment it It is the shape [design 38](38-building-the-operators-machine.md) gives the operator's machine. *Revised 2026-10-03, after design 38's WP1–WP4b ran:* the node's tool runtime is live on all four -machines, tools are bundles it serves and each is given only the words its artifact declares, and a -bundle has no bus credential of its own. Three things in the first version of this plan changed with -that: the wait on design 38's WP3 is over; the agent module calls nothing, so the manager starts every -exchange (ADR 0183's dated note); and the manager's own process now waits on a different question, -named under WP4. +machines, tools are bundles it serves and each is given only the words its artifact declares, every +bundle is a child the runtime launches over stdio and is the bus for +([ADR 0193](../../02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md), +[ADR 0198](../../02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)), +and the console offers five tools over addresses +([ADR 0195](../../02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)). What changed +in this plan: the wait on design 38's WP3 is over; the manager starts every exchange, by the operator's +direction (ADR 0183's dated note); and the manager's daemon is a long-running bundle the runtime launches, +which ADR 0198 decided the same day — nothing in this plan waits on another record. ## How this is built, and where it is run @@ -52,8 +56,8 @@ Measured 2026-10-03 on the four machines and in the repositories. | escalation | passwordless `sudo` for the operator account on all four — a fact about the machines, checked by nobody | how the agent module writes its managed directory under `/etc` | | the agent itself | installed on all four, at four different versions, all above the one the managed tool-server key needs | declared as the module's package | | a bundle's words | paths and constants written with `${dir:…}` and `${port:…}` only; a fact the mesh knows reaches a bundle as a file whose path is a word | the agent module's facts file and settings file | -| a bundle calling a tool | **not possible**: a bundle answers calls; it holds no bus credential | the manager starts every exchange | -| a module's own long-running process with a bus credential | **undecided** — design 38's WP4c names it as the question its next record answers | the manager's daemon (WP4) | +| a bundle calling a tool | `mesh/ask` through the runtime that launched it (ADR 0198); no bundle holds a bus credential | how the manager visits every node | +| a module's own long-running code | a bundle the runtime launches and restarts (ADR 0198); the runtime's subscription and grants built, the modules moving in design 38 WP4c's waves | the manager's daemon (WP3, WP4) | | the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalogue, built on the controller placement ADR 0183 moved away from; assigned to nothing | its client ported into the manager; the module retired (WP6) | | the credentials write, the strip, the identity read | `anthropic-consumer` in the catalogue; tested; assigned to nothing | ported into the agent module with its tests; the module retired (WP6) | | the predecessor's manager and consumer | the lease per licence, the expiry floor, the lineage comparison, the identity guard, three touchpoints, cooldowns | ported as logic with its tests | @@ -68,7 +72,7 @@ WP3 the manager's code, built and tested (mesh-catalog) │ WP2 live: one workstation, configuration only — no licence yet ── the first live proof │ -WP4 the manager live on the control node ── waits on design 38 WP4c's record (a process's bus credential) +WP4 the manager live on the control node ── its daemon a long-running bundle (ADR 0198) WP5 the licence end to end on one workstation WP6 the rest of the nodes, and the predecessor's remains ``` @@ -127,7 +131,7 @@ contains a token. The catalogue's checks pass. **Proof, live, on one workstation, configuration only.** Assign the module; set the node's role; push. The agent's managed directory holds the two files; everything under the person's agent directory is -byte-identical to before; a new session lists the mesh's tools under `mesh` and answers *which node am +byte-identical to before; a new session lists the console's five tools under `mesh` and answers *which node am I* from the managed instruction file. `claude_code_status` answers through the console. No licence is touched: the module writes the credentials file only when it is handed a token. @@ -136,7 +140,7 @@ touched: the module writes the credentials file only when it is handed a token. *mesh-catalog. Two to three days.* **What is written**, as design 39 says: the manifest (the seat and its verbs, a database, a `secret` -for the key the grants are encrypted with, a tools bundle, a process bundle for the daemon, settings +for the key the grants are encrypted with, a tools bundle and a long-running bundle for the daemon, both launched by the runtime, settings with defaults); the store's migrations; the refresh with its plan, lease, floor and cadence as pure functions; the vendor client from `anthropic-manager`; adoption from a file and from a node's waiting login with the identity guard; usage and its threshold; the visit — key, hand-over, waiting login — per @@ -149,9 +153,9 @@ node's key. ## WP4 — The manager live on the control node -*The live mesh. Half a day.* **Waits on design 38 WP4c's record** — how a module's own long-running -process is given a bus credential and its subscriptions — because the daemon must call the agent module -on every node. Until that record exists, nothing in this plan works around it: no tool container, no +*The live mesh. Half a day.* The daemon is a long-running bundle the control node's runtime launches +(ADR 0198); it calls each node's agent module by `mesh/ask`. If the runtime's half of ADR 0198 is not +yet live on the control node when this package starts, this package waits for it: no tool container, no credential copied by hand. **Order.** Assign the manager on the control node; push. Adopt the API key from a file there. Adopt the From f6668d76d6d8929c120263cea9d59a59b7e046d4 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 23:50:06 +0200 Subject: [PATCH 5/6] There is no home-scoped module: ADR 0181 and 0182 say so as progressive insights; design 36 and to-be 40: the module declares the two directories it owns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0173 §2: a module is what it declares, and there are no kinds of module. The two records called a resource under a home and a module placing one home-scoped; the wording is corrected in place, marked and dated, the decisions unchanged. Design 36 and to-be 40 now say the module declares /etc/claude-code and ~/.claude as directories, so the ownership check sees both, and declares no file under either (mesh-catalog #244). --- ...-is-a-node-fact-and-a-home-is-a-placement-root.md | 12 +++++++++--- ...wns-what-it-places-and-holds-the-rest-as-found.md | 12 +++++++++--- .../01-to-be/36-the-operators-agent-on-a-machine.md | 12 ++++++++---- ...ng-the-operators-agent-and-its-licence-manager.md | 4 ++-- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md b/02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md index 9902909..3dd6848 100644 --- a/02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md +++ b/02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md @@ -9,6 +9,12 @@ extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md # 181. The operator account is a node fact, and a home is a placement root +> **Progressive insight — 2026-10-04.** This record called a resource under a home *home-scoped*, and a +> module that places one a *home-scoped module*. There is no such kind of module +> ([ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) §2: a module is +> what it declares), so the three places now say *a resource placed under a home* and *a module placing +> files under a home*. What was decided is unchanged. + *Reconstructed. The controller shipped this on 2026-09-27 and [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) recorded it as built without a decision behind it. This record states what was decided, from the code and the design, and adds the @@ -35,7 +41,7 @@ its home; the account and its home are machine facts a definition may name in a and content; a roster file may say it lives under the home, and is then rendered per node, placed under that node's account's home, owned by the account, and left out on a node with no account. On 2026-10-02 **all four nodes of the live mesh carry an empty account**: the fact exists and nobody has -stated it, so no home-scoped resource can land anywhere yet. +stated it, so no resource placed under a home can land anywhere yet. ## Considered Options @@ -68,7 +74,7 @@ account. A definition names the account and its home as machine facts, never as may say it is a home file and is then placed and owned the same way. The controller resolves both at composition, and the host chowns what it creates. -**A node with no account cannot carry a home-scoped resource, and says so.** A roster fact that lives +**A node with no account cannot carry a resource placed under a home, and says so.** A roster fact that lives under the home is left out of that node's declaration rather than written to nowhere. A resource naming the account fact on such a node is refused at composition, naming the fact the machine does not have. A module that writes a person's files is thereby unassignable to a machine with no person on it, which @@ -80,7 +86,7 @@ anything. ## Consequences -- **The operator states the account before any home-scoped module lands.** Today none is stated, so the +- **The operator states the account before any module placing files under a home lands.** Today none is stated, so the first assignment of such a module begins with four node records. - The roster carries each node's account, so a composed ssh configuration logs in as the right person on every machine — the gap that surfaced this, closed by the same fact. diff --git a/02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md b/02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md index 358d620..54c9547 100644 --- a/02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md +++ b/02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md @@ -9,6 +9,12 @@ extends: 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found- # 182. 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 +> **Progressive insight — 2026-10-04.** This record said *a home-scoped module* and *the family of +> home-scoped modules*. There is no such kind of module +> ([ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) §2), and the rule +> is about a directory under a home, whichever module declares it; the three places now say so. The +> decision, its options and its consequences are unchanged. + ## Context [ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) lets a module @@ -35,7 +41,7 @@ use tools that no longer exist. Nothing owns them; nothing will ever rewrite or `~/.ssh`: the mesh owns the directory and the files it places; it holds the person's private keys and personal drop-ins as found. That was argued from the lockout `~/.ssh` can cause. The argument here is the same shape with a different stake — the person's work rather than the person's way in — and it has -to hold for every directory the family of home-scoped modules will touch, so it is a rule, not a +to hold for every directory under a home that any module will touch, so it is a rule, not a section. ## Considered Options @@ -52,7 +58,7 @@ section. ## Decision -**A home-scoped module owns the directory it declares: its existence, owner and mode.** The host creates +**A module that declares a directory under a home owns that directory: its existence, owner and mode.** The host creates it if absent, owned by the account, and never removes it while it holds anything ([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)). Inside it, every path the module touches is in exactly one of four classes, and **the class is visible in the definition from the shape @@ -105,7 +111,7 @@ finished its definition. | An owned file found with no record is kept once, then written | host tests of ADR 0102's kept-original rule | | Only the declared keys of a written-into file change, and are given back | host tests of ADR 0102: declared keys set, the rest kept, restored when undeclared | | Nothing found is touched | the family's lab check: a machine with a seeded home holding a person's file beside a predecessor's; after apply the person's file is byte-identical, the predecessor's is kept as the original, the mesh's keys are set and the person's keys in the same file remain; after unassign the mesh's files are gone, the keys are restored, the person's files are untouched and the directory stands | -| Every path a home-scoped module touches is classified | a catalogue review rule for this family: each path is a directory, a file, a file written into, a secret-and-step, or absent — the first module written to it is [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) | +| Every path a module touches under a home is classified | a catalogue review rule for this family: each path is a directory, a file, a file written into, a secret-and-step, or absent — the first module written to it is [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) | ## References diff --git a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md index d6527c2..4195067 100644 --- a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md +++ b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md @@ -54,8 +54,10 @@ instruction file and the manager's tools: | 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 +the module owns the directory `~/.claude` — that it exists, that the operator owns it, its mode, +`0700` — and declares it, so the mesh refuses a second module owning it. Of what is inside, it owns only +the agent's credentials file, which its own code writes for a subscription licence (§5); every other path +is *found*. 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. @@ -66,7 +68,9 @@ lists them, and until they go the agent reads stale instructions beside the mesh in that directory carrying the node's name and the console's endpoint, and a settings file carrying the role and the extra tool servers, merged from the module's settings layers — the bundle is told the two files' paths, because a bundle's words are paths and constants only (ADR 0192); the bus, the console's provision, and that it uses the `anthropic-licence-manager` seat. -Nothing under the home, nothing under `/etc`. +Two directories, declared so the ownership check sees them: the agent's managed directory under +`/etc`, root's, and `~/.claude` under the operator's home, the operator's. No *file* resource under +either: what is in them is written by the module's code (§2 below) or is the person's. **Written by the module's code**, from the facts file and the manager's hand-over, whenever either changes: @@ -187,7 +191,7 @@ installer is rejected: it puts a self-updating binary under the person's home, i | 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 | +| the module's definition names no node, path or login, declares no file under a home or `/etc` (only the two directories), 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 | diff --git a/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md index 0806e71..1f4f4cf 100644 --- a/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md +++ b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md @@ -108,8 +108,8 @@ runtime's port; the controller's tests and the catalogue's checks pass. 1. **The manifest.** The agent's package. A state directory. A facts file in it, rendered by the mesh: the node's name and the console's address from `mcp-endpoint`. A settings file in it, merged from the module's settings layers: the node's role and the extra tool servers. A tools bundle whose - words name the two files, the state directory and nothing else. **No file resource under a home or - under `/etc`.** + words name the two files, the state directory and nothing else. The two directories it owns declared — the agent's managed + directory and `~/.claude` — and **no file resource under either.** 2. **The renderer**, run whenever the runtime collects the module's tools: from the two files, the managed settings file (the tool servers under the entry `mesh`, the attribution trailers, the key-helper for an API-key binding) and the managed instruction file, written under the agent's From 82fa5f79ea22df977b487efcd4cd2b5502d3e58f Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 11:58:46 +0200 Subject: [PATCH 6/6] ADR 0206: a node reports the grant it holds; the manager adopts a licence by refreshing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The operator's flow: clients publish what their credentials file holds, the manager takes in a licence it does not own and rotates it from then on. The token itself cannot be published (design 32 §10, ADR 0201), so a node reports fingerprints and identity as state and hands the grant over only when the manager asks; adopting is refreshing, newest login first; bindings with a generation replace the rotated/switched events. Designs 36 and 39 and to-be 40 amended; a pointer note on ADR 0183. --- ...-hands-tokens-to-the-agent-over-the-bus.md | 10 ++ ...nager-adopts-a-licence-by-refreshing-it.md | 144 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + .../36-the-operators-agent-on-a-machine.md | 48 +++--- .../39-the-anthropic-licence-manager.md | 76 +++++---- ...operators-agent-and-its-licence-manager.md | 28 ++-- 6 files changed, 243 insertions(+), 64 deletions(-) create mode 100644 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md diff --git a/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md b/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md index 1b49091..2cef166 100644 --- a/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md +++ b/02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md @@ -166,6 +166,16 @@ the node is bound to, and refuses with a notification otherwise. > "offers the grant to the manager" in the decision above now mean in practice. The agent module could > ask through its runtime; it does not need to. +> **The mechanism changed — 2026-10-04, by [ADR 0206](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md).** +> What stands: the manager holding the seat, one rotation source, the grants encrypted in its store, a +> token sealed to the receiving module's key on request/reply and never an event, the agent module alone +> writing what the agent reads, the identity guard, bindings as a person's act. What moved: the dated note +> above — the manager no longer starts every exchange. Each node reports what it holds as state, without +> the secret; the manager asks a node for its grant only when a report shows one it does not hold, adopts +> a licence by refreshing it rather than into a licence configured beforehand, and keeps what each +> consumer should hold as state, from which the node fetches its token by request. The rotation and switch +> events are gone. + ## 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 diff --git a/02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md b/02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md new file mode 100644 index 0000000..ef63907 --- /dev/null +++ b/02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md @@ -0,0 +1,144 @@ +--- +topic: what runs on it +status: accepted +date: 2026-10-04 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md +--- + +# 206. A node reports the Anthropic grant it holds; the licence manager adopts a licence by refreshing it, and what each node should hold is the manager's state + +## Context + +[ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) +made the licence manager a module holding the `anthropic-licence-manager` seat: one rotation source, the +long-lived grants in its own store, a short-lived token handed to a node sealed on request/reply, the +agent module alone writing what the agent reads. How the manager *learns* a licence, and who starts each +exchange, it left to a later shape, and three texts have since disagreed: ADR 0183 has a node register +its key and the manager adopt a login only into a licence the node is already bound to; its dated note +of 2026-10-03 has the manager start every exchange and visit every node on a schedule; the agent module +as built asks the seat for its token when an event says to, and pushes a login to the seat. + +**The operator settled it on 2026-10-04, in the operator's own words:** the manager must hold the active refresh token; +whichever node a login happened on holds the latest one; every client publishes what its credentials +file holds, the manager sees a licence it does not own yet and takes it into its store, and from then on +rotates it and distributes the access token. A manager launched for the first time holds no licence and +accepts what the clients report. Several nodes report the same account — today the nodes are all logged in +to one personal account — and before the manager adopts a grant it must know the refresh token still +works. + +Two facts bound how that is built: + +- **A refresh token cannot be published.** Anything published on the bus is kept, and a secret never + enters a stream, sealed or not ([design 32](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §10). + A module's state is a stream too, and the runtime refuses a value carrying a field named like a + credential ([ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md); + refused live on 2026-10-04 for an `Authorization` header). +- **A refresh token can only be checked by using it.** No endpoint answers "is this refresh token + valid" without exchanging it, and an exchange is presumed to rotate it (ADR 0183: the predecessor + lost a licence to a reused one). Checking and adopting are therefore one act, and whoever checks + becomes the token's only live holder. + +Since ADR 0201 the bus has the shape this needs: **state** every node sees, including one that joins +later or a manager that starts later, read whole on start and then watched. + +## Considered Options + +1. **Each node publishes its credentials file, the token included.** What the operator described, + literally. Rejected for the token only: it would sit in a stream every principal that reads the + bucket can read, for as long as the bucket keeps it, and the runtime refuses it anyway. +2. **The manager visits every node on a schedule and collects a waiting login** (ADR 0183's dated + note). Rejected: the manager must know every node in advance and poll it, a node that joins later + waits for the next visit, and "what does each node hold" lives nowhere anyone can read. +3. **Each node reports what it holds as state, without the secret; the manager asks for the secret + only when the report shows a grant it does not hold, and adopts by refreshing.** Chosen: the + operator's flow, with the one part that cannot be on the bus moved onto request/reply. + +## Decision + +**1. Every agent module reports what its node holds, as its own state.** One key per node in the +module's `holdings` state: the account's identity as the agent's own state file names it (account id, +address, organisation), the kind, the refresh token's **fingerprint** and whether one is present at +all, the access token's fingerprint and expiry, the licence it was last handed, and when the credentials +file last changed. Written when the module starts — a node already logged in when the module is first +assigned reports at once — and again whenever the credentials file changes. **No token, ever**: a +fingerprint names a token without being one. + +**2. A licence is an account, and the manager learns it from the reports.** The manager reads every +node's `holdings` at start and watches them. A report carrying a refresh token whose fingerprint the +manager does not hold is a **candidate**: for an account it has no licence for yet, a new licence; for +one it has, a login made since. A manager launched for the first time holds no licence and treats +every report as a candidate. An API key still enters only through the seat's `adopt` verb, from a file +on the manager's node. + +**3. The secret travels only when asked for.** For a candidate, the manager calls that node's agent +module on request/reply, giving its own public key, and is answered with the grant sealed to that key +(ADR 0183's channel, unchanged). + +**4. Adopting is refreshing.** The manager exchanges the candidate's refresh token at the vendor's +endpoint under its lease for that account. If the exchange succeeds, the grant it got back is the +licence's, stored encrypted, and the manager is from then on its only rotation source. If it fails, the +candidate is recorded dead, nothing is adopted, and the report says so. **Several nodes, one account:** +candidates for one account are tried newest login first; the first that refreshes is adopted, and the +manager does not exchange the others. + +**5. A node holds an access token only, so the latest login wins.** A node bound to an adopted licence +is handed the access token and nothing else, and the agent module writes the credentials file without a +refresh token — so the agent on the node can never refresh it, and two refreshers never hold one grant. +A refresh token appearing in a node's file afterwards can therefore only be a person's login there; its +report makes it a candidate, and if it refreshes it replaces the licence's grant. That is the operator's +"whichever node a login happened on holds the latest one", made mechanical. + +**6. What each consumer should hold is the manager's state.** One key per consumer in the manager's +`bindings` state: the licence, its kind, and a **generation** that increases with every rotation and +every switch. The agent module watches its own key; when the generation is newer than the one it +applied, it asks the seat's `current` verb for the token, sending its public key, and is answered sealed +(request/reply). A node that was away reads its key when it is back and asks once. The `licence.rotated` +and `licence.switched` events go: what they announced is now the state itself, and a node needs the +latest, not the history. + +**7. A first binding follows the login.** When the manager adopts a licence from a node's report, a +node with no binding yet whose report names that account is bound to it. Every later change is a +person's act through `bind`, `switch` and `release`, as ADR 0183 says. + +**8. The identity guard stands, on two sources.** The account a grant is filed under is the identity +the node read from the agent's own state. Where the vendor's answer to the refresh names the account, +the manager compares the two and refuses a mismatch with a notification; whether it names it is +measured when the manager is built, and the record of which source decided is kept in the audit. + +## Consequences + +- The manager needs no configuration to start: launched on a mesh whose nodes are logged in, it adopts + every account they hold, one licence each, from the newest login that still refreshes. +- Every node's holding is readable by anyone allowed to read the state — the console, an agent, the + operator — without a token in sight, which is what `licence_status` on each node answered one at a + time. +- **What got harder:** adoption consumes the refresh token the node held. On a node whose grant was + adopted, the agent's own copy is dead from that moment; until the manager hands it an access token + (decision 6), the agent keeps the access token it already had, which lives hours. And a node whose + file still holds a refresh token after adoption — it was not handed one yet — is a second holder of a + dead grant, not a live one, so the rotation-source rule holds. +- A candidate whose refresh fails is not retried by the manager: a dead refresh token does not come + back. A person logs in again, and the new report is a new candidate. +- Nothing in the reports is secret, but they do say which account each node uses; readers of the state + are declared in manifests like any other. + +## How it is checked + +| Rule | Checked by | +|---|---| +| No report carries a token | the runtime refuses a credential-named field (ADR 0201's test); the agent module's test: a report built from a full credentials file holds fingerprints and identity only | +| A node already logged in reports at start | the agent module's test: with a credentials file present and unchanged, starting writes its `holdings` key | +| A candidate is adopted only by a successful refresh, newest login first, once per account | the manager's tests against a stub vendor: two reports for one account, the newer refreshes and is adopted, the older is never exchanged; a failing refresh adopts nothing and records the candidate dead | +| A node is handed an access token only | the agent module's test: the file it writes after a hand-over holds no refresh token | +| A newer generation is fetched once, by request | the agent module's test: a `bindings` change with a newer generation asks `current` once; an equal one asks nothing | +| No event carries a token, and none announces a rotation any more | the manager's test of everything it publishes | +| Live | the manager launched with no licence on a mesh whose four nodes are logged in to one account adopts one licence, binds the four nodes, and each node's file then holds an access token and no refresh token | + +## References + +- [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the manager, its seat and its channel, which this extends +- [ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md) — module state, and the refusal of a secret in it +- [design 32](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §10 — no secret in a stream +- [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md), [to-be 39](../03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md) — the two modules, amended by this record diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 80a0de4..771e1ca 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -304,6 +304,7 @@ python3 00-META/checks/index.py fail if stale - **0203** — [The account's environment is one module's, and every module contributes to it](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md) - **0204** — [A module contributes shell code to the login shell in named slots, and the login shell is the mesh's seat](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md) - **0205** — [Software the distribution does not package ships as a pinned archive of the module's own](0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md) +- **0206** — [A node reports the Anthropic grant it holds; the licence manager adopts a licence by refreshing it, and what each node should hold is the manager's state](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md) ### How it is built diff --git a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md index 4195067..89b2dc4 100644 --- a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md +++ b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md @@ -2,8 +2,9 @@ layer: to-be status: designed code: [] -updated: 2026-10-03 +updated: 2026-10-04 decisions: + - 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md - 02-DECISIONS/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 @@ -139,31 +140,32 @@ it is the person's to remove, and until then the agent sees the mesh's tools twi ## 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: +decides it and [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md) says how it moves; to-be 39 is the manager's half. This module: -- **makes a keypair** in its state the first time it runs, and answers `claude_code_public_key` with the - public half when the manager asks; -- **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; -- **is reconciled, never pulls**: the manager asks every bound node on a schedule and after every - rotation, so a node that was away receives its token when it is back; between visits it keeps the last - token, and `licence_status` says how long it has left; -- **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; -- **holds a login for the manager to collect**: when the credentials file holds a full grant it did not - write — a person logged in — it answers `claude_code_pending_login`, when the manager asks, with the - grant sealed to the key the manager gives in its request and the account's identity read from the - agent's state file; the manager decides, and the next hand-over strips the refresh token; -- **serves `licence_status`**: which licence and kind this node holds, when the token expires, whether +- **makes a keypair** in its state the first time it runs, and sends the public half with every request + that is answered sealed; +- **reports what the node holds**, as its own `holdings` state, one key for this node: the account's + identity read from the agent's state file, the kind, the refresh token's fingerprint and whether one is + present, the access token's fingerprint and expiry, the licence and generation it last applied, when the + credentials file last changed. Written at start — a node already logged in reports at once — and on every + change of the file. Never a token: the runtime refuses one anyway; +- **hands over a grant only when asked**: `claude_code_grant` answers the manager, which gives its public + key, with the full grant in the credentials file sealed to that key — the one time a refresh token + leaves the node, for the manager to adopt by refreshing it; +- **watches the manager's `bindings` state** for this node, and when the generation is newer than the one + it applied, asks the seat's `current` verb for the token, sealed to its own key. A rotation of the same + licence is applied only if newer within one lineage; a switch is applied regardless; +- **writes** for a subscription licence the credentials file as the operator, **access-token-only** — so + the agent here never refreshes, and a refresh token appearing later is a person's login, reported like + any change; 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; +- **serves `claude_code_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. *2026-10-03:* every exchange is started by the manager, by the operator's direction (ADR 0183's dated -note); this module is a bundle the node's runtime launches over stdio and answers what it is asked -([ADR 0193](../../02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md)). The tool names follow the catalogue's `_` form. +Switching is the seat's `switch` verb, asked through the console; this module only applies what the +state says it should hold. *2026-10-04:* this replaces the manager's visits of 2026-10-03 (ADR 0183's dated +note): the node reports, the manager asks for a secret only when a report shows one it does not hold, and +a token is fetched by request when the state says it changed. ## 6. Scope, settings and the order of assignment diff --git a/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md b/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md index f52667c..0c1d422 100644 --- a/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md +++ b/03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md @@ -2,8 +2,9 @@ layer: to-be status: designed code: [] -updated: 2026-10-03 +updated: 2026-10-04 decisions: + - 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md - 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md - 02-DECISIONS/0024-model-access-is-a-provision.md - 02-DECISIONS/0050-model-access-is-vendor-agnostic.md @@ -74,24 +75,22 @@ Carried from the predecessor, where each rule was earned by an incident: ## 4. Handing a token to a node -**The manager starts every exchange** (ADR 0183's dated note of 2026-10-03), by `mesh/ask` through the -runtime that launched it ([ADR 0198](../../02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)). The manager asks each bound node's module for its public -key the first time and keeps it. From then on: +*Amended 2026-10-04 by [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)*, replacing the manager's visits: **what each consumer +should hold is the manager's state, and the token is fetched when it changes.** -- **On rotation**, the manager calls `claude-code.apply@` 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 schedule**, every few minutes, the manager visits each bound node: a node whose token is near - expiry, or that did not answer last time, is handed its current token. A node that was away is - served when it is back, with nothing for it to ask. -- **Never as an event.** What the manager emits names the licence and the outcome and carries no token. +- **The manager keeps a `bindings` state**, one key per consumer: the licence, its kind, and a + **generation** that increases with every rotation and every switch. Nothing in it is secret. +- **The agent module on each node watches its own key.** When the generation is newer than the one it + applied, it asks the seat's `current` verb, sending its public key, and is answered with the token + sealed to it — request/reply, never an event. A node that was away reads its key when it is back and + asks once; a manager that is down leaves every node on its last token, which lives hours. +- **On a switch** the agent applies the new licence's token without comparing expiries, because across + two licences the numbers are unrelated; within one licence it applies only a newer grant. +- **No event announces a rotation or a switch.** What they announced is the state itself, and a node + needs the latest, not the history. What the manager still emits names an outcome and carries no token. -A node whose module does not answer for its 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. +A consumer that never asks is visible: its own report (§6) names the licence and generation it holds, +and a node behind its binding is drift the manager reports. ## 5. Who gets which licence @@ -118,27 +117,46 @@ 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: +*Amended 2026-10-04 by [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)*: a licence is an account, learned from what the nodes +report, and adopted by refreshing it. -- **From a node's login.** A person logs in on a node, as they always have. On its next visit the manager - asks that node's module for a waiting login, giving its own public key; the module answers with the - full grant sealed to it and the account's identity read from the agent's own state file. 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. +- **Every node reports what it holds**, as the agent module's `holdings` state, one key per node: the + account's identity read from the agent's own state file, the kind, the refresh token's fingerprint and + whether one is present, the access token's fingerprint and expiry, the licence and generation it was + last handed, when the credentials file last changed. Written when the module starts — a node already + logged in reports at once — and on every change. Never a token. +- **The manager reads every report at start and watches them.** A report with a refresh token whose + fingerprint the manager does not hold is a candidate: a new licence for an account it has none for, a + login made since for one it has. A manager launched for the first time holds no licence and takes every + report as a candidate. +- **The secret is asked for, never published.** For a candidate the manager calls that node's agent + module, giving its own public key, and is answered with the grant sealed to it. +- **Adopting is refreshing.** The manager exchanges the candidate's refresh token under its lease for + that account; success makes the returned grant the licence's and the manager its only rotation source; + failure records the candidate dead and adopts nothing. Candidates for one account are tried newest login + first, and the first that refreshes ends the search — the others are never exchanged. +- **The latest login wins.** A bound node is handed an access token only and its file holds no refresh + token, so a refresh token appearing there later is a person's login; its report makes it a candidate, + and if it refreshes it replaces the licence's grant. +- **A first binding follows the login**: a node with no binding whose report names the adopted account + is bound to it. Every later change is `bind`, `switch` or `release`. +- **The identity guard** files a grant under the identity the node read; where the vendor's refresh + answer names the account too, a mismatch is refused and notified. Which source decided is audited. - **An API key** is delivered to the manager by the operator through the seat's `adopt` verb from a file on the manager's node, never as an argument. ## 7. What it emits and serves -**Events**, no secret in any: `licence.rotated`, `licence.switched`, `licence.adopted`, -`licence.failing`, `licence.refused`, `usage.read` — the audit logger records them all. +**Events**, no secret in any: `licence.adopted`, `licence.failing`, `licence.refused`, `usage.read` — +the audit logger records them all. *2026-10-04 (ADR 0206):* `licence.rotated` and `licence.switched` are +gone; a rotation or a switch is a new generation in the `bindings` state. + +**State**: `bindings`, which it keeps; the agent module's `holdings`, which it reads. **The seat's verbs**, the contract every future holder must serve: `licences` (each with kind, identity, expiry, failures, who is bound), `bindings`, `bind`, `switch`, `release`, `refresh` (now, one -or all), `usage` (current and history), `adopt`, and `visit` (reconcile one node now). A node's key and -a consumer's token are not seat verbs: the manager asks the node, by the agent module's own tools. +or all), `usage` (current and history), `adopt`, and `current` (a consumer's token, sealed to the key the +consumer sends — ADR 0206). The manager asks a node for a candidate grant by the agent module's own tool. ## 8. Settings diff --git a/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md index 1f4f4cf..399dc05 100644 --- a/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md +++ b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md @@ -2,8 +2,9 @@ layer: to-be status: designed code: [] -updated: 2026-10-03 +updated: 2026-10-04 decisions: + - 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md - 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md - 02-DECISIONS/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 @@ -116,12 +117,13 @@ runtime's port; the controller's tests and the catalogue's checks pass. managed directory through the account's escalation, only when their content changed. 3. **The keypair**, made once in the state directory; X25519 and an authenticated cipher from the language's own library, so the bundle carries no dependency. -4. **The tools**: `claude_code_status` (what is rendered, what licence is held, when its token expires, - fingerprints only); `claude_code_render` (render now); `claude_code_public_key`; - `claude_code_apply` (a sealed token, applied only if newer within one lineage unless it is a switch; - the credentials write as the operator, access-token-only, atomic; the key-helper program for an API - key); `claude_code_pending_login` (a full grant found in the credentials file, sealed to the key the - caller gives, with the account's identity). +4. **The tools and the state** (*2026-10-04, ADR 0206*): `claude_code_status` (what is rendered, what + licence is held, when its token expires, fingerprints only); `claude_code_render` (render now); + `claude_code_grant` (the full grant in the credentials file, sealed to the key the manager gives). + The `holdings` state, written at start and on every change of the credentials file; a watch of the + manager's `bindings` key for this node, which asks the seat's `current` on a newer generation and + applies the sealed answer — only if newer within one lineage unless it is a switch; the credentials + write as the operator, access-token-only, atomic; the key-helper program for an API key. 5. **The documentation**: the six predecessor files and the hand-made console entry a person removes. **Proof, before anything runs live.** Unit tests: the renderer writes the mesh's keys and nothing else; @@ -143,8 +145,9 @@ touched: the module writes the credentials file only when it is handed a token. for the key the grants are encrypted with, a tools bundle and a long-running bundle for the daemon, both launched by the runtime, settings with defaults); the store's migrations; the refresh with its plan, lease, floor and cadence as pure functions; the vendor client from `anthropic-manager`; adoption from a file and from a node's waiting -login with the identity guard; usage and its threshold; the visit — key, hand-over, waiting login — per -bound node; the seat's verbs. +login with the identity guard; usage and its threshold; the seat's verbs. *2026-10-04 (ADR 0206):* in place +of the visit, the watch of every node's `holdings`, adoption of a candidate by refreshing it (newest login +first, once per account), the `bindings` state with a generation per consumer, and `current`. **Proof, before anything runs live.** Unit tests: two refresh runs started together rotate one grant once; a mismatching identity is refused; a worker bound to a dead licence is refused and never lent @@ -158,9 +161,10 @@ node's key. yet live on the control node when this package starts, this package waits for it: no tool container, no credential copied by hand. -**Order.** Assign the manager on the control node; push. Adopt the API key from a file there. Adopt the -two subscription grants: a login on a workstation carrying the agent module, collected by the manager's -visit. Bind each node's agent to a licence. +**Order.** Assign the manager on the control node; push. It reads every node's `holdings` and adopts each +account the nodes are logged in to, by refreshing the newest login's grant (ADR 0206); each node with no +binding is bound to the account it reported. Adopt the API key from a file there. A second subscription +account enters by a login on a workstation carrying the agent module. **Proof.** Through the console: `anthropic-licence-manager.licences` lists three licences with identity and expiry; within the cadence the audit shows a rotation and a later expiry; a forced `refresh` is