|
|
|
@@ -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).
|