ADR 0206: a node reports the grant it holds; the manager adopts a licence by refreshing it
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.
This commit is contained in:
+10
@@ -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
|
> "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.
|
> 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
|
## 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
|
- [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
|
||||||
|
|||||||
+144
@@ -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
|
||||||
@@ -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)
|
- **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)
|
- **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)
|
- **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
|
### How it is built
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: designed
|
status: designed
|
||||||
code: []
|
code: []
|
||||||
updated: 2026-10-03
|
updated: 2026-10-04
|
||||||
decisions:
|
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/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/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
|
- 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
|
## 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)
|
[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
|
- **makes a keypair** in its state the first time it runs, and sends the public half with every request
|
||||||
public half when the manager asks;
|
that is answered sealed;
|
||||||
- **serves `apply`**: the manager's hand-over, a token sealed to the module's key, with the licence's
|
- **reports what the node holds**, as its own `holdings` state, one key for this node: the account's
|
||||||
name and kind. A rotation of the same licence is applied only if newer within one lineage; a switch is
|
identity read from the agent's state file, the kind, the refresh token's fingerprint and whether one is
|
||||||
applied regardless, because across licences the expiries are unrelated. The answer says applied or
|
present, the access token's fingerprint and expiry, the licence and generation it last applied, when the
|
||||||
refused and why, and never echoes a token;
|
credentials file last changed. Written at start — a node already logged in reports at once — and on every
|
||||||
- **is reconciled, never pulls**: the manager asks every bound node on a schedule and after every
|
change of the file. Never a token: the runtime refuses one anyway;
|
||||||
rotation, so a node that was away receives its token when it is back; between visits it keeps the last
|
- **hands over a grant only when asked**: `claude_code_grant` answers the manager, which gives its public
|
||||||
token, and `licence_status` says how long it has left;
|
key, with the full grant in the credentials file sealed to that key — the one time a refresh token
|
||||||
- **writes** for a subscription licence the credentials file as the operator, access-token-only; for the
|
leaves the node, for the manager to adopt by refreshing it;
|
||||||
API-key licence sets the key-helper in the managed settings to a small program that prints the key
|
- **watches the manager's `bindings` state** for this node, and when the generation is newer than the one
|
||||||
from the module's state, so no file under the home is touched;
|
it applied, asks the seat's `current` verb for the token, sealed to its own key. A rotation of the same
|
||||||
- **holds a login for the manager to collect**: when the credentials file holds a full grant it did not
|
licence is applied only if newer within one lineage; a switch is applied regardless;
|
||||||
write — a person logged in — it answers `claude_code_pending_login`, when the manager asks, with the
|
- **writes** for a subscription licence the credentials file as the operator, **access-token-only** — so
|
||||||
grant sealed to the key the manager gives in its request and the account's identity read from the
|
the agent here never refreshes, and a refresh token appearing later is a person's login, reported like
|
||||||
agent's state file; the manager decides, and the next hand-over strips the refresh token;
|
any change; for the API-key licence sets the key-helper in the managed settings to a small program that
|
||||||
- **serves `licence_status`**: which licence and kind this node holds, when the token expires, whether
|
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.
|
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
|
Switching is the seat's `switch` verb, asked through the console; this module only applies what the
|
||||||
handed. *2026-10-03:* every exchange is started by the manager, by the operator's direction (ADR 0183's dated
|
state says it should hold. *2026-10-04:* this replaces the manager's visits of 2026-10-03 (ADR 0183's dated
|
||||||
note); this module is a bundle the node's runtime launches over stdio and answers what it is asked
|
note): the node reports, the manager asks for a secret only when a report shows one it does not hold, and
|
||||||
([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 `<module>_<verb>` form.
|
a token is fetched by request when the state says it changed.
|
||||||
|
|
||||||
## 6. Scope, settings and the order of assignment
|
## 6. Scope, settings and the order of assignment
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: designed
|
status: designed
|
||||||
code: []
|
code: []
|
||||||
updated: 2026-10-03
|
updated: 2026-10-04
|
||||||
decisions:
|
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/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/0024-model-access-is-a-provision.md
|
||||||
- 02-DECISIONS/0050-model-access-is-vendor-agnostic.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
|
## 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
|
*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
|
||||||
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
|
should hold is the manager's state, and the token is fetched when it changes.**
|
||||||
key the first time and keeps it. From then on:
|
|
||||||
|
|
||||||
- **On rotation**, the manager calls `claude-code.apply@<node>` on every node bound to the rotated
|
- **The manager keeps a `bindings` state**, one key per consumer: the licence, its kind, and a
|
||||||
licence, with the new token sealed to that node's module key. The module answers *applied*, or
|
**generation** that increases with every rotation and every switch. Nothing in it is secret.
|
||||||
*refused* and why, and the manager records it.
|
- **The agent module on each node watches its own key.** When the generation is newer than the one it
|
||||||
- **On a switch**, the same call with the other licence's token, and the binding is the authority: the
|
applied, it asks the seat's `current` verb, sending its public key, and is answered with the token
|
||||||
module applies a bind without comparing expiries, because across two licences the numbers are
|
sealed to it — request/reply, never an event. A node that was away reads its key when it is back and
|
||||||
unrelated.
|
asks once; a manager that is down leaves every node on its last token, which lives hours.
|
||||||
- **On a schedule**, every few minutes, the manager visits each bound node: a node whose token is near
|
- **On a switch** the agent applies the new licence's token without comparing expiries, because across
|
||||||
expiry, or that did not answer last time, is handed its current token. A node that was away is
|
two licences the numbers are unrelated; within one licence it applies only a newer grant.
|
||||||
served when it is back, with nothing for it to ask.
|
- **No event announces a rotation or a switch.** What they announced is the state itself, and a node
|
||||||
- **Never as an event.** What the manager emits names the licence and the outcome and carries no token.
|
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
|
A consumer that never asks is visible: its own report (§6) names the licence and generation it holds,
|
||||||
rather than falling silent. A node whose module refuses — a wrong identity, a stale grant within one
|
and a node behind its binding is drift the manager reports.
|
||||||
lineage — is recorded as drift and reported.
|
|
||||||
|
|
||||||
## 5. Who gets which licence
|
## 5. Who gets which licence
|
||||||
|
|
||||||
@@ -118,27 +117,46 @@ already keeps.
|
|||||||
|
|
||||||
## 6. Adopting a grant
|
## 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
|
*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
|
||||||
argument:
|
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
|
- **Every node reports what it holds**, as the agent module's `holdings` state, one key per node: the
|
||||||
asks that node's module for a waiting login, giving its own public key; the module answers with the
|
account's identity read from the agent's own state file, the kind, the refresh token's fingerprint and
|
||||||
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
|
whether one is present, the access token's fingerprint and expiry, the licence and generation it was
|
||||||
identity matches** that licence's recorded account; a licence not yet identified is identified by its
|
last handed, when the credentials file last changed. Written when the module starts — a node already
|
||||||
first adoption; a mismatch is refused and notified, because the predecessor once filed one account's
|
logged in reports at once — and on every change. Never a token.
|
||||||
grant into another's row this way.
|
- **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
|
- **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.
|
on the manager's node, never as an argument.
|
||||||
|
|
||||||
## 7. What it emits and serves
|
## 7. What it emits and serves
|
||||||
|
|
||||||
**Events**, no secret in any: `licence.rotated`, `licence.switched`, `licence.adopted`,
|
**Events**, no secret in any: `licence.adopted`, `licence.failing`, `licence.refused`, `usage.read` —
|
||||||
`licence.failing`, `licence.refused`, `usage.read` — the audit logger records them all.
|
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,
|
**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
|
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
|
or all), `usage` (current and history), `adopt`, and `current` (a consumer's token, sealed to the key the
|
||||||
a consumer's token are not seat verbs: the manager asks the node, by the agent module's own tools.
|
consumer sends — ADR 0206). The manager asks a node for a candidate grant by the agent module's own tool.
|
||||||
|
|
||||||
## 8. Settings
|
## 8. Settings
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: designed
|
status: designed
|
||||||
code: []
|
code: []
|
||||||
updated: 2026-10-03
|
updated: 2026-10-04
|
||||||
decisions:
|
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/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/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/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.
|
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
|
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.
|
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,
|
4. **The tools and the state** (*2026-10-04, ADR 0206*): `claude_code_status` (what is rendered, what
|
||||||
fingerprints only); `claude_code_render` (render now); `claude_code_public_key`;
|
licence is held, when its token expires, fingerprints only); `claude_code_render` (render now);
|
||||||
`claude_code_apply` (a sealed token, applied only if newer within one lineage unless it is a switch;
|
`claude_code_grant` (the full grant in the credentials file, sealed to the key the manager gives).
|
||||||
the credentials write as the operator, access-token-only, atomic; the key-helper program for an API
|
The `holdings` state, written at start and on every change of the credentials file; a watch of the
|
||||||
key); `claude_code_pending_login` (a full grant found in the credentials file, sealed to the key the
|
manager's `bindings` key for this node, which asks the seat's `current` on a newer generation and
|
||||||
caller gives, with the account's identity).
|
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.
|
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;
|
**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
|
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
|
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
|
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
|
login with the identity guard; usage and its threshold; the seat's verbs. *2026-10-04 (ADR 0206):* in place
|
||||||
bound node; the seat's verbs.
|
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
|
**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
|
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
|
yet live on the control node when this package starts, this package waits for it: no tool container, no
|
||||||
credential copied by hand.
|
credential copied by hand.
|
||||||
|
|
||||||
**Order.** Assign the manager on the control node; push. Adopt the API key from a file there. Adopt the
|
**Order.** Assign the manager on the control node; push. It reads every node's `holdings` and adopts each
|
||||||
two subscription grants: a login on a workstation carrying the agent module, collected by the manager's
|
account the nodes are logged in to, by refreshing the newest login's grant (ADR 0206); each node with no
|
||||||
visit. Bind each node's agent to a licence.
|
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
|
**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
|
and expiry; within the cadence the audit shows a rotation and a later expiry; a forced `refresh` is
|
||||||
|
|||||||
Reference in New Issue
Block a user