Merge pull request 'ADR 0209: a login on a node moves that node to its account; an API key is added from any node, sealed' (#359) from decision/0209-a-login-moves-its-node into main

This commit is contained in:
2026-10-04 13:12:01 +00:00
4 changed files with 98 additions and 5 deletions
@@ -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)
+1
View File
@@ -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
@@ -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.
@@ -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