Design 39 bus-and-switch: the licence manager keeps its data on the bus and switches on its own usage
The operator, 2026-11-11: the licence manager itself moves every consumer off a licence at 99 % of its session or weekly window to the licence with the most room, configurable, with its Postgres database replaced by bus state and tools for all it keeps and does. That night a session meant to switch missed the readings, so no session does it any more. The new milestone part writes every line it builds (kept data, the one carry-over of today's rows, the vault move of ADR 0550 §3, bindings and holdings as events, the switch, its warning and condition, the tools, adding a licence), and names the three questions no record settles; the main file is put in the shape a builder reads whole. Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
This commit is contained in:
@@ -535,6 +535,11 @@ and the operator act. **Recorded by** the controller's `identity` and `licences`
|
||||
coding agent on a node (ADR 0183).
|
||||
Taking a consumer off a licence is **unbinding** it; the licence manager's verb for it is still
|
||||
`release`, an identifier to rename.
|
||||
- **room** of a licence — how much of its vendor's usage windows is still unused: 100 less the higher of its
|
||||
session window's (five hours) and weekly window's percentage in its latest reading, known only for a subscription
|
||||
read recently and healthy. The licence manager moves every consumer off a licence at its threshold to the one with
|
||||
the most room (design 39 bus-and-switch §25, §26).
|
||||
*Not:* ~~headroom~~
|
||||
- **proof** — what an authorising answer carries: a verified sender, a one-time code, a security key's
|
||||
touch (ADR 0234). How much proof a kind of ask needs is its **assurance level**.
|
||||
- **controller's terminal** — a command line the controller runs as typed by its operator, not asked through a
|
||||
@@ -991,6 +996,10 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283, 0564, 0668.
|
||||
with its largest size in seven days by disk-load on that node, in its own state, one key
|
||||
`<node>.<module>.<item>` written only when it changes; the holder of `node-backup`, mounts, nfs-server and the
|
||||
self-check read them there, never the controller (ADR 0571 §5).
|
||||
- **carry-over** — a module's one copy of the data it kept in its old form into its bus state, at its first start
|
||||
on the new form and before it does anything else, recorded in a key of its own so it is never copied again
|
||||
(ADR 0437 §7; design 39 bus-and-switch §11 to §13). Not a take-over, which moves a job between parts.
|
||||
*Not:* ~~data take-over~~
|
||||
- **sticky binding** — a consumer's tie to the data a provider keeps for it, which moves only by a
|
||||
person (ADR 0232).
|
||||
- **bus state** — what a module keeps on the bus, asked for in `requires` as a provision of the bus module, by a
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
---
|
||||
layer: to-be
|
||||
design: 39
|
||||
milestone: bus-and-switch
|
||||
updated: 2026-11-11
|
||||
code: [mesh-catalog modules/claude-licence-manager, mesh-catalog modules/claude-code, mesh-catalog modules/mesh-vault]
|
||||
decisions:
|
||||
- "ADR 0183"
|
||||
- "ADR 0206"
|
||||
- "ADR 0209"
|
||||
- "ADR 0054"
|
||||
- "ADR 0550"
|
||||
- "ADR 0595"
|
||||
- "ADR 0596"
|
||||
- "ADR 0437"
|
||||
- "ADR 0564"
|
||||
- "ADR 0570"
|
||||
- "ADR 0230"
|
||||
- "ADR 0380"
|
||||
- "ADR 0253"
|
||||
- "ADR 0289"
|
||||
- "ADR 0349"
|
||||
---
|
||||
|
||||
# Design 39, milestone bus-and-switch — the licence manager on the bus, switching on its own usage
|
||||
|
||||
**What it builds.** The seat `anthropic-licence-manager` keeps everything it holds in the bus's own storage, by the
|
||||
kept-data templates, and its store database goes; every grant and token moves into the vault, as ADR 0550 §3 has it;
|
||||
and the manager itself moves every consumer off a licence whose session or weekly usage reaches a threshold, to the
|
||||
licence with the most room, says each switch as an event and to the operator, and raises a condition when no licence
|
||||
has room. Its tools read all it keeps and start by hand all it does. The whole shape is in the design's main file
|
||||
([39](../39-the-anthropic-licence-manager.md)); this part holds every line the milestone builds, cited as "design 39
|
||||
bus-and-switch §n".
|
||||
|
||||
**Why now.** The operator, 2026-11-11 22:10 to 22:20 CET: "build it in the licence manager, but make it configurable
|
||||
of course, also remove the postgres database in favor of a bus store, implement useful tools, as much relevant state
|
||||
as possible"; earlier that evening: "when the current licence usage is at 99%, switch to a licence that still has
|
||||
room" (first said at 98, then 99), "on all nodes". That night every node was bound to one licence that reached 99 % of
|
||||
its weekly window, and a session meant to switch them missed the readings: so the manager switches on its own
|
||||
readings, and never a session. Issue 1409 (no warning before a licence's limit) is fixed in the same change (§30).
|
||||
|
||||
**Where.** One repository, mesh-catalog, and three of its modules: `claude-licence-manager` (the seat's holder),
|
||||
`claude-code` (the agent module on every node, the consumer) and `mesh-vault` (the seat `mesh-vault`), with hq for this
|
||||
part, the main file and the glossary. The order between them is the module graph's, each an edge the delivery plan
|
||||
derives (§50).
|
||||
|
||||
**Open design questions.** Three questions no accepted record settles; the lines that wait on one say so, and develop
|
||||
does not start on them before the operator's answer is written here (playbook 02, *Answering a design question*):
|
||||
|
||||
- **Q1, the vault's own-account rule.** ADR 0550 §3 moves the grants and tokens into the vault and waits until the
|
||||
licence manager and each agent module run under a bus account of their own (ADR 0349 §11, issue 569); both run
|
||||
today in their machine's tool runner, under its account. Lines §15 to §20 wait on it.
|
||||
- **Q2, the record behind the switch.** ADR 0183 §21 (moving a consumer is a person's act), ADR 0206 §16 (no
|
||||
`licence.switched` event) and ADR 0206 §18 (every later change is a person's act) are in force; the operator's
|
||||
answers of 2026-11-11 change them. A record changing those three clauses comes before develop (playbook 02,
|
||||
*Changing a decision*). Lines §24 to §33 are written from the operator's answers and wait on it.
|
||||
- **Q3, how the first start reads today's rows.** The carry-over (§11 to §14) reads the store database with the
|
||||
module's own credential, and a build whose manifest no longer requires `postgres-database` is composed with no
|
||||
credential for it, while a retiring provision keeps its account only until the operator's Approve (ADR 0570 §5).
|
||||
Line §11's source waits on it.
|
||||
|
||||
## The kept data on the bus
|
||||
|
||||
1. **The manifest asks for its kept data by template and requires no database.** `requires` loses
|
||||
`postgres-database` and the `secret` requirement `grant-key` (its key ends with the grants' move, §20); `contributes`,
|
||||
`binds`, `secrets`, the resources `database-url` and `prepare` and the run-once step `prepare` go with them. It
|
||||
gains one `bus-state` entry per piece of kept data, each in the typed long form (ADR 0595 §1): the local name, the
|
||||
storage type, the template and its options, as §2 to §9 list them. The old section `state` (today `bindings`) and
|
||||
the `reads` entry `claude-code.holdings` go: the module moves to the new form whole (ADR 0595 §11), keeping the local
|
||||
name `bindings` so its data stays. Why: the operator, 2026-11-11, and principle 1; the templates are ADR 0595 §2.
|
||||
How it is checked: the manifest validator accepts the manifest at the merge check; a catalog test finds no
|
||||
`postgres-database`, `reads` or old `state` in it.
|
||||
2. **State `licences`: one key per licence, no secret in it.** Template `state`, option `history` 1. The key is the
|
||||
licence's key form (§10). The value: `name`, `kind` (`subscription` or `api-key`), `account` (the account id),
|
||||
`address`, `organisation`, `refreshFingerprint`, `accessExpiresAt`, `refreshExpiresAt`, `failures` (refreshes
|
||||
failed in a row), `notifiedAt`, `adoptedAt`, `adoptedFrom` (the node), `rotatedAt`, `generation` (the vault
|
||||
secret's, §15), and `usage`, the latest reading of §24 (`sessionPct`, `sessionResetsAt`, `weeklyPct`,
|
||||
`weeklyResetsAt`, `sonnetPct`, `sonnetResetsAt`, `readAt`, `readError`). Written by the manager alone, at each
|
||||
adoption, rotation, failure and reading; deleted by no act of the milestone (a licence is never removed today, and
|
||||
none is added here). It replaces the table `licence`, less its column `sealed`, which goes to the vault (§12).
|
||||
Why: ADR 0550 §3 keeps the licence records without a secret in the manager; principle 4 keeps the latest reading
|
||||
beside its licence so `licences` answers from one key. How it is checked: unit, a rotation and a reading each write
|
||||
the key and no field holds a token; the after-move scan of §20.
|
||||
3. **State `bindings`: one key per consumer.** Template `state`, option `history` 8, so the last moves of a consumer
|
||||
are read with the state's `history` call. The key is the consumer's: the node's name for a node's agent, `<node>.<worker>`
|
||||
for a worker (ADR 0183 §19). The value: `licence` (its name), `kind`, `generation` (one more at every rotation and
|
||||
every switch of that consumer, ADR 0206 §13), `since`, `why` and `by` (who or what moved it last: a person's
|
||||
name, `login` for ADR 0209's move, `usage` for §26). Written by the manager alone; deleted by `release`. It replaces
|
||||
the table `binding` and today's old-section state of the same name. Why: ADR 0206 §13; the eight past values are
|
||||
the design's choice, for `licences_switches` to show a consumer's last moves. How it is checked: unit, `bind`,
|
||||
`switch`, `release` and §26 each write or delete the key with its `why` and `by`.
|
||||
4. **State `leases`: one key per lease, ended by its holder or its lifetime.** Template `state`, option `history` 1,
|
||||
with a lifetime per key of 2 minutes (the time one exchange with the vendor is given today). The key is
|
||||
`licence.<licence key>` or `account.<account id>`; the value names the holder (the process) and `until`. A lease
|
||||
is taken by creating the key, which fails while another holds it, before the licence is read (ADR 0183 §6); the
|
||||
holder deletes it at the end; a holder that dies leaves it to its lifetime. It replaces the table `lease`. Why:
|
||||
ADR 0183 §6 keeps one rotation source; a create that fails is the bus's own compare-and-set. How it is checked:
|
||||
unit, two refreshes started together rotate once and the second answers "another run holds its lease"
|
||||
(the main file's first acceptance test).
|
||||
5. **State `offered`: one key per grant a node offered.** Template `state`, option `history` 1, a lifetime per key
|
||||
of 90 days. The key is the refresh token's fingerprint; the value: `node`, `account`, `outcome` (`adopted`,
|
||||
`dead`, `refused`), `why`, `at`. Written at each candidate's outcome (ADR 0206 §9, §10); deleted by its lifetime,
|
||||
after which a fingerprint offered again is tried once more and recorded again. It replaces the table `offered`.
|
||||
Why: the manager never exchanges a dead candidate twice within its lifetime; 90 days is longer than a refresh
|
||||
token lives. How it is checked: unit, a dead candidate is not exchanged at its second report.
|
||||
6. **Log `usage`: every reading, kept 35 days.** Template `log`, options `max-age-days` 35 and `max-mib` 256, one
|
||||
subject per licence key. Each entry: `at`, the flattened reading of §24 and `raw`, the vendor's answer whole
|
||||
(ADR 0054). Written at every reading, never rewritten. It replaces the table `usage`. Why: ADR 0054 keeps the raw
|
||||
reading beside the normalised one; 35 days hold five weekly windows, and at five minutes for four licences
|
||||
the log stays under a quarter of its cap. How it is checked: unit, a reading appends one entry under its licence's
|
||||
subject; the bus module's fill condition (ADR 0595 §6) warns before the log is full.
|
||||
7. **Log `audit`: every act, kept for good.** Template `log`, option `max-mib` 64, no age, one subject per kind:
|
||||
`adopted`, `refused`, `failed`, `rotated`, `bound`, `switched`, `released`, `drift`, `carried`, `settings`. Each
|
||||
entry: `at`, `by`, `why` and the act's own fields (for `switched`: §27's). Written after each act, never rewritten.
|
||||
It replaces the table `audit`. Why: ADR 0183 §5 keeps the audit of every switch; a log keeps it in order and for
|
||||
good. How it is checked: unit, each act appends one entry under its kind.
|
||||
8. **State `carried`: the carry-over's record.** Template `state`, option `history` 1, one key `postgres`: `at`, and
|
||||
for each table of today's database the rows read and the entries written. Written once, at the end of §11; never
|
||||
deleted, since its presence is what keeps a second start from carrying over again. Why: ADR 0437 §7's way of
|
||||
carrying a module's state over once. How it is checked: §13.
|
||||
9. **State `findings` and the events `finding-events`.** The manager is an observer (design 47 G §383): it asks
|
||||
the state `findings` by the template `state` and the stream `finding-events` by the template `events`, with
|
||||
`max-age-days` 7 and `max-mib` 16, its field `events` listing `finding-written`, `finding-cleared` and
|
||||
`findings-stated`, as design 47 G §529 has every part that is not of the foundation declare them; `consumes`
|
||||
gains `mesh-conditions.findings-asked`. It writes its findings (§29 to §31, §33) with mesh-sdk's finding writer
|
||||
(design 47 G §461), which states them at its start and on the keeper's ask. Why: a condition reaches the operator
|
||||
only from a finding the condition keeper hears (ADR 0564 §2 as design 47 G §383 changes it). How it is checked:
|
||||
whole (manager), a finding written raises its condition at the keeper and its clearing clears it.
|
||||
10. **A licence's key form.** A licence's name is its account's address, or its id where it has none, or the name
|
||||
given at `adopt` for an API key (today's `NameFor`); a name holds characters a key may not. Its key is the name
|
||||
with each `@` written as `_at_` and every other character outside letters, digits, `-` and `.` written as `_`. An
|
||||
adoption whose key another licence already has is refused, said in the audit and as `licence.refused`. Every
|
||||
verb and tool still takes and answers the name; the key is the store's alone. Why: a key of the bus takes only
|
||||
those characters; the form is the design's choice, readable in `licences_kept`. How it is checked: unit, a table
|
||||
of names and their keys, and the refused clash.
|
||||
|
||||
## Carrying over today's rows
|
||||
|
||||
11. **At its first start the manager carries over every row before it does anything else.** When the state
|
||||
`carried` has no key `postgres`, the manager, before it hears an event, reads a holding report, refreshes or reads
|
||||
usage, reads every row of the six tables of its database `claude_licences` and writes them as §12 says, then the
|
||||
key `carried.postgres`. Until that key exists it serves its read verbs and tools only, answering every other
|
||||
with "the carry-over has not ended", and writes the finding `carry-over-failed` (urgent; "Needs you: the licence
|
||||
manager could not carry over its licences; no consumer is moved and no token is refreshed until it does", naming
|
||||
the table and the error) when a read or a write fails, cleared when the key is written. Why: a manager that ran on
|
||||
an empty state would take every holding report for a candidate (ADR 0206 §5) and exchange refresh tokens it
|
||||
already holds; failure is loud (principle 3). Its source, the database read with the module's own credential,
|
||||
waits on Q3. How it is checked: whole (manager, a store database filled from a copy of today's rows), every row
|
||||
is carried and the key written; with the database unreachable the finding is raised and no vendor call is made.
|
||||
12. **What each table becomes.**
|
||||
- `licence` → a key of `licences` (§2) per row; each row's `sealed` grant is opened with today's `grant.key` and
|
||||
handed to the vault as §15 says, its generation 1; the row's other columns become the key's fields.
|
||||
- `binding` → a key of `bindings` (§3) per row, its `generation` kept, `why` "carried over", `by` the manager.
|
||||
- `lease` → nothing: a lease lives two minutes, and none taken before the start still holds.
|
||||
- `offered` → a key of `offered` (§5) per row, its lifetime counted from the row's `at`.
|
||||
- `usage` → one entry of the log `usage` (§6) per row of the last 35 days, oldest first, its `at` kept.
|
||||
- `audit` → one entry of the log `audit` (§7) per row, oldest first, its `at` kept, under the subject its `what`
|
||||
names (`drift` for any other).
|
||||
Why: no licence, binding, grant or history is lost (the operator, 2026-11-11); usage older than the log's age
|
||||
would be dropped by the log at once. How it is checked: whole, the counts in `carried.postgres` equal the rows read
|
||||
for every table but `lease`, and `mesh-vault.get` of each licence's grant (§16) answers it.
|
||||
13. **A second start carries over nothing.** With `carried.postgres` present the manager never reads the database,
|
||||
and a carry-over that failed halfway is started again from the beginning, every write of §12 being idempotent
|
||||
(a key put again with the same value, a log entry not appended twice: before appending, the manager reads its
|
||||
log's last entry for that subject and skips rows at or before its `at`). Why: ADR 0437 §7. How it is checked:
|
||||
whole, a second start writes nothing; a start stopped after `bindings` and started again leaves every count equal
|
||||
to the rows read.
|
||||
14. **The database's fate, and that this delivery moves data.** Once `carried.postgres` exists and §20's scan finds
|
||||
no grant left outside the vault, the database `claude_licences` is retired: the provision is retiring once no
|
||||
build a want names requires it, and its account is removed only on the operator's Approve (ADR 0570 §5); the
|
||||
database is renamed aside by the postgres module's `postgres_retire_database` and deleted only by a person
|
||||
(ADR 0230 §14). The delivery of this milestone moves data, so it is said to the operator before it starts
|
||||
(which module, its machine, that its licences, bindings and grants move to the bus and the vault), and the
|
||||
manager's machine is backed up before the module moves, as for every module that holds data. Why: ADR 0570 §5,
|
||||
ADR 0230; the operator's instruction that a delivery moving data is said first. How it is checked: the delivery's
|
||||
plan names the backup; after the delivery `mesh-store.databases` lists `claude_licences` until the operator's
|
||||
Approve, then its renamed-aside form.
|
||||
|
||||
## Grants and tokens in the vault
|
||||
|
||||
*Lines §15 to §20 build ADR 0550 §3 and wait on Q1.*
|
||||
|
||||
15. **The vault holds two secrets per licence, handed to it by the manager alone.** `licence.<licence key>.grant`
|
||||
holds, for a subscription, the refresh token, the access token and both expiries, and for an API key the key;
|
||||
`licence.<licence key>.access` holds a subscription's access token and its expiry, or the API key. The vault
|
||||
serves a new verb on its seat, `licence-put`: `licence`, `part` (`grant` or `access`), `generation` and the value
|
||||
sealed to the vault's key; callable by the holder of the seat `anthropic-licence-manager` only, refused for any
|
||||
other caller naming the rule; it keeps the newest generation and refuses an older one; and it says its event
|
||||
`secret-changed` with the secret's name and generation, never the value. The manager calls it after every
|
||||
adoption, rotation and carry-over (§12), the `grant` first. Why: ADR 0550 §3 (the manager hands what it gets back
|
||||
to the vault, keeping nothing at rest); the names, the verb and its fields are the design's choice. How it is
|
||||
checked: unit (vault), `licence-put` from another caller is refused and an older generation is refused; unit
|
||||
(manager), a rotation calls it twice and keeps no token after the call returns.
|
||||
16. **The manager reads a grant from the vault when it needs one, and keeps it only for that act.** To refresh,
|
||||
adopt again or read usage, the manager calls `mesh-vault.get` for `licence.<licence key>.grant`, sealed to its own
|
||||
public key, under the licence's lease (§4), and drops the value when the act ends. Why: ADR 0550 §3, ADR 0183 §6.
|
||||
How it is checked: unit, the manager's state and log hold no token after a rotation.
|
||||
17. **A consumer's agent module gets its token from the vault, which serves only the licence a binding names.** The
|
||||
vault answers `mesh-vault.get` for `licence.<licence key>.access` only to an asker whose machine is the node the
|
||||
asking consumer's binding names, and only for the licence that binding names. It learns the bindings from the
|
||||
manager's `binding.changed` (§21), which it consumes, and at its start from `anthropic-licence-manager.bindings`,
|
||||
its first view, keeping them in its own state `licence-bindings` (template `state`, one key per consumer, the
|
||||
licence and generation, deleted on a release). The agent module never receives a refresh token (ADR 0206 §11).
|
||||
Why: ADR 0550 §3; ADR 0596 §2 (the vault asks the manager, never reads its state); the state is the design's
|
||||
choice. How it is checked: unit (vault), a token is refused to a machine no binding names and for a licence its
|
||||
binding does not name (the main file's acceptance test), and given after a `binding.changed` naming it.
|
||||
18. **`current` answers which licence and its health, never the token.** The seat verb `current` takes `consumer`
|
||||
and answers `licence`, `kind`, `generation`, `health` (`ok`, `failing`, `expiring`) and `accessExpiresAt`; its
|
||||
argument `public_key` goes. Why: ADR 0550 §3. How it is checked: unit, no answer of `current` holds a token.
|
||||
19. **The agent module applies a new token when the vault says it changed or its binding moved.** The agent module
|
||||
on a node asks `mesh-vault.get` for its licence's `access` sealed to its own key, when it hears a
|
||||
`binding.changed` for its consumer with a newer generation than it applied, when it hears the vault's
|
||||
`secret-changed` for the licence it holds, and at its start; it then writes the credentials file or the key-helper
|
||||
as design 36 §5 has it, unchanged. Why: ADR 0550 §3, ADR 0206 §14 as ADR 0596 §2 changes it (an event and a first
|
||||
view in place of a watched key). How it is checked: whole, `switch` changes a workstation's token end to end (the
|
||||
main file's acceptance test).
|
||||
20. **What ends with the move.** The manager's `grant.key`, its `secret` requirement and the code that seals a grant
|
||||
at rest go; the `public-key` verb and the manager's keypair stay, since a login offered by a node is sealed to it
|
||||
(ADR 0183 §23, ADR 0550 §3). After the delivery the vault's `secret_holders` lists both secrets of every licence
|
||||
and the manager's own secret `grant-key` is retired with `mesh-vault.retire` on the operator's word. Why:
|
||||
ADR 0550 §3 (a scan after the move that the manager holds no grant). How it is checked: `licences_kept` (§41)
|
||||
shows no field holding a token in any entry, and `secret_holders` the two secrets per licence.
|
||||
|
||||
## Bindings and holdings said as events
|
||||
|
||||
21. **Every change of a binding is said as `binding.changed`.** The manager declares in `emits` the event
|
||||
`binding.changed`, said after the key of `bindings` is written or deleted: `consumer`, `licence` (empty on a
|
||||
release), `kind`, `generation`, `from` (the licence before), `why`, `by` and `at`; never a token. Why: ADR 0596 §2,
|
||||
§3 (the owner gains the event its readers need before they move); the name and fields are the design's choice.
|
||||
How it is checked: unit, `bind`, `switch`, `release`, a login's move and §26 each say one event per consumer
|
||||
moved.
|
||||
22. **The agent module follows its binding by that event, and asks once at its start.** `claude-code` gains
|
||||
`consumes` `claude-licence-manager.binding.changed`, acting only on the event naming its node's consumer, and at
|
||||
its start asks `anthropic-licence-manager.bindings` with `consumer` its node for its first view; its `reads`
|
||||
entry `claude-licence-manager.bindings` goes. A node that was away asks once when it is back (ADR 0206 §15), by its
|
||||
start or its reconnect. Why: ADR 0596 §2, §4. How it is checked: unit (claude-code), an event for another
|
||||
consumer is ignored; whole, a node started after a switch applies the new licence from its first view.
|
||||
23. **A node's holdings are said as events, and asked for by the manager.** `claude-code` declares in `emits`
|
||||
`holdings-changed`, said at its start and whenever its holdings change (ADR 0206 §2), carrying its holdings
|
||||
report whole as ADR 0206 §1 lists it, never a token, and gains `consumes`
|
||||
`claude-licence-manager.holdings.asked`, on which it says `holdings-changed` again. The manager declares
|
||||
`holdings.asked` (`at`, `by`) and says it after its carry-over (§11) and on `licences_resync` (§42), and consumes
|
||||
`claude-code.holdings-changed` in place of today's `reads`. Its state `holdings` stays `claude-code`'s own. Why:
|
||||
ADR 0596 §2, §3; a first view by events, since the holdings are one key per node; the names are the design's
|
||||
choice. How it is checked: whole (manager and a fixture agent module), a `holdings.asked` is answered by one
|
||||
`holdings-changed` per node, and a report carrying an unknown refresh token's fingerprint makes a candidate
|
||||
(ADR 0206 §4).
|
||||
|
||||
## Switching on usage
|
||||
|
||||
*Lines §24 to §33 are the operator's answers of 2026-11-11 and wait on Q2's record.*
|
||||
|
||||
24. **The manager reads each subscription's usage on a cadence, and keeps the reset times.** Every
|
||||
`usage_minutes` (§33), for each licence of kind `subscription`, the manager reads the vendor's usage with the
|
||||
licence's access token (§16) and flattens it: `sessionPct` and `sessionResetsAt` (the five-hour window),
|
||||
`weeklyPct` and `weeklyResetsAt` (the seven-day window), `sonnetPct` and `sonnetResetsAt`. It writes the entry of
|
||||
§6, the key's `usage` of §2, and says `usage.read` with those fields, never a token. A failed reading writes
|
||||
`readError` (the vendor's status and reason) and keeps the last percentages. This is the manager's one timer: the
|
||||
vendor says nothing as usage grows. Why: ADR 0054; the operator's "reset times"; the reset times of the weekly and
|
||||
sonnet windows are read beside the session's, which alone is read today. How it is checked: unit, a vendor
|
||||
answer is flattened with its three reset times; a failed answer keeps the percentages and writes the reason.
|
||||
25. **A licence's room, and when it is known.** A licence's room is 100 less the higher of its `sessionPct` and
|
||||
`weeklyPct` in its latest reading. It is known only for a licence of kind `subscription`, read successfully within
|
||||
`usage_stale_minutes`, whose refresh has not failed `failures_to_notify` times in a row and whose refresh token is
|
||||
not expired. A licence whose room is not known is never a target of §26: an API key has no window to read and is
|
||||
billed per use, so it is never one. Why: the operator's "a licence that still has room"; a licence's room is known
|
||||
only from a reading; the sonnet window is shown and does not count, since the operator named the session and
|
||||
weekly windows. How it is checked: unit, a table of readings, ages and healths and the room each gives.
|
||||
26. **On each reading, a licence at its threshold moves every consumer bound to it.** After writing a reading of
|
||||
licence L, when `switch_auto` is on, L has one or more consumers bound to it, and L's `sessionPct` or `weeklyPct`
|
||||
is at or above `switch_at_pct`, the manager chooses the target: among the other licences whose room is known and
|
||||
whose `sessionPct` and `weeklyPct` are both below `room_below_pct`, the one with the most room, the first by name
|
||||
on a tie. It then moves every consumer bound to L to the target, each as the `switch` verb does (its binding's
|
||||
generation one more, `why` naming L, the window, its percentage and its reset, `by` `usage`), says
|
||||
`binding.changed` per consumer (§21) and one `licence.switched` (§27), and tells the operator (§28). A consumer
|
||||
that inherits its node's licence (ADR 0183 §20) follows its node. A licence below its threshold, or with no
|
||||
consumer, moves nothing; a consumer is never moved back by the rule, only off a licence at its threshold, so no
|
||||
consumer flaps between two licences. Why: the operator, 2026-11-11 ("switch to a licence that still has room",
|
||||
"on all nodes"; the manager itself, never a session); the order of choice and the tie are the design's choice.
|
||||
How it is checked: unit, a table of licences, readings and bindings and the moves each gives; whole (manager, a
|
||||
fixture vendor), a reading of 99 % moves four bound consumers to the licence with the most room in one act.
|
||||
27. **Every switch is said as `licence.switched`.** The manager declares `licence.switched` in `emits` again and
|
||||
says it once per act that moves consumers, by `switch` or by §26: `from`, `to`, `consumers` (the list), `by` (a
|
||||
person's name or `usage`), `why`, and for §26 `window` (`session` or `weekly`), `pct`, `resetsAt` and the target's
|
||||
`room`. The same fields are the log entry `switched` (§7). Why: the operator, 2026-11-11 ("each switch said as an
|
||||
event"); this changes ADR 0206 §16 (Q2). How it is checked: unit, one event per act with the consumers moved.
|
||||
28. **Every switch the manager makes is told to the operator.** After an act of §26 the manager calls
|
||||
`operator-channel.tell` (ADR 0380): `subject` the seat `anthropic-licence-manager`, `headline` "<consumers> moved
|
||||
to <target>", and one line "<L> reached <pct> % of its <window> window, which resets at <time>; <target> has
|
||||
<room> % room". A switch by the `switch` verb is not told, since the person who asked knows it. Why: the operator,
|
||||
2026-11-11 ("told to the operator"); a notice that needs no answer is `tell` (ADR 0380). How it is checked: unit,
|
||||
§26 calls `tell` once per act, and `switch` never.
|
||||
29. **No licence with room is a condition, cleared when one has room.** When §26 finds no target for L, the
|
||||
manager writes the finding `licence.<L key>.no-room`, severity `urgent`: headline "No licence has room for
|
||||
<consumers>"; "Needs you: add a licence, or wait for <L>'s <window> window to reset at <time>"; explanation
|
||||
naming L's percentage and every other licence's room or why it is not known; resolved line "<consumers> have a
|
||||
licence with room again". The manager clears it when a later reading of L falls below `switch_at_pct`, when its
|
||||
consumers have moved (a target was found at a later reading of L), or when L has no consumer left. Why: the
|
||||
operator, 2026-11-11 ("a condition when no licence has room, cleared when one has"); its words are ADR 0253's. How
|
||||
it is checked: unit, with every licence at its threshold the finding is written once and cleared by a reading with
|
||||
room; whole, the condition keeper raises and clears it.
|
||||
30. **A licence near its limit is warned about once per crossing.** When a licence with one or more consumers has
|
||||
`sessionPct` or `weeklyPct` at or above `warn_at_pct`, the manager writes the finding `licence.<L key>.near-limit`,
|
||||
severity `warning`: headline "<L> at <pct> % of its <window> window"; "Nothing for you to do: <consumers> move to
|
||||
<target> at <switch_at_pct> %" while §26 would find a target, "Needs you: no other licence has room" when it would
|
||||
not; explanation naming the reset time. It is written again only when what it says changes (ADR 0289 §1), and
|
||||
cleared when both windows read below `warn_at_pct`. Why: issue 1409 (a warning before the limit, cleared when the
|
||||
reading falls back); ADR 0183 §21 (warned once per crossing of a declared threshold). How it is checked: unit, a
|
||||
rising series of readings writes the finding once at the crossing and again only when its words change; a reading
|
||||
after the reset clears it.
|
||||
31. **A licence whose usage cannot be read is a condition.** When a subscription's latest successful reading is
|
||||
older than `usage_stale_minutes`, the manager writes the finding `licence.<L key>.usage-unread`, severity
|
||||
`warning`, naming the vendor's reason from `readError`; "Needs you" only when consumers are bound to it ("<consumers>
|
||||
are not moved off it while its usage is unknown"); cleared at its next successful reading. Why: principle 3 (a
|
||||
switch that cannot see must say so). How it is checked: unit, three failed readings in a row write it, one
|
||||
success clears it.
|
||||
32. **The rule is started by hand too.** `licences_switch_now` (§39) runs §26 for one licence or all at once, from
|
||||
their latest readings, and `licences_read_usage` (§38) reads first; with `dry` true either answers what §26 would
|
||||
do and does nothing. Why: principle 4 (everything it does by itself can be started by hand). How it is checked:
|
||||
unit, `dry` moves nothing and answers the same moves as the act.
|
||||
33. **The settings, with their defaults, set through mesh-config.** The manifest's `config` gains, each `trusted`
|
||||
and read from the settings file at every reading so a change takes effect at the next one: `switch_auto`
|
||||
(boolean, default true: whether §26 moves consumers; off, §29 and §30 still raise), `switch_at_pct` (number 1 to
|
||||
100, default 99), `room_below_pct` (number 1 to 100, default 90: what a target's windows must both be below),
|
||||
`warn_at_pct` (number 1 to 100, default 90), `usage_minutes` (number 1 to 60, default 5) and
|
||||
`usage_stale_minutes` (number 5 to 180, default 15). The settings file's template and its `takes-config` list
|
||||
them beside today's five. A person sets one with `mesh-config.config`, at mesh or node level. When
|
||||
`room_below_pct` is above `switch_at_pct` or `warn_at_pct` is above `switch_at_pct`, the manager writes the finding
|
||||
`settings.<setting>.invalid` (warning, naming both values) and moves no consumer until it is fixed. Each change of
|
||||
a value it reads is written in the audit under `settings`. Why: the operator, 2026-11-11 ("make it configurable";
|
||||
99 by default); 90 as the room and the warning is the value the session used that night, so a licence warned
|
||||
about is never a target; the bounds are the design's choice. How it is checked: unit, each setting's default and
|
||||
bound, and the invalid pair moving nothing.
|
||||
|
||||
## The tools
|
||||
|
||||
*Each answers through the mesh MCP server; any person and agent sessions may call them. "Reads" and "acts" are each
|
||||
tool's effect (the manifest's mark).*
|
||||
|
||||
34. **`licences` shows every licence with its latest usage and room.** The seat verb answers, per licence, today's
|
||||
fields (`name`, `kind`, `identity`, expiries, `failures`, the consumers bound to it) and gains `usage` (§2's
|
||||
latest reading with its reset times and `readAt`), `room` (§25, or why it is not known), `target` (whether §26
|
||||
would choose it now), `adoptedAt`, `adoptedFrom` and the open findings about it. Reads. Why: principle 4; the
|
||||
operator's "read every licence with its latest usage and reset times". How it is checked: unit, an answer per
|
||||
licence carries each field and no token.
|
||||
35. **`usage` answers the history.** The seat verb takes `licence` (absent for all), `since` (a time) and `limit`
|
||||
(default 20, at most 2000) and answers entries of the log `usage`, newest first, the flattened reading and, with
|
||||
`raw` true, the vendor's answer. Reads. How it is checked: unit, `since` and `limit` bound the answer.
|
||||
36. **`switch`, `bind` and `release` keep their arguments.** Each also says §21's and §27's events and writes the
|
||||
audit with the caller as `by`. Acts. How it is checked: unit, as §21 and §27.
|
||||
37. **`licences_switches`: every switch and why.** A module tool reading the log `audit` under `switched`: `licence`,
|
||||
`consumer`, `since` and `limit` (default 20), newest first, each with `at`, `from`, `to`, `consumers`, `by`,
|
||||
`why` and, for §26, the reading that moved them. Reads. How it is checked: unit, a switch by `usage` and one by a
|
||||
person are answered with their causes.
|
||||
38. **`licences_read_usage`: read usage now.** Takes `licence` (absent for every subscription) and `dry`; reads as
|
||||
§24 does at once and then applies §26 unless `dry` is true; answers each reading and what §26 did or would do.
|
||||
Acts. How it is checked: unit, as §24 and §32.
|
||||
39. **`licences_switch_now`: apply the rule now.** Takes `licence` (absent for all) and `dry`; applies §26 from the
|
||||
latest readings, even when `switch_auto` is off, since a person asked; answers the moves. Acts. How it is checked:
|
||||
as §32.
|
||||
40. **`licences_settings`: the settings in force.** Answers each setting of §33 and today's five: its value, its
|
||||
default, where it was set (mesh, node or the default, as mesh-config answers it) and whether §33 finds it invalid.
|
||||
Reads. How it is checked: unit, a value set at node level is answered with its level.
|
||||
41. **`licences_kept`: every entry it keeps.** Takes `entry` (one of §2 to §9's local names; absent for all) and
|
||||
`key`; answers each entry's keys (or the subjects of a log with their last entry), one key's value, and its fill
|
||||
against its cap. Reads. Why: principle 4 and ADR 0564 §12 (a read of every bucket and stream a part keeps). How it
|
||||
is checked: a unit test lists every bus-state entry of the manifest and fails on one this tool does not read.
|
||||
42. **`licences_resync`: recover.** Says `binding.changed` again for every binding, `holdings.asked`, states its
|
||||
findings again through the writer (§9) and calls `licence-put` again for each licence's `access` (§15); answers
|
||||
what it said. Acts. Why: principle 4 (everything that goes wrong is recovered through its tools): a consumer, the
|
||||
vault or the keeper that missed an event converges on one call. How it is checked: unit, each is said once per
|
||||
binding, licence and finding.
|
||||
43. **`licences_findings`: its findings.** Answers each key of `findings` with its fields and `since`. Reads. Why:
|
||||
design 47 G §383 (the owner's read tool shows its newest findings). How it is checked: unit.
|
||||
44. **`licences_carried`: the carry-over's record, and its recovery.** Answers `carried.postgres` (§8), or, when it
|
||||
is absent, the failure `carry-over-failed` names; with `run` true and the key absent, starts §11 again; refused
|
||||
with the key present. Acts with `run`, reads without. How it is checked: unit, `run` is refused once carried.
|
||||
45. **`refresh` and `adopt` stay.** `refresh` (one licence or every due one, under its lease) and `adopt` (an API key
|
||||
from a file on the manager's node or sealed to its `public-key`) are unchanged but for reading and writing the
|
||||
grant in the vault (§15, §16). Acts.
|
||||
|
||||
## A further licence
|
||||
|
||||
46. **A subscription licence is added by logging in on any node.** A person logs in to the new account (a team seat
|
||||
among them) with the agent's own login on any node running `claude-code`. That node's `holdings-changed` (§23)
|
||||
carries a refresh token whose fingerprint the manager does not hold, so it is a candidate; the manager asks that
|
||||
node's `claude_code_grant` for it sealed to its key, refreshes it under the account's lease, and on success it is
|
||||
a new licence, named by its account's address (ADR 0206 §4, §7, §8; §10), its grant handed to the vault (§15). The
|
||||
node it came from is bound to it (ADR 0209); a person who wants that node back on its licence calls `switch`.
|
||||
From the next reading (§24) it has a room and is a target of §26. Nothing is configured and no tool beside the
|
||||
login is needed. Why: ADR 0206, ADR 0209. How it is checked: whole (manager, a fixture vendor), a report with a new
|
||||
fingerprint is adopted, listed by `licences` with its organisation, and chosen as a target at its first reading
|
||||
with room.
|
||||
47. **A team seat's licence is shown as any subscription.** `licences` answers its `organisation` beside its address;
|
||||
`kind` stays `subscription`. Whether the vendor's usage answer for a team seat carries the same windows is not
|
||||
known (a guess that it does); if it does not, §31's `usage-unread` says so and it is never a target. Why: the
|
||||
operator's new team-seat licence. How it is checked: live, after the login, `licences` lists it with a reading.
|
||||
48. **An API key is added with `adopt`**, from a node through `claude_code_add_api_key` or from a file on the
|
||||
manager's node (ADR 0209), and is never a target of §26 (§25). Why: ADR 0206 §6, ADR 0209.
|
||||
|
||||
## Delivery
|
||||
|
||||
49. **One pull request in mesh-catalog, with hq.** The three modules change in one mesh-catalog pull request; hq's
|
||||
pull request carries this part, the main file and the glossary. Why: one milestone, one release (the operator's
|
||||
instruction on designing a change as it should be).
|
||||
50. **The order is the module graph's.** The edges the delivery plan derives: the bus module creates the manager's
|
||||
bus-state entries before its want moves (the provision `bus-state`, ADR 0570 §4); `mesh-vault`'s `licence-put`
|
||||
and its consumption of `binding.changed` are served before the manager calls them (a served verb); `claude-code`
|
||||
declares `holdings-changed` before the manager consumes it (ADR 0595 §11's judge refuses the reverse); and the
|
||||
finding writer of mesh-sdk and the keeper's consumption of `finding-events` are design 47's milestone G, delivered
|
||||
before this one. A node's agent keeps its last token, which lives hours, while the manager moves.
|
||||
51. **Tonight's interim rule ends with the delivery.** Until the delivery, a session follows the readings and calls
|
||||
`switch` (issue 1409's note of 2026-11-11 20:11); once §26 runs live, that session stops, said on issue 1409.
|
||||
|
||||
## Acceptance
|
||||
|
||||
52. **The tests of the milestone**, each run in its module's suite or live after the delivery:
|
||||
- every row of today's database is carried over, counted in `carried.postgres`, and a second start carries
|
||||
nothing (§11 to §13);
|
||||
- after the delivery `licences_kept` and the vault's `secret_holders` show no token outside the vault (§20);
|
||||
- a reading at `switch_at_pct` moves every consumer bound to that licence to the licence with the most room,
|
||||
says one `licence.switched` and one `binding.changed` per consumer, and tells the operator once (§26 to §28);
|
||||
- with every licence at its threshold, `no-room` is raised once and cleared by a reading with room (§29);
|
||||
- a licence crossing `warn_at_pct` is warned about once (§30), and one that cannot be read is said (§31);
|
||||
- each setting of §33 changes the rule at the next reading;
|
||||
- every entry the manager keeps is read by `licences_kept`, and every act it takes by itself is started by a tool
|
||||
(§37 to §45);
|
||||
- live: a login to a new account on a node is adopted and listed with its usage (§46).
|
||||
@@ -1,10 +1,20 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-catalog modules/claude-licence-manager, mesh-vault]
|
||||
updated: 2026-10-26
|
||||
code: [mesh-catalog modules/claude-licence-manager, mesh-catalog modules/claude-code, mesh-catalog modules/mesh-vault]
|
||||
updated: 2026-11-11
|
||||
decisions:
|
||||
- "ADR 0550"
|
||||
- "ADR 0595"
|
||||
- "ADR 0596"
|
||||
- "ADR 0437"
|
||||
- "ADR 0564"
|
||||
- "ADR 0570"
|
||||
- "ADR 0230"
|
||||
- "ADR 0380"
|
||||
- "ADR 0253"
|
||||
- "ADR 0289"
|
||||
- "ADR 0349"
|
||||
- "ADR 0209"
|
||||
- "ADR 0206"
|
||||
- "ADR 0183"
|
||||
@@ -29,24 +39,18 @@ which is the consumer on every node.
|
||||
|
||||
## Every grant in the vault
|
||||
|
||||
*Decided 2026-10-26 (ADR 0550 §3), on the operator's answer that every secret lives in the vault; built in
|
||||
ADR 0547's second milestone (issue 1126). Until it is delivered, the sections below hold as first written.*
|
||||
*Decided 2026-10-26 (ADR 0550 §3), on the operator's answer that every secret lives in the vault; built in milestone
|
||||
bus-and-switch (§15 to §20 of its part, [39-milestones/bus-and-switch.md](39-milestones/bus-and-switch.md)), which
|
||||
waits on the vault's own-account rule (its open question Q1). Until it is delivered, the sections below hold as first
|
||||
written.*
|
||||
|
||||
- **The vault keeps every licence's secrets**: the refresh tokens, the API key and the access tokens are vault
|
||||
secrets ([24](24-the-secrets-vault.md)). The manager keeps its bindings, its licence records without a secret
|
||||
(account, kind, health, usage, audit), and nothing at rest that is secret.
|
||||
- **The manager stays the one rotation source.** It alone calls the vendor's token endpoint: to refresh or adopt,
|
||||
it asks the vault for the grant sealed to itself and hands what it gets back to the vault. A grant offered at
|
||||
login, sealed to the manager's key (its `public-key` verb is kept), is handed on to the vault the same way.
|
||||
- **A holder's token comes from the vault.** The agent module on a node registers its public half with the vault,
|
||||
not the seat, and asks the vault for its token sealed to it. The vault serves only the licence the manager's
|
||||
binding names for that holder, and says `secret-changed`, never a value. The seat's `current` verb answers which
|
||||
licence a consumer holds and its health, never the token.
|
||||
- **It waits for accounts of their own.** The vault hands a value only to a module running under a bus account of
|
||||
its own (issue 569), so the move waits until the manager and each agent module run under one.
|
||||
|
||||
*How it is checked:* a unit test that the vault refuses a token to a holder no binding names or to one without an
|
||||
account of its own, and a scan after the move that the manager's tables hold no grant.
|
||||
- **The vault keeps every licence's secrets**, two per licence: its grant, and the access token a consumer is handed
|
||||
([24](24-the-secrets-vault.md)). The manager keeps its bindings and its licence records without a secret.
|
||||
- **The manager stays the one rotation source**: it reads a grant from the vault for one act and hands what the
|
||||
vendor answers back to it, keeping nothing at rest.
|
||||
- **A consumer's token comes from the vault**, which serves only the licence the manager's binding names for that
|
||||
consumer, learned from the manager's `binding.changed`; the seat's `current` answers which licence and its health,
|
||||
never the token.
|
||||
|
||||
## 1. What it is
|
||||
|
||||
@@ -55,19 +59,24 @@ holder, on the node the operator assigns it to — the control node is the natur
|
||||
definition says so. It requires a database for its own store and the bus; it claims the seat; it serves
|
||||
the seat's verbs. It has no port, no route, no file under anyone's home.
|
||||
|
||||
Its store holds four things:
|
||||
**It keeps all its data in the bus's own storage, by the kept-data templates, and requires no database**
|
||||
(ADR 0595, ADR 0596; the operator, 2026-11-11). Built in milestone bus-and-switch (§1 to §14 of its part), which carries
|
||||
today's store database over at the manager's first start and then retires it on the operator's Approve:
|
||||
|
||||
| table | holds |
|
||||
|---|---|
|
||||
| **licences** | name, kind (`subscription` or `api-key`), the account's identity (id, address, organisation) once adopted, the grant encrypted at rest, when the access token expires, when the refresh token expires, consecutive failures, the refresh lease, when a person was last notified |
|
||||
| **bindings** | one row per consumer: kind (`node-agent`, `node-session`, `worker`), its key (the node, or the node and the worker), the licence, or *inherit* |
|
||||
| **usage** | the vendor's readings per licence per period, raw beside normalised (ADR 0054) |
|
||||
| **audit** | every switch, adoption, refusal and drift, with who asked |
|
||||
| entry | template | holds |
|
||||
|---|---|---|
|
||||
| `licences` | state | one key per licence: kind, the account's identity, expiries, failures, its latest usage reading with its reset times; never a secret |
|
||||
| `bindings` | state | one key per consumer: its licence, kind, generation, and who or what moved it last |
|
||||
| `leases` | state | one key per lease, ended by its holder or its two-minute lifetime |
|
||||
| `offered` | state | one key per grant a node offered, with its outcome |
|
||||
| `usage` | log | every reading, raw beside normalised (ADR 0054), 35 days |
|
||||
| `audit` | log | every adoption, refusal, rotation, binding, switch and setting, kept for good |
|
||||
| `carried` | state | the record of the one carry-over from the store database |
|
||||
| `findings` and `finding-events` | state and events | its findings for the condition keeper |
|
||||
|
||||
**The grants are encrypted with a key the vault made for the manager** — its one `secret` requirement.
|
||||
The vault keeps that key; the manager keeps the grants. That is ADR 0050's carve-out, one module, one
|
||||
node, the long-lived grants only. ADR 0550 §3 ends the carve-out: the grants move into the vault, and the
|
||||
`licences` table keeps no grant (*Every grant in the vault*, above).
|
||||
Every grant and token is the vault's (*Every grant in the vault*, above). Until the milestone is delivered, the
|
||||
module keeps its store database, its tables `licence` (with the grant encrypted at rest with a key the vault made for
|
||||
the manager, ADR 0050's carve-out), `binding`, `lease`, `offered`, `usage` and `audit`.
|
||||
|
||||
## 2. The licences it manages today
|
||||
|
||||
@@ -135,12 +144,15 @@ own, with its own agent directory and credentials file, which the agent module o
|
||||
it as it writes the operator's — the predecessor ran its agents exactly so.
|
||||
|
||||
**Binding is a person's act through the seat's verbs**, listed by the mesh MCP server: `bind`, `switch`,
|
||||
`release`. **Exhaustion is observed, not acted on**: usage is read every few minutes, a crossing of a
|
||||
declared threshold in the five-hour window is notified once per crossing, and moving a consumer is the
|
||||
operator's call. Switching remains a reaction, not a declaration
|
||||
(ADR 0024), and an automated policy — move to the
|
||||
least-used licence, stay off a dying one — is designed later if wanted, on the readings this module
|
||||
already keeps.
|
||||
`release`. **A licence at its threshold is left by the manager itself** (the operator, 2026-11-11: "when the current
|
||||
licence usage is at 99%, switch to a licence that still has room", "on all nodes"; built in milestone
|
||||
bus-and-switch, §24 to §33, which waits on a record changing ADR 0183 §21 and ADR 0206 §16, §18, its open question
|
||||
Q2): on each of its own usage readings, a licence whose session or weekly usage reaches the threshold (99 % by
|
||||
default) has every consumer bound to it moved to the licence with the most room; each switch is said as
|
||||
`licence.switched` and told to the operator; a licence near its limit is warned about once per crossing, and no
|
||||
licence with room is a condition until one has room. The threshold, the room a target needs and the switch itself
|
||||
are settings. Switching stays a reaction to a reading, never a declaration (ADR 0024). Until the milestone is
|
||||
delivered, exhaustion is observed and moving a consumer is the operator's call.
|
||||
|
||||
## 6. Adopting a grant
|
||||
|
||||
@@ -180,9 +192,14 @@ report, and adopted by refreshing it.
|
||||
|
||||
**Events**, no secret in any: `licence.adopted`, `licence.failing`, `licence.refused`, `usage.read` —
|
||||
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.
|
||||
gone; a rotation or a switch is a new generation in the `bindings` state. *Milestone bus-and-switch:*
|
||||
`binding.changed` says every change of a binding, `licence.switched` comes back for every switch, `holdings.asked`
|
||||
asks every node's agent module for its holdings, and its findings go on `finding-events` (§9, §21, §23, §27 of its
|
||||
part).
|
||||
|
||||
**State**: `bindings`, which it keeps; the agent module's `holdings`, which it reads.
|
||||
**State**: `bindings`, which it keeps; the agent module's `holdings`, which it reads. *Milestone bus-and-switch:*
|
||||
the kept data of §1, and no other module's state read: the agent module's holdings are heard as its
|
||||
`holdings-changed`, and the agent module follows its binding by `binding.changed` (ADR 0596).
|
||||
|
||||
**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
|
||||
@@ -191,10 +208,17 @@ or all), `usage` (current and history), `adopt` (a file on the manager's node, o
|
||||
consumer sends — ADR 0206; from ADR 0550 §3, which licence a consumer holds and its health, never the
|
||||
token, which the vault hands). The manager asks a node for a candidate grant by the agent module's own tool.
|
||||
|
||||
The module's own tools beside the verbs (milestone bus-and-switch, §37 to §44): every switch and why, the settings
|
||||
in force, usage read now, the switch rule applied now, every entry it keeps with its fill, its findings, the
|
||||
carry-over's record, and one recovery that says every binding, finding and access token again.
|
||||
|
||||
## 8. Settings
|
||||
|
||||
The refresh cadence; the usage threshold; the notification cooldown. Each declared with a default, so
|
||||
one definition serves and one mesh may differ.
|
||||
one definition serves and one mesh may differ. *Milestone bus-and-switch* (§33 of its part) adds whether the manager
|
||||
switches by itself, the threshold that moves consumers (99), the room a target needs (both windows below 90), the
|
||||
warning's threshold (90), the reading's cadence (5 minutes) and when a reading is too old (15 minutes), each set
|
||||
through mesh-config.
|
||||
|
||||
## How it is checked
|
||||
|
||||
@@ -208,10 +232,20 @@ one definition serves and one mesh may differ.
|
||||
| the mesh MCP server lists the seat's verbs and `switch` changes a workstation's token end to end | ADR 0132, the exit of the build |
|
||||
| the vault refuses a token to a holder no binding names, or to one without a bus account of its own | ADR 0550 §3 |
|
||||
| after the move, the manager's tables hold no grant | ADR 0550 §3 |
|
||||
| every row of the store database is carried over once, and a second start carries nothing | bus-and-switch §11 to §13 |
|
||||
| a reading at the threshold moves every consumer of that licence to the one with the most room, said as an event and to the operator | bus-and-switch §26 to §28 |
|
||||
| no licence with room raises one condition, cleared by a reading with room; a licence near its limit is warned about once | bus-and-switch §29, §30 |
|
||||
| every entry the manager keeps is read by a tool, and every act it takes by itself is started by one | bus-and-switch §37 to §45 |
|
||||
|
||||
## Milestones
|
||||
|
||||
| Milestone | What it builds | State |
|
||||
|---|---|---|
|
||||
| the manager and its consumer | §1 to §8 as first written: the module, its seat, its store database, adoption and the sealed hand-over (ADR 0183, ADR 0206, ADR 0209) | delivered; built as design [40](40-building-the-operators-agent-and-its-licence-manager.md) describes |
|
||||
| bus-and-switch | its kept data on the bus and its store database carried over and retired; every grant and token in the vault (ADR 0550 §3); bindings and holdings as events; the switch on its own usage, its warning and its condition; its tools. mesh-catalog's `claude-licence-manager`, `claude-code` and `mesh-vault` in one pull request, after design 47's milestone G | designed; its part is [`39-milestones/bus-and-switch.md`](39-milestones/bus-and-switch.md), with three open questions (Q1 to Q3) |
|
||||
|
||||
## What this does not settle
|
||||
|
||||
- An automated switch on exhaustion (§5).
|
||||
- Whether an OAuth refresh token is single-use; the lab measures it, and §3 holds either way.
|
||||
- How the mesh's own session and a worker read their token on a node once those exist
|
||||
([to-be 15](15-the-agent-session.md), ADR 0003):
|
||||
|
||||
Reference in New Issue
Block a user