From 82fa5f79ea22df977b487efcd4cd2b5502d3e58f Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 11:58:46 +0200 Subject: [PATCH] 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