ADR 0209: a login on a node moves that node to its account; an API key is added from any node, sealed
Traced live: a login to a second account was adopted and left its node bound to the first, holding a spent refresh token. Designs 36 and 39 amended.
This commit is contained in:
+80
@@ -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)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user