|
|
|
@@ -0,0 +1,226 @@
|
|
|
|
|
---
|
|
|
|
|
layer: to-be
|
|
|
|
|
design: 39
|
|
|
|
|
milestone: parts
|
|
|
|
|
updated: 2026-11-12
|
|
|
|
|
code: [mesh-catalog modules/claude-licence-manager]
|
|
|
|
|
decisions:
|
|
|
|
|
- "ADR 0183"
|
|
|
|
|
- "ADR 0050"
|
|
|
|
|
- "ADR 0054"
|
|
|
|
|
- "ADR 0040"
|
|
|
|
|
- "ADR 0192"
|
|
|
|
|
- "ADR 0198"
|
|
|
|
|
- "ADR 0259"
|
|
|
|
|
- "ADR 0349"
|
|
|
|
|
- "ADR 0436"
|
|
|
|
|
- "ADR 0570"
|
|
|
|
|
- "ADR 0230"
|
|
|
|
|
- "ADR 0595"
|
|
|
|
|
- "ADR 0672"
|
|
|
|
|
- "ADR 0675"
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# Design 39, milestone parts — the licence manager built of its parts
|
|
|
|
|
|
|
|
|
|
**What it builds.** The module holding the seat `anthropic-licence-manager` is one manifest built of four parts,
|
|
|
|
|
each in a form the manifest validator accepts and the builder, the node-engine and the tool runner honour today
|
|
|
|
|
([design 20](../20-writing-a-module.md) *Who reads a manifest* and *What a module is built of*; playbook
|
|
|
|
|
[06](../../../00-META/process/06-writing-a-module.md) *Where the parts of the mesh read a manifest differently*): the
|
|
|
|
|
manager bundle, which runs long and answers the seat's verbs and every act; the tools bundle, which answers the
|
|
|
|
|
module's reading tools; the settings file; and the module's account with its directory. The carry-over from the store
|
|
|
|
|
database, done live, leaves the module whole in this milestone, with its requirement of the database and of the secret
|
|
|
|
|
`grant-key`, and both provisions then retire on the operator's Approve. 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
|
|
|
|
|
parts §n".
|
|
|
|
|
|
|
|
|
|
**Why now.** The operator, 2026-11-12: "build the anthropic-licence-manager as clean as possible"; a module is one
|
|
|
|
|
manifest with several parts; the seat stays named for the vendor (ADR 0183 §4, ADR 0050, ADR 0054), and a second
|
|
|
|
|
vendor is a module of its own. That morning the operator had said "the licence manager module should have a service
|
|
|
|
|
part and a tools part" (design 39 bus-and-switch C1). Today, after milestone bus-and-switch, one Go bundle `code` does
|
|
|
|
|
everything in one program: it runs long, answers the seat's ten verbs and the module's eight tools, and keeps the
|
|
|
|
|
carry-over's code, which ran once, live, at 2026-11-12 09:44:59Z, carrying every row of every table (issue 1409's note
|
|
|
|
|
of that morning); for that code alone the module still requires `postgres-database` and the secret `grant-key`
|
|
|
|
|
(bus-and-switch A3, B15).
|
|
|
|
|
|
|
|
|
|
**Where.** One repository, mesh-catalog, and one of its modules, `claude-licence-manager`; hq carries this part, the
|
|
|
|
|
main file and the glossary. No other module changes: every verb, event and provision the manager uses is served today
|
|
|
|
|
(§17).
|
|
|
|
|
|
|
|
|
|
**The shape at a glance.**
|
|
|
|
|
|
|
|
|
|
| a module's part | its artifact | how it runs | its account | what it keeps or reads |
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
| the manager | `manager`, a Go bundle, binary `claude-licence-manager` | loaded by the module's runtime of its own; runs long and answers the seat's ten verbs and the three acting tools | the module's account `claude-licence-manager`, on the runtime's bus account | writes all its kept data (bus-and-switch §1 to §9) and its two logs; reads its key pair and settings file |
|
|
|
|
|
| the tools | `tools`, a Go bundle, binary `claude-licence-manager-tools` | loaded by the same runtime; answers the four reading tools | the same | reads the kept data and the logs, writes nothing |
|
|
|
|
|
| the settings | `files`, an archive of the templates directory | the `settings` file resource, written by the node-engine | owned by the module's account | holds the eleven settings mesh-config sets |
|
|
|
|
|
| the account and its directory | none | the `user` resource `account` and the `directory` resource `state` | `claude-licence-manager` | the key pair, the settings file, the runtime's `broker` secret |
|
|
|
|
|
|
|
|
|
|
## The module's parts
|
|
|
|
|
|
|
|
|
|
1. **The module is four parts in one manifest, and runs no process resource, container or run-once step of its
|
|
|
|
|
own.** `build.artifacts` names exactly `manager`, `tools` and `files`; `resources` names exactly `account`,
|
|
|
|
|
`state` and `settings`, in that order; the module's runtime of its own is the process the node-engine composes for
|
|
|
|
|
`runs-as` (design 20 *A runtime of its own*), never one the manifest writes. Why: the operator, 2026-11-12; a module
|
|
|
|
|
is one piece of software built of parts (ADR 0040); principle 7 (fewer parts). How it is checked: the manifest
|
|
|
|
|
validator accepts the manifest at the change's check, and the composition step composes it for the manager's
|
|
|
|
|
machine; a catalog test of the module reads its manifest and fails on any other artifact or resource, a `process`
|
|
|
|
|
or `container` resource, or a `run-once` field.
|
|
|
|
|
2. **The manager bundle runs long and answers the seat's verbs.** The artifact `manager` (today's `code`, renamed):
|
|
|
|
|
kind `bundle`, `language` go, `system` arch, `from` `cmd/manager`, `binary` `claude-licence-manager`, `loads`
|
|
|
|
|
that binary, and `env` with exactly two words, `MESH_LICENCE_STATE` (`${dir:state}`) and `MESH_LICENCE_SETTINGS`
|
|
|
|
|
(`${dir:state}/settings.json`). It is loaded by the module's runtime of its own and runs long (ADR 0198 §1, §2: a
|
|
|
|
|
bundle may serve tools, run long, or both): its usage timer, its refresh cadence, the switch rule on each reading,
|
|
|
|
|
its consumption of `claude-code.holdings-changed`, the conditions it judges at its start, and every event of
|
|
|
|
|
bus-and-switch §21 to §27. It registers the seat's ten verbs (`licences`, `bindings`, `bind`, `switch`,
|
|
|
|
|
`release`, `refresh`, `usage`, `adopt`, `current`, `public-key`) under the seat and the three acting tools of §4
|
|
|
|
|
under the module. It is the only part that writes the module's kept data and logs, emits, calls the vendor, calls
|
|
|
|
|
the vault, tells the operator and reads the key pair. Why: one rotation source (ADR 0183 §6); the binary keeps its
|
|
|
|
|
name so the runtime's journal and the loaded entrypoint stay as they are. How it is checked: unit tests as
|
|
|
|
|
bus-and-switch has them, moved with the code; the catalog test of §1 reads the artifact's fields.
|
|
|
|
|
3. **The tools bundle answers the module's reading tools, and writes nothing.** The artifact `tools`: kind `bundle`,
|
|
|
|
|
`language` go, `system` arch, `from` `cmd/tools`, `binary` `claude-licence-manager-tools`, `loads` that binary, and
|
|
|
|
|
no `env`: the runtime hands a module's words to all its bundles (ADR 0192), so it reads `MESH_LICENCE_SETTINGS`
|
|
|
|
|
from the manager bundle's words, and a word two bundles gave different values would be refused at composition. It
|
|
|
|
|
registers under the module the four reading tools, `licences_switches`, `licences_settings`, `licences_kept` and
|
|
|
|
|
`licences_conditions`, with the arguments and answers of bus-and-switch §37, §40, §41 and §43. It reads the
|
|
|
|
|
module's kept data with the runtime's bus-state calls and the logs `usage` and `audit` with the runtime's log
|
|
|
|
|
calls, each answered as the module whose bundle asks; it calls `seat:mesh-config.config` for `licences_settings`
|
|
|
|
|
(bus-and-switch B18). It writes no key, appends to no log, emits nothing, subscribes to no event, asks no
|
|
|
|
|
heartbeat, calls neither the vendor nor the vault and opens neither the key pair nor a secret. Why: principle 4
|
|
|
|
|
(everything a module holds can be read): the runtime serves the tools of every bundle of a module that loaded, so
|
|
|
|
|
these four still answer while the manager bundle fails to load or stops; C1 (the tools act only through the running
|
|
|
|
|
manager) holds, since these do not act; principle 1 governs reading between modules, and both bundles are the one
|
|
|
|
|
module reading its own data. How it is checked: a unit test of the tools bundle runs each tool against a fixture
|
|
|
|
|
of the runtime's calls and fails on any write, append, emit or subscribe; whole, with the manager bundle refusing to
|
|
|
|
|
start, `licences_kept` answers every entry.
|
|
|
|
|
4. **Every act is the manager's.** The three tools that act, `licences_read_usage`, `licences_switch_now` and
|
|
|
|
|
`licences_resync` (bus-and-switch §38, §39, §42), are registered by the manager bundle beside the seat's verbs, and
|
|
|
|
|
no act is answered by the tools bundle. A person's act on a consumer or a licence stays a seat verb (`bind`,
|
|
|
|
|
`switch`, `release`, `refresh`, `adopt`). Why: C1 (no tool keeps a second copy of what the manager does);
|
|
|
|
|
ADR 0183 §6; principle 7 (no call between the two bundles, so no path the mesh lacks). How it is checked: the
|
|
|
|
|
catalog test of §1 finds the module's `tools` to be exactly the four of §3 and the three of this line; a unit test of each
|
|
|
|
|
bundle lists what it registers.
|
|
|
|
|
5. **The code both bundles share is one package of the module.** The package `internal/licences`, in the module's
|
|
|
|
|
one Go module, holds the layouts of the kept data and of the log entries (bus-and-switch §2 to §9), the key form
|
|
|
|
|
(§10), the names of the entries, and the settings with their defaults, bounds and validity (§33, B37); `cmd/manager`
|
|
|
|
|
and `cmd/tools` each import it, and neither holds a copy of what it holds. Why: a layout in one place, so the reading
|
|
|
|
|
tools never read a form the manager no longer writes; it is code the module owns alone (design 20 *Where a seat's
|
|
|
|
|
code goes*: a module's own package). How it is checked: the review reads that no type of a kept entry is declared
|
|
|
|
|
in either command package; `go vet ./...` at the module's root builds both.
|
|
|
|
|
6. **The settings file stays, without the database's template.** The resource `settings` (a `file` at
|
|
|
|
|
`${dir:state}/settings.json`, mode 0600, owner `claude-licence-manager`, `template` `files/settings.json`,
|
|
|
|
|
`merge` json, `takes-config` the eleven settings of bus-and-switch §33 and today's five) and the archive `files`
|
|
|
|
|
(`from` `files`, the one archive the templates are read from, ADR 0349 §2) stay as they are. The template
|
|
|
|
|
`files/database.url` is deleted. Why: the manager reads its settings at every reading (bus-and-switch §33); the
|
|
|
|
|
database's address has no reader once §9 holds. How it is checked: the composition step writes the file; the catalog
|
|
|
|
|
test of §1 finds no file under `files` but `settings.json`.
|
|
|
|
|
7. **The account, its directory and the runtime's secret stay.** `runs-as` `claude-licence-manager`, `secrets-owner`
|
|
|
|
|
the same, the own secret `broker` at `${dir:state}/broker`, the `user` resource `account` and the `directory`
|
|
|
|
|
resource `state` (mode 0700, owner the account, `place` `.`, `unknown` `program`) stay as they are; the key pair
|
|
|
|
|
`manager-key.json` stays in that directory, read and made only by the manager bundle (C2). No `runtime-health` is
|
|
|
|
|
added: the node-engine's judgement that the runtime is alive stands as its health. Why: design 20 *A runtime of its
|
|
|
|
|
own*; principle 7. How it is checked: the catalog test of §1.
|
|
|
|
|
8. **The seat's verbs are answered on the bus account of the runtime of its own, never by a process account.** The
|
|
|
|
|
manager bundle answers the verbs through the runtime that `runs-as` gives the module (ADR 0259 §8; ADR 0436 §5:
|
|
|
|
|
`runs-as` is unchanged and still gets a runtime of its own), as the router answers the operator channel's. The
|
|
|
|
|
manager is not a `process` resource with `bus`: a process account is granted no subject on which a tool or a seat's
|
|
|
|
|
verb is answered and holds no seat (ADR 0436 §3), which only ADR 0672 §7 lifts, for the recovery module, and
|
|
|
|
|
ADR 0675 §1 and §2, for the bus module's keeper; and a process account's grant names events, work queues, states,
|
|
|
|
|
asks and calls (ADR 0436 §1), never the old-section logs `usage` and `audit` the manager appends to (C3). Why: these
|
|
|
|
|
records. How it is checked: the catalog test of §1 finds no `process` resource and no `bus` key in the manifest;
|
|
|
|
|
whole, after the delivery, `anthropic-licence-manager.licences` answers through the mesh MCP server.
|
|
|
|
|
|
|
|
|
|
## The carry-over leaves
|
|
|
|
|
|
|
|
|
|
9. **The carry-over is done, and its code leaves the module now.** The carry-over (bus-and-switch §11 to §13) ran on
|
|
|
|
|
2026-11-12 at 09:44:59Z, every table's count equal and no lease live (issue 1409's note of 10:4x CET). In this
|
|
|
|
|
milestone the code that reads the store database, the carry-over itself, the opening of grants at rest with
|
|
|
|
|
`grant.key`, the gate that answers "the carry-over has not ended", the condition `carry-over-failed` and its
|
|
|
|
|
recovery go, with the postgres driver in the module's `go.mod`. No run-once step is built for it: no row is left to
|
|
|
|
|
carry, a fresh install has no database (bus-and-switch §11: read as no rows), and a run-once process with a bus
|
|
|
|
|
account could not append to the logs (§8). Why: bus-and-switch A3, B15 and §14 named this change as the module's
|
|
|
|
|
next; principle 7. How it is checked: the module's `go.mod` requires no postgres driver; a unit test fails on the
|
|
|
|
|
words `DATABASE_URL_FILE` or `MESH_LICENCE_KEY_FILE` anywhere in the module's code.
|
|
|
|
|
10. **What leaves the manifest, and what stays of the carry-over.** `requires` loses `postgres-database` and `secret`;
|
|
|
|
|
`contributes`, `binds` and the whole `secrets` section go; `env` loses `DATABASE_URL_FILE` and
|
|
|
|
|
`MESH_LICENCE_KEY_FILE`; the resource `database-url` goes; the tool `licences_carried` goes. The kept-data entry
|
|
|
|
|
`carried` stays, with its one key `postgres` as the record of the carry-over, read by `licences_kept`; nothing
|
|
|
|
|
writes it again. Why: removing the entry would drop kept data, which needs a warrant and a snapshot (ADR 0595 §5),
|
|
|
|
|
for nothing gained; principle 7. How it is checked: the catalog test of §1; after the delivery `licences_kept`
|
|
|
|
|
with `entry` `carried` answers the record of 09:44:59Z.
|
|
|
|
|
11. **The manager starts without waiting for a carry-over.** At its start the manager bundle judges each kind of
|
|
|
|
|
condition anew from its own state and says cleared each key of `conditions` it no longer holds (bus-and-switch
|
|
|
|
|
§9), `carry-over-failed` among them when present; then it says `holdings.asked` (B12) and starts its timers. It
|
|
|
|
|
reads `carried` never. Why: bus-and-switch §9, B12. How it is checked: unit, a start with a `carry-over-failed` key
|
|
|
|
|
in `conditions` says it cleared once and deletes the key.
|
|
|
|
|
12. **The pull request waits on two readings.** Before the push, the lead reads `licences_carried` of the running
|
|
|
|
|
build answering `carried.postgres` with every table's read equal to its written, and `mesh-vault.secret_holders`
|
|
|
|
|
listing `licence.<key>.grant` and `licence.<key>.access` for every licence `licences` answers, and writes both
|
|
|
|
|
readings in the milestone file. Why: bus-and-switch §14 and §20 (the database's requirement goes only once the
|
|
|
|
|
carry-over is done and no grant is left outside the vault); principle 3. How it is checked: the code review finds
|
|
|
|
|
both readings in the milestone file.
|
|
|
|
|
|
|
|
|
|
## The store database retires
|
|
|
|
|
|
|
|
|
|
13. **Both provisions retire, each on the operator's Approve, asked by the provisioner.** Once the new build's want
|
|
|
|
|
moves, no want needs this module's `postgres-database` nor its secret `grant-key`, so the holder of
|
|
|
|
|
`mesh-provisioner` marks both keys `retiring` and raises one ask each on the operator channel, saying what is
|
|
|
|
|
removed from which provider on which node (ADR 0570 §5). On the warrant postgres retires the consumer's account of
|
|
|
|
|
the database `claude_licences` and the vault removes `grant-key`; on a decline both stay retiring. The module asks
|
|
|
|
|
nothing. Before the delivery starts, the lead tells the operator that these two asks will come and what each
|
|
|
|
|
removes. Why: ADR 0570 §5; this replaces bus-and-switch §20's `mesh-vault.retire` of `grant-key`. How it is
|
|
|
|
|
checked: after the delivery `operator-channel.asks` holds both asks; after each warrant the provision's key is gone.
|
|
|
|
|
14. **The database's data is removed only by the operator.** Retiring keeps the database `claude_licences` and its
|
|
|
|
|
data (ADR 0570 §5). Its removal is a person's act, the postgres module's tool `provisioner_delete` called through
|
|
|
|
|
the mesh (ADR 0230 §5, §34): after both warrants the lead asks the operator for it, saying it removes the store's
|
|
|
|
|
copy of the licences, bindings, grants sealed with `grant-key`, usage and audit, whose every row is on the bus or
|
|
|
|
|
in the vault (§12). Why: ADR 0570 §5, ADR 0230; data removed is the operator's. How it is checked:
|
|
|
|
|
`mesh-store.databases` lists `claude_licences` until the operator's call and not after it.
|
|
|
|
|
15. **The files the database's requirement wrote leave the state directory with it.** `database.url` is the
|
|
|
|
|
resource `database-url`, which the node-engine removes as it removes every resource that leaves a declaration
|
|
|
|
|
(playbook 06, step 5); `database.json`, `database.secret` and `grant.key` are the files it wrote for the
|
|
|
|
|
requirements of §10, and whether it removes such a file when its requirement leaves is not known (a guess that it
|
|
|
|
|
does). A file left behind is a fault of the node-engine, noted on its issue and never removed by hand. Why:
|
|
|
|
|
principle 2. How it is checked: after the delivery, `node-reads.list` of the module's state directory on the
|
|
|
|
|
manager's machine lists none of the four.
|
|
|
|
|
|
|
|
|
|
## Delivery
|
|
|
|
|
|
|
|
|
|
16. **One pull request in mesh-catalog, with hq.** The module's change is 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).
|
|
|
|
|
17. **No order between modules; within the module, the node-engine's.** The change adds no served verb, provision
|
|
|
|
|
or event and drops none another module uses: the manager still calls `seat:mesh-vault.licence-put` and
|
|
|
|
|
`seat:mesh-vault.get`, `seat:operator-channel.tell`, `seat:mesh-config.config` and `claude-code.claude_code_grant`,
|
|
|
|
|
and consumes `claude-code.holdings-changed`, each served or said today, so the delivery plan derives no edge. On
|
|
|
|
|
the manager's machine the node-engine writes the state directory and the settings file, unpacks both bundles, and
|
|
|
|
|
restarts the module's runtime once, since its `restart-on` names every bundle's archive; the runtime then loads
|
|
|
|
|
both. The kept data keeps its names and form, so no restore point is taken (ADR 0672 §1 is for a form change), and
|
|
|
|
|
the delivery itself moves no data; a node's agent keeps its last token, which lives hours, while the runtime
|
|
|
|
|
restarts. Why: the module graph orders deliveries; ADR 0595 §11. How it is checked: the delivery plan names one
|
|
|
|
|
module and no edge.
|
|
|
|
|
|
|
|
|
|
## Acceptance
|
|
|
|
|
|
|
|
|
|
18. **The tests of the milestone**, each run in the module's suite or live after the delivery:
|
|
|
|
|
- the manifest is accepted by the manifest validator, built by the builder and composed by the node-engine for
|
|
|
|
|
the manager's machine, with the artifacts and resources of §1 and nothing else (§1 to §8);
|
|
|
|
|
- after the delivery the runtime serves the seat's ten verbs and the module's seven tools, the four reading tools
|
|
|
|
|
from the tools bundle (§2 to §4);
|
|
|
|
|
- with the manager bundle refusing to start, the four reading tools still answer (§3);
|
|
|
|
|
- the module's code names no database and no `grant.key` (§9), and `licences_kept` answers the carry-over's record
|
|
|
|
|
(§10);
|
|
|
|
|
- after the delivery the provisioner holds both retiring asks, and after the operator's call the database is gone
|
|
|
|
|
(§13, §14);
|
|
|
|
|
- the state directory holds none of the database's files (§15).
|
|
|
|
|
|
|
|
|
|
## The lead's answers
|
|
|
|
|
|
|
|
|
|
- **D1** (§12's vault reading, review round 1: no vault tool lists licence secrets, so `secret_holders` cannot show
|
|
|
|
|
`licence.<key>.grant` and `.access`): the reading that proves bus-and-switch §20's condition is, for every licence
|
|
|
|
|
`licences` answers, a rotation after the carry-over with no failure (`rotatedAt` later than `carried.postgres.at`,
|
|
|
|
|
`failures` 0), since a rotation reads the grant from the vault and puts both secrets back; and the vault's journal
|
|
|
|
|
handing `licence.<key>.access` to a bound node. Both go in the milestone file before the push (P3, P7).
|
|
|
|
|
- **D2** (§17's wording): the manager calls `seat:mesh-config.config` no more; the tools bundle does (§3). `invokes`
|
|
|
|
|
is the module's, so the manifest is unchanged (P8).
|