diff --git a/02-DECISIONS/0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md b/02-DECISIONS/0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md new file mode 100644 index 0000000..0077899 --- /dev/null +++ b/02-DECISIONS/0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md @@ -0,0 +1,80 @@ +--- +topic: what runs on it +status: accepted +date: 2026-10-04 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md +--- + +# 209. A login on a node moves that node to the account it logged in to; an API key is added from any node, sealed + +## Context + +[ADR 0206](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md) +made a licence an account the manager learns from what the nodes report, adopted by refreshing it, and +bound a node to a licence automatically only when it was bound to nothing (§7); every later change was a +person's act through `bind` and `switch`. It went live on 2026-10-04 with one account, bound to all four +nodes. + +**The operator then asked what happens on a login to a second account on one node, and traced, the +answer was wrong.** The manager adopts the second account as a new licence — and leaves the node bound to +the first. The node is left holding the second account's access token, a refresh token the adoption just +spent, and a binding to the first; at the first account's next rotation it is handed a token its own state +file does not name. A login is the most direct thing a person does on a machine about which account it +uses, and the mesh read it as a contribution of a grant only. + +**An API key could enter only from a file on the manager's node** (ADR 0206, design 39 §6), so adding one +meant reaching that machine. The operator asked for a streamlined process for both. + +## Considered Options + +1. **A login on a node switches that node to the account logged in to.** Chosen. +2. **Keep ADR 0206 §7, and have the person `switch` after logging in.** Rejected: the step is easy to + forget and the state between the login and the switch is the broken one described above. +3. **Refuse to adopt a login for an account other than the node's binding.** Rejected: it discards what + the person plainly meant, and a second account could then enter only by a separate act. + +## Decision + +**1. A login on a node is that node's choice of account.** When the manager adopts a node's login (ADR +0206 §4) — a new account, or a newer login of one it holds — it binds that node to the licence the login +belongs to. If the node was bound to another licence, this is a switch: the node is handed the new +licence's access token and its agent's account is pointed at it, as `switch` does. Every other node stays +where it is. `bind`, `switch` and `release` remain for moving a node without a login. + +**2. A login that does not refresh moves nothing.** The candidate is recorded dead (ADR 0206 §4) and the +node keeps its binding; the person logs in again. + +**3. An API key is added from any node, sealed, never as an argument.** The agent module serves a tool +that reads a key from a file on its own node, seals it to the manager's public key — which the seat now +serves as a verb — and hands it to the seat's `adopt` on request/reply; the file is removed once the +manager has taken it. Optionally the same call binds that node to the new licence. The seat's `adopt` +still also takes a file on the manager's node. An API key is a licence of its own, never an account's: +nothing is learned about it from a report, and it moves a node only when a person says so. + +## Consequences + +- Logging in on a node is the whole of moving that node to an account, new or known. The mesh's state + stays consistent: the binding, the token on the node and the account its agent names agree. +- A second account enters the mesh by one login, and only the node it was logged in on uses it. +- **What got harder:** a person who logs in on a node to try an account moves that node; moving it back is + `switch`. Said in the seat's own description of `switch`, and in the agent module's instruction file. +- The key file on a node exists only until the manager has taken it; the key then lives encrypted in the + manager's store alone (ADR 0183). + +## How it is checked + +| Rule | Checked by | +|---|---| +| A login for another account moves its node and no other | the manager's test: two accounts, the login on one node adopted, that node switched, the others unchanged | +| A newer login of a known account on a node bound elsewhere moves that node | the manager's test | +| A login that does not refresh moves nothing | the manager's test: the binding unchanged, the candidate dead | +| An API key never crosses the bus in the clear and its file is gone afterwards | the agent module's test: the request carries a sealed box only; the file is removed after the seat answered | +| Live | a login to a second account on one workstation: a second licence appears, that workstation is bound to it and its agent names it, the other nodes keep the first | + +## References + +- [ADR 0206](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md) — the flow this extends; §7 is changed by decision 1 +- [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 +- [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) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 65e9b4e..650255c 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -307,6 +307,7 @@ python3 00-META/checks/index.py fail if stale - **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) - **0208** — [The graphical session is one module per piece, on the mesh's seats](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md) +- **0209** — [A login on a node moves that node to the account it logged in to; an API key is added from any node, sealed](0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.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 89b2dc4..f78d169 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 @@ -4,6 +4,7 @@ status: designed code: [] updated: 2026-10-04 decisions: + - 02-DECISIONS/0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md - 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 @@ -159,6 +160,11 @@ decides it and [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-g 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; +- **adds an API key from this node** (*ADR 0209*): `claude_code_add_api_key` reads the key from a file + here, seals it to the manager's `public_key`, hands it to the seat's `adopt`, removes the file once + taken, and on request switches this node to the new licence; +- **follows a login made here**: a login to another account is adopted and moves this node to it (ADR + 0209) — nothing for this module to do beyond reporting it; - **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. 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 0c1d422..990696c 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 @@ -4,6 +4,7 @@ status: designed code: [] updated: 2026-10-04 decisions: + - 02-DECISIONS/0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md - 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 @@ -138,12 +139,16 @@ report, and adopted by refreshing it. - **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`. +- **A login moves its node** (*amended 2026-10-04 by [ADR 0209](../../02-DECISIONS/0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md)*): the node a login was + adopted from is bound to that login's licence — switched, if it was bound to another — and every node + bound to nothing whose report names an account the manager holds is bound to it. `bind`, `switch` and + `release` move a node without a login. - **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. +- **An API key** enters from any node (*ADR 0209*): the agent module there reads it from a file on its own + node, seals it to the manager's key (the seat's `public_key` verb) and hands it to `adopt`, removing the + file once taken — or `adopt` reads a file on the manager's node. Never an argument, never on a stream. + An API key is a licence of its own and moves a node only through `bind` or `switch`. ## 7. What it emits and serves @@ -155,7 +160,8 @@ gone; a rotation or a switch is a new generation in the `bindings` state. **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 `current` (a consumer's token, sealed to the key the +or all), `usage` (current and history), `adopt` (a file on the manager's node, or a key sealed to its +`public_key` — ADR 0209), `public_key`, 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