Merge pull request 'Design 39 bus-and-switch: the licence manager keeps its data on the bus and switches on its own usage' (#557) from design/39-licence-switch into main
This commit was merged in pull request #557.
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,449 @@
|
||||
---
|
||||
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).
|
||||
|
||||
**The lead's answers, by the principles.** Four questions no accepted record settled were answered on 2026-11-11 by
|
||||
the lead of issue 1409, against hq's principles, as the operator's way of working since 21:15 that evening has it (the
|
||||
lead answers develop and design questions against the principles; no decision record is written for a detail). Each
|
||||
line they reach names its answer:
|
||||
|
||||
- **A1, the vault's own-account rule** (§15 to §17). ADR 0550 §3 waits until the licence manager and each agent
|
||||
module run under a bus account of their own (ADR 0349 §11, issue 569); both run in their machine's tool runner,
|
||||
under its account. Settled by principle 6 (inside the mesh every agent and module is trusted; who may do what is not
|
||||
a question) and the operator's instruction that who may do what is not the operator's question (approved
|
||||
2026-11-11 21:15 CET): the vault serves licence secrets to the tool runner's account. Its check that the asking
|
||||
machine is the node the binding names stays, as a guard against mistakes, not as a permission.
|
||||
- **A2, the switch without a record** (§24 to §33). 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
|
||||
decided, 2026-11-11 about 21:10 to 22:20 CET: switch automatically at 99 %, on all nodes, configurable; and said that
|
||||
night not to write records for what they decided. So the operator's answer of 2026-11-11 replaces those three
|
||||
clauses for this module, and no record is written.
|
||||
- **A3, how the first start reads today's rows** (§1, §11, §14). A build whose manifest no longer requires
|
||||
`postgres-database` is composed with no credential for it, and `mesh-store.query` refuses the grant column. Settled
|
||||
by principle 7 (the simplest thing that works): this milestone keeps `requires postgres-database`, and the `secret`
|
||||
`grant-key` that opens the grants at rest, as the carry-over's read-only source; dropping both, and the database's
|
||||
retirement on the operator's Approve, is the module's next change once `licences_carried` (§44) shows the
|
||||
carry-over done. The delivery gap this names: no path hands a build the credential of a provision its manifest
|
||||
dropped, so a module cannot read its retiring database at the first start of its new form.
|
||||
- **A4, no wait for design 47's milestone G** (§9, §50). Settled by principle 7: this milestone ships soon, built only
|
||||
on what main holds today (mesh-sdk, the vault, the operator channel, and today's way a module says a condition,
|
||||
ADR 0437 §9), so it uses neither G's writer nor its stream.
|
||||
|
||||
## The kept data on the bus
|
||||
|
||||
1. **The manifest asks for its kept data by template, and keeps its database only as the carry-over's source.**
|
||||
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. `requires` keeps `postgres-database` and the `secret`
|
||||
`grant-key`, with their `contributes`, `binds`, `secrets` and the resource `database-url`, read only by §11 and
|
||||
never written (A3); the run-once step `prepare`, which makes the tables, goes, since nothing writes them again.
|
||||
`uses` gains `operator-channel` (§9, §28). Why: the operator, 2026-11-11, and principle 1; the templates are
|
||||
ADR 0595 §2; A3. How it is checked: the manifest validator accepts the manifest at the merge check; a catalog test
|
||||
finds no `reads`, old `state` or `prepare` in it; a unit test finds no write to the database in the code.
|
||||
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. **Its conditions are said on the operator channel, and kept in its own state `conditions`.** The manager says
|
||||
each condition of §11, §29 to §31 and §33 as any module does on main today (ADR 0437 §9): published on
|
||||
`mesh.seat.operator-channel.accept.condition.claude-licence-manager` (the subject the operator channel's contract
|
||||
names, through its client in mesh-contracts), carrying the condition's `key`, `severity` (`urgent` or `warning`),
|
||||
`scope` and `id`, `raised` or `cleared`, its `summary` and ADR 0253's words (`headline`, `needs`, `explanation`
|
||||
opened by its verdict, `resolved` and `actions`); the router alone decides whether and where the operator is told.
|
||||
It keeps each condition it said raised in its state `conditions` (template `state`, option `history` 1, one key per
|
||||
condition key, its body and `since`), written before it is said and deleted when it is said cleared. At its start
|
||||
it judges each kind anew from its own state and readings, says raised again each it still holds and cleared each
|
||||
key of `conditions` it no longer holds. A raise or clear is said again only when what it says changes (ADR 0289
|
||||
§1). Why: A4 (today's way a module raises a condition, without G's writer); the state lets a restart clear what
|
||||
it said. How it is checked: unit, a condition is written in `conditions` and published once with these fields;
|
||||
a start with a key it no longer holds says it cleared; whole (manager and the router on a test bus), a raised
|
||||
condition reaches the desk channel 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 says the condition `carry-over-failed` (§9; 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 is the database read with the module's own credential, which this milestone keeps for it alone (A3). 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 condition is raised and no vendor call is made. A table that does not
|
||||
exist (a fresh install) is read as no rows.
|
||||
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.** In this milestone the database `claude_licences`
|
||||
stays, read once by §11 and never written. Once `licences_carried` (§44) shows the carry-over done and §20's scan
|
||||
finds no grant left outside the vault, the module's next change drops `requires postgres-database` and
|
||||
`grant-key`; the provision is then retiring, its account removed only on the operator's Approve (ADR 0570 §5),
|
||||
and 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: A3,
|
||||
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` still lists `claude_licences`.
|
||||
|
||||
## Grants and tokens in the vault
|
||||
|
||||
*Lines §15 to §20 build ADR 0550 §3, on the tool runner's account (A1).*
|
||||
|
||||
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; answered for a call from the machine that holds the seat `anthropic-licence-manager`
|
||||
(as the seat's placement names it), and refused from any other machine naming the rule, a guard against a
|
||||
mistaken caller rather than a permission (A1); 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 machine 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` to the tool runner's account of a machine,
|
||||
and only when that machine is the node the asking consumer's binding names and the licence is the one that
|
||||
binding names: a guard against a node applying another's licence by mistake, not a permission (A1). Its rule that a
|
||||
value goes only to a module under a bus account of its own (ADR 0349 §11) does not apply to the licence
|
||||
secrets. 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; A1; 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 answer of 2026-11-11, which replaces ADR 0183 §21 and ADR 0206 §16 and §18 for
|
||||
this module (A2): the manager moves consumers by itself on its own readings, and says every switch as an event.*
|
||||
|
||||
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 replaces ADR 0206 §16 for this module (A2). 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 says the condition `licence.<L key>.no-room` (§9), 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 condition is said once and cleared by a reading with
|
||||
room; whole, the router takes its raise and its clearing.
|
||||
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 says the condition `licence.<L key>.near-limit` (§9),
|
||||
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 says the condition 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 says the condition `licence.<L key>.usage-unread` (§9), 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 says the condition `settings.<setting>.invalid` (§9) (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 conditions 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`, says again each condition it holds (§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 condition.
|
||||
43. **`licences_conditions`: the conditions it said.** Answers each key of `conditions` (§9) with its body and
|
||||
`since`. Reads. Why: principle 4 (everything it holds can be read). 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); and `claude-code`
|
||||
declares `holdings-changed` before the manager consumes it (ADR 0595 §11's judge refuses the reverse). It waits for
|
||||
no other milestone: the operator channel's condition subject, the vault and mesh-sdk are used as main holds them
|
||||
(A4). 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)), the vault
|
||||
serving the licence secrets to the tool runner's account (principle 6; its answer A1). 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,25 @@ 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, reading it and never writing it; the module's next change
|
||||
drops the database, which is then retired on the operator's Approve (principle 7; its answer A3):
|
||||
|
||||
| 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 |
|
||||
| `conditions` | state | each condition it said on the operator channel, so a restart can clear what it said |
|
||||
|
||||
**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 +145,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; the operator's answer replaces ADR 0183 §21 and ADR 0206 §16, §18 for this module, with
|
||||
no record, its answer A2): 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 +193,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 conditions are said on the operator channel (§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 +209,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, the conditions it
|
||||
said, the carry-over's record, and one recovery that says every binding, condition 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 +233,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, on what main holds today | designed; its part is [`39-milestones/bus-and-switch.md`](39-milestones/bus-and-switch.md), its four questions answered by the principles (A1 to A4) |
|
||||
|
||||
## 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