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.
11 KiB
topic, status, date, deciders, reconstructed, extends
| topic | status | date | deciders | reconstructed | extends |
|---|---|---|---|---|---|
| what runs on it | accepted | 2026-10-04 | jochen | false | 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
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 §10).
A module's state is a stream too, and the runtime refuses a value carrying a field named like a
credential (ADR 0201;
refused live on 2026-10-04 for an
Authorizationheader). - 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
- 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.
- 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.
- 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_statuson 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 |