diff --git a/03-DESIGN/01-to-be/57-building-config-and-provisioning.md b/03-DESIGN/01-to-be/57-building-config-and-provisioning.md new file mode 100644 index 00000000..5872d227 --- /dev/null +++ b/03-DESIGN/01-to-be/57-building-config-and-provisioning.md @@ -0,0 +1,343 @@ +--- +layer: to-be +status: designed +code: [mesh-controller, mesh-host, mesh-sdk, mesh-catalog] +updated: 2026-10-14 +decisions: + - "ADR 0348" + - "ADR 0349" + - "ADR 0201" + - "ADR 0113" + - "ADR 0158" +--- + +# 57. Building config and provisioning + +**The work of ADR 0348 (config is a domain of its own) and ADR 0349 (provisioning), broken into +packages small enough that each ends at something that can be seen to work, in the order their +dependencies allow.** The two decision records are the authority on *what* is built; this document holds +only the packages, their order, their sizes and their proofs, and is wrong the moment it disagrees with +them. It is the shape [design 38](38-building-the-operators-machine.md) gave the operator's machine. + +## How this is built + +**Each package is one issue in the issue tracker, and one pull request per repository it touches.** The +issue carries the package's number here in its title, links the packages it waits for with `depends-on`, +and links ADR 0348 or ADR 0349. At most five packages are worked at once, each by its own agent in its +own worktree; a package starts once every package it depends on is merged. Every pull request is +reviewed independently at its head and merged only with its checks green. + +**It ships as one release.** The operator's decision of 2026-10-14: one delivery, not one per package. +Each of mesh-controller, `mesh-host`, mesh-sdk, mesh-catalog and hq has the branch +`feat/57-config-and-provisioning`, and every package's pull request merges into that branch, never into +main. When the last package is merged, the whole is rehearsed in the lab: the walk, the moves of data in +their order, and every proof that is run live. Then each repository's branch reaches main in one pull +request, and the five, sharing their branch's name, are one delivery group (ADR 0239 §2), judged as one +future state. The five are merged within one merge window, so they are one batch and one walk (ADR 0276 +§1, §2), in an order declared with `after:` lines in each pull request (ADR 0239 §11): mesh-sdk, then +`mesh-host`, then mesh-controller, then mesh-catalog, then hq. The release holds every package but T6, R1 and R2: +the media catalogue and the applications live in repositories of their own and move after it, and only +then can the old forms be refused (R1) and the record say what runs (R2). + +**The moves of data run after the delivery, in this order, each a verb that says what it did:** +placement out of the settings layers (F2), the settings layers into mesh-config (K5), the secrets into +the vault, nothing rotated (V3), then the providers one by one (B2, B3). Each is rehearsed in the lab +first, and each is checked by reading back through the module that owns the data before anything +points at it. + +**What a release costs.** A proof that is run live (dunst on a workstation, a secret reaching its file, a +database consumer connecting) is first seen in the lab rehearsal, and a fault found then holds the +release, not one package. A walk that fails fails every commit it carries (ADR 0276 §3), so a fault on +the release's first node holds all five; when mesh-delivery or a person stops a member, the others are walked +again without it, leaving out every merge that shares its modules (ADR 0299 §1, §2), which in this +release is most of them; the fix is a new merge into the release's branch, and the group is walked again. + +**Nothing breaks on the way.** ADR 0349 clause 17: each file is filled the old way or the new way, never +both, by the form of its manifest, and the old forms stay accepted until the last module has moved. +Inside the release this still holds: the media catalogue and the applications kept in their own +repositories move after it, so the old forms stay accepted until they have (R1). + +## What exists already, measured + +Counted 2026-10-14 at mesh-controller `1601cb1`, the node-engine (`mesh-host`) `1a26778`, mesh-sdk `a2d2af0`, mesh-catalog +`ebf7f37`. + +| Piece | Today | Becomes | +|---|---|---| +| the controller's composer | `catalogue.Resolution.compose` fills every namespace but `${secret:}` in one loop (`declaration.go` ~1000–1085); the namespaces and their fields are tabled in `fillers` | fills placement only, for a module that has moved | +| settings | tables `settings`, `settings_history`; the `settings` verb (`modules.go` 407–760); proposals and asks (`proposals.go`, 975 lines); trusted files; `takes-settings` merging; about 4 100 lines and 2 500 of tests | the `mesh-config` module and its buckets; the verb `config` | +| placement kept as settings | `places`, `accesses` and the kept networks are read from settings layers | facts of an assignment, the controller's own | +| secrets | the controller mints, seals and accepts every secret (`internal/secrets`, `inventory/secrets.go` 1 125 lines; tables `secret`, `module_secret`, `secret_ask`, `operator_key`); the vault (`mesh-vault`, TypeScript) is read-only and never asked | the vault, in Go, holding and minting; handing out on request and reply | +| bindings | grant files and binding files composed by the controller (`declaration.go` 750–818); `${bound:}` 117 uses | each provider answers its consumers; the vault mints the pair's credential | +| contributions | `${contribution:}` (4 collectors), `${shell:}` (3), `${environment:}` (1); 22 contributors | collections, filled by the provisioner client | +| file text | 225 file resources in 88 modules, all inline (about 193 KB); 12 modules keep a second copy in 34 files with a test that the two agree (ADR 0349's context counted eleven; the twelfth is lemurs) | template files under `files/`, packed by the module's build | +| packing a directory | the builder's `archive` artifact packs a tree and publishes it by digest; the node-engine fetches and unpacks it | the same mechanism carries a module's templates | +| the node-engine's file writing | `applyFile`, `applyBlock`, `applyInto`, create-once, drift written back, `Keep` backups, restart-on and reload-on, container reads | the same code, reached also through a narrow local write verb | +| the node-engine's local verbs | one socket (`cli.sock`), peer-checked by uid, forwarding to the controller; no local write | a second, narrow socket for the provisioner client | +| bus identities | one principal per node for the tool runner, one per module with a process of its own | the provisioner client gets one of its own | +| the SDK | KV, watches, asks, caller proof; no sealing helper; the TypeScript `provisioner` is a database-provider harness | a seal box, provider helpers, and readers for config, bound values and secrets | + +**Two measurements decide the order.** The builder can already pack a module's directory, and the +controller already fills every placeholder at registration time from text; so template files can ship +first, the controller reading the text back from the packed `files/`, and nothing on a node changes. And +the node-engine's writing code needs no change in kind, only a second way in; so the provisioner client +reuses it rather than writing as root itself. + +## The order the work allows + +``` +package waits for package waits for +F1 — P1 — +F2 — P2 P1 +F3 — P3 F3 +F4 — P4 P3 +T1 — P5 P2, P4, T1, K3 +T2 T1 P6 P5 +T3 T4 T5 T2 P7 P6 +T6 T1 V1 F1 +K1 T1 (same file) V2 V1 +K2 K1 V3 V2 +K3 F3, K2 V4 V3, P5 +K4 K3 V5 V3, V4 +K5 F2, K4 B1 P5, V5 +K6 K1 B2 B1 +K7 K5, P6 B3 B2 +L1 P6 L2 L1 +S1 P6 R1 T5, T6, K6, K7, P7, V4, B3, L2, S1 +R2 R1 +``` +The issue tracker carries these as `depends-on` links (thirteen issues also link what they wait for +through another package), so the packages ready to start are those whose linked packages are all merged +into the release's branches. An issue's "Waits for: … delivered" means merged into the release's +branches: nothing is delivered before the release. The first wave touches different repositories and different files: +**T1, F1, F3, P1 and F4**. K1 waits for T1 because both change `internal/catalogue/manifest.go`; P3 +waits for F3 because both change `internal/broker`; F2 joins as soon as a slot is free. + +## 0 — Foundations + +**F1 (issue 650) — A seal box in the SDK.** *mesh-sdk. Half a day.* The ADR 0206 box (X25519, HKDF-SHA256, +AES-256-GCM) moves from the licence manager into `mesh-sdk/go/seal`, with a TypeScript twin, and the +licence manager uses it. **Proof:** a byte-for-byte test against the licence manager's existing sealed +values; the licence manager's tests pass unchanged. + +**F2 (issue 651) — Placement leaves the settings layers.** *mesh-controller. Two days.* `places`, `accesses` and the +kept networks become facts of an assignment, held by the controller beside the assignment, with a +migration moving each existing layer's keys; `Settle` stops letting a setting replace a provider's +served keys. **Proof:** composition of every node before and after the migration is byte-identical; +the settings layers hold no placement key afterwards. + +**F3 (issue 652) — The assignments and the modules' config declarations on the bus.** *mesh-controller. Two days.* +The controller keeps the bucket of assignments design 54 decides (which module on which node), and a +bucket of each registered module's declared config values, written when they change, with one event per +write. mesh-config needs both to resolve. **Proof:** a test against a real bus: an assignment and a +registration each change exactly one key and raise one event. + +**F4 (issue 653) — The words.** *hq. Half a day.* The glossary gains *config value*, *predefined key*, *provision* +and *provisioner client*, *collection*, and the ownership words *owned*, *seed* and *managed*. The word "setting" goes on *config +value*'s *Not:* line only in R2, once the 67 documents still using it are reworded; until then the +glossary says it is the older word. **Proof:** `words.py` passes. + +## 1 — Template files (ADR 0349 clause 2) + +**T1 (issue 654) — A file resource may name a template.** *mesh-controller. Three days; the largest package of the +stage.* A file resource takes `"template": "files/"` in place of `content`; the module's build packs +`files/` into an archive artifact of its own (the existing `archive` packing); registration and `Resolve` +read each template's text back from the packed tree into `content`, so composition and the node-engine +are unchanged; `module check` and the merge gate read templates from the repository tree beside the +manifest; a file naming both `template` and `content` is refused. **Proof:** composition tests: a module +with its text in `files/` composes byte-identically to the same module with it inline; the merge gate +judges a change to a template alone. + +**T2 (issue 655) — The twelve modules with copies.** *mesh-catalog. A day.* adwaita, clipmenu, dunst, feh, +gnome-keyring, i3, i3status-rust, lemurs, picom, rofi, screen-lock and xdg move to their templates; the +34 inline copies and the `sameAsSource` tests go. **Proof:** each module's composed declaration on its +nodes is unchanged (the merge gate's plan says no file changes). + +**T3 (issue 656) — The desktop modules.** *mesh-catalog. A day.* Every remaining module assigned only to +workstations. **Proof:** as T2. + +**T4 (issue 657) — The servers' modules.** *mesh-catalog. Two days.* Every remaining module with a build. **Proof:** +as T2. + +**T5 (issue 658) — The ten modules without a build.** *mesh-catalog. A day.* ca-trust, de-spiegel, invoicing, +local-model-consumer, matrix, node-env, only-office, sshd, step-ca and supabase gain an archive build for +their templates. **Proof:** as T2; afterwards no manifest in the catalogue carries file text. + +**T6 (issue 659) — The media catalogue and the applications.** *mesh-media-catalog and each application's +repository. Two days.* **Proof:** as T2, per repository. + +## 2 — Config (ADR 0348) + +**K1 (issue 660) — A module declares its config.** *mesh-controller. Two days.* The manifest takes `config` (type, +description, validation, trusted, `predefined`, `default` or `required`), `takes-config` and +`${config:}`, with `settings`, `takes-settings` and `${setting:}` read as aliases; registration +refuses everything ADR 0348's "How it is checked" lists for registration: a value without type or +description; a value with neither a default nor `required`; a default failing its validation; an +object or list not trusted; a predefined key not on the list or of another type (with K2); an untrusted +value falling back to a trusted key; a value neither in a template nor read at runtime; a placeholder +naming an undeclared value; a `reload-on` or `restart-on` naming an undeclared file; a moved module's +mergeable file not naming its keys; a mesh word or `puid`/`pgid` in `takes-config`. **Proof:** the registration refusal tests, one per +rule; every manifest in the catalogue still registers. + +**K2 (issue 661) — The list of predefined keys.** *mesh-controller and mesh-catalog. A day.* `predefined.json` at the +catalogue's root, with `font-size` and `agent-managed-settings` first; the build validates it and every +manifest naming a key; the merge check refuses removing a key still named or holding a value; the newest +trunk build's list is published. **Proof:** the merge check refuses a planted removal; an older build's +list does not replace a newer one. + +**K3 (issue 662) — The mesh-config module.** *mesh-catalog. Four days.* A Go module holding the seat `mesh-config`: +the buckets `mesh-config_values`, `_resolved`, `_predefined`, `_proposals`; the four sources resolved from +F3's assignments and declarations; the verb `config` (set, clear, list, history, narrowed by module and +node); validation; the hand-act log; `config-changed`; only its account writes its buckets. Terminal-only +for trusted values until K4. **Proof:** its tests resolve each source in turn; a test against a real bus +refuses a put from another account. + +**K4 (issue 663) — Trusted values are proposed and approved.** *mesh-catalog. Two days.* mesh-config asks under its +own name, with its own grant and cap of open asks, binds each option to the level, key and value, records +the approver beside the value, and checks the proposal on Approve (ADR 0277's rules, ADR 0348 clause 8). +**Proof:** a proposal test end to end with a stand-in channel; an Approve against a changed value is +refused. + +**K5 (issue 664) — The values move, and `settings` forwards.** *mesh-controller and mesh-catalog. Two days.* Each +existing layer moves into mesh-config (a node layer to `module..`; a whole-mesh layer to +mesh config, a manifest default or each node, as ADR 0348's consequences say; claude-code's managed +settings to `agent-managed-settings`), recorded with why; for a module not yet handed to the provisioner +the controller reads `mesh-config_resolved` where it read its layers (ADR 0349 clause 17's old way, +an exception to ADR 0348 §9 that R1 removes), and `settings` answers by forwarding to `config`. **Proof:** composition of +every node before and after is byte-identical; `settings` and `config` list the same values. + +**K6 (issue 665) — The catalogue says config.** *mesh-catalog. A day.* The 15 modules using `${setting:}`, the 9 +declaring settings and the 4 with `takes-settings` move to the new names and the full declaration. +**Proof:** the catalogue's check finds no old name in a moved module. + +**K7 (issue 666) — The operator's acceptance on dunst.** *live. An hour.* With every level cleared the first +workstation's dunstrc says 10; a mesh font size, then that node's font size, then dunst's own value on +that node each appear in its dunstrc within the call's answer, and the history shows each change with who and why. Needs P6. + +## 3 — The provisioner (ADR 0349 clauses 3–5, 11, 13) + +**P1 (issue 667) — The node-engine knows who owns a placed file.** *`mesh-host`. A day.* `declaration.File` gains +`owner-at-runtime` (`owned`, `seed`, `managed`), mapped onto today's paths (drift written back, +create-once, `into`), and the declaration gains the list of files the provisioner fills. **Proof:** the +apply tests: each ownership behaves as its old form did. + +**P2 (issue 668) — The narrow local write verb.** *`mesh-host`. Two days.* A second unix socket accepts a write only from +the provisioner client's account (`SO_PEERCRED`) and only for a path the declaration names as filled, +writes through `applyFile`, `applyBlock` or `applyInto` with `Keep`, and fires restart-on, reload-on and +container reads as an apply does. **Proof:** tests: an undeclared path and another uid are refused; a +write under `reload-on` reloads its program once. + +**P3 (issue 669) — The provisioner client's own bus account.** *mesh-controller. A day.* A principal of its own per +node for `node-provisioner`, granted to read its node's keys in each domain's bucket, publish +`prov.applied..>` and ask the vault; nothing else. **Proof:** the grants test, and a test against a +real bus refusing a read of another node's keys. + +**P4 (issue 670) — The provisioner's server.** *mesh-catalog. Two days; after P3.* A Go module holding `mesh-provisioner` once: +the registry of which domain answers which kind, the buckets `_registry`, `_needs`, `_applied` and +`_collect`. **Proof:** its tests; the buckets listed live. + +**P5 (issue 671) — The provisioner's client.** *mesh-catalog. Four days.* A Go service on every node, outside the tool +runner: fetches a module's templates by digest, records needs, watches each owner's keys for its node, +fills only when every value is there, writes through P2, reports `prov.applied`, writes an `owned` file +back on drift, removes a module's files and needs when it leaves, fills from current values when its node +comes back. Its first owners are placement (from the declaration) and config (from +`mesh-config_resolved`). **Proof:** the client's test list in ADR 0349, against a real bus. + +**P6 (issue 672) — The controller hands a module to the provisioner, and the gate waits for it.** *mesh-controller. +Two days.* For a module marked moved, the declaration carries placement, the templates' digest and the +filled-files list, and the controller runs no other filling pass for it; the first-node gate counts the +module healthy only after its node's `prov.applied` for the new build (`gateFacts`). dunst moves first. +**Proof:** composition and gate tests; dunst on the first workstation filled by the client, live. + +**P7 (issue 673) — The node-engine stops writing placed files from the declaration.** *`mesh-host`. A day.* For a moved +module the engine writes its filled files only through P2, and removes as an orphan only what the verb +placed. **Proof:** the apply tests; no placed file written twice in a day's journal on the +first workstation. + +## 4 — Secrets (ADR 0349 clauses 6–11, 16; issue 647) + +**V1 (issue 674) — The vault in Go, holding secrets.** *mesh-catalog. Four days.* `mesh-vault` is rewritten in Go: +its own store, a copy of each secret sealed to the operator key, `get` on request and reply sealed to the +asker's key (F1's box; the node-engine's key format is settled here and written down), `secret-changed` +without a value, no secret in any bucket. **Proof:** the vault's test list in ADR 0349. + +**V2 (issue 675) — The vault's verbs.** *mesh-catalog and mesh-controller. Three days.* `secret accept`, `secret-ask` +(with ADR 0277 §24–§27's record and bounds in the vault's store), `issued` (ADR 0305, the provider's tool +answering the vault alone) and `rotate` become the vault's; the controller's verbs forward. **Proof:** the +`secret-ask` and issuing provider tests in ADR 0349. + +**V3 (issue 676) — The secrets move to the vault.** *mesh-controller and mesh-catalog. Two days.* Every row of +`secret`, `module_secret`, `secret_ask` and the operator key's public half are copied into the vault as they are, +nothing rotated, and read back through the vault before anything points at it. **Proof:** for every +secret, the vault's sealed answer opens to the same value the controller's does. + +**V4 (issue 647) — A node's secrets come from the vault.** *mesh-catalog, mesh-controller, `mesh-host`. +Three days.* The client fills `${secret:}` from the vault's `get`; for a moved module the controller seals +nothing; the core's credentials travel sealed by the vault to their node and the node-engine opens them. +**Proof:** a secret rotated in the vault reaches its file without a new declaration, live. + +**V5 (issue 677) — The vault mints pair credentials.** *mesh-catalog. Two days.* One value per provider assignment and +provision, remade for all holders when the vault sees a consumer's need recorded or deleted (ADR 0158 +§4–§6). A need recorded for a bind the vault already holds from V3 adopts that value and remakes +nothing, so the move itself rotates no credential. **Proof:** the provider test in ADR 0349, with a stand-in provider. + +## 5 — Bindings (ADR 0349 clause 12) + +**B1 (issue 678) — A provider's helper in the SDK.** *mesh-sdk. Two days.* Go: watch the needs for a kind, write one +entry per consumer, refuse a derived value for a multi-holder consumer, answer from the vault's pair +credential. The TypeScript `provisioner` harness is renamed so the word means one thing. **Proof:** its +tests with a stand-in bus. + +**B2 (issue 679) — PostgreSQL answers its consumers.** *mesh-catalog and mesh-controller. Three days.* The first +provider moves; its grant files and binding files are retired. **Proof:** every database consumer on +every node connects after the move with the credential it had, adopted by the vault as V5 says, live. + +**B3 (issue 680) — The other providers.** *mesh-catalog. A week, one pull request per provider.* **Proof:** as B2, per +provider; afterwards the controller composes no grant file. + +## 6 — Collections (ADR 0349 clause 13) + +**L1 (issue 684) — A collection is declared and filled by the client.** *mesh-controller and mesh-catalog. Three days.* +A collector requires a collection; a contributor provides an item from a template; the client fills the +collector's template from `_collect`. `${contribution:}`, `${shell:}` and `${environment:}` read as +collections. **Proof:** zsh's file holds every placed contributor's item and loses one whose contributor +leaves. + +**L2 (issue 681) — The collectors and contributors move.** *mesh-catalog. Two days.* zsh first, then the bar, the +display session, the hotkeys, xorg, power and node-env, with their 22 contributors. **Proof:** each +collector's composed file is unchanged at the move. + +## 7 — A module's code asks at runtime (ADR 0349 clause 14) + +**S1 (issue 685) — The SDK's readers.** *mesh-sdk and mesh-catalog. Two days.* `config`, `bound` and their watches, +answered by the node's client for what the manifest declares. `secrets` refuses until the module runs +under a bus account of its own, which waits on issue 569 and its decision. **Proof:** the SDK's test in +ADR 0349; claude-code reads its managed settings through it. + +## 8 — The old forms go (ADR 0349 clause 17) + +**R1 (issue 682) — Refused and removed.** *mesh-controller, `mesh-host`, mesh-catalog. Three days.* Once every module of +every catalogue has moved: the catalogue's check refuses inline text, `settings`, `${setting:}` and the +old contribution forms; the controller's filling passes but placement, its settings, secret and binding +code and tables (a new migration) go; the node-engine keeps opening sealed values for the core's files +only; the controller stops reading `mesh-config_resolved`. **Proof:** the controller's test that its +code opens no secret outside genesis and reads no config value; the absence of the +removed code. + +**R2 (issue 683) — The record says what runs.** *hq. Two days.* The as-is designs are written, designs 27 and 32 are +amended, the 67 documents that still say "setting" in running prose are reworded so the glossary can +retire the word (F4 left it off the *Not:* line for them), design 49 lists the Config domain, ADR 0348's and 0349's built status is said, and this document's status becomes `implemented`. + +## What is deliberately not here + +- **Secrets in a module's own code** wait on issue 569 (a module's tools run as the tool runner's + account) and its decision; S1 ships without them. +- **Release separated from delivery** stays issue 644's, and **the bus's replicas** issue 645's. +- **Renaming "walk"** is issue 646's. + +## How this list is kept true + +Each package's proof that runs in its tests is run when it is merged into the release's branch, and its +line here gains the date and the commit; a proof that runs live is run in the lab rehearsal and again +after the release's delivery. A package whose proof fails is not reworded; the failure is recorded under it and the package +stays open. The issue tracker says which packages are worked now; this document says only the order. diff --git a/03-DESIGN/01-to-be/README.md b/03-DESIGN/01-to-be/README.md index eb725dff..f3999865 100644 --- a/03-DESIGN/01-to-be/README.md +++ b/03-DESIGN/01-to-be/README.md @@ -56,6 +56,7 @@ document is written and this one's status becomes `implemented`. | [`54-the-controllers-records-on-the-bus.md`](54-the-controllers-records-on-the-bus.md) | **Designed.** The controller keeps five record mirrors on the bus (nodes, assignments, walks, builds, settings): each a bucket written from its store only when the record changed, with one event per write, and audited against the store every self-check run, a difference raised as `mirror-drifted` before the healer H6 repairs it | ADR 0289 | | [`55-the-knowledge-base.md`](55-the-knowledge-base.md) | **Designed.** The mesh keeps its decision records and research efforts whole in one module, `mesh-knowledge`, on the control-node: number, status, topic, typed links both ways, each decision's clauses with their own status, and the reasoning, with every version; a record is proposed, checked at the write, reviewed by the knowledge base's own judgement and accepted there, with the operator's Approve when it needs it; agents ask what holds in one bounded call | ADR 0290 | | [`56-the-mail-client-set-up-by-the-mesh.md`](56-the-mail-client-set-up-by-the-mesh.md) | **Designed.** On every node with a display server, the `betterbird` module installs Betterbird from a pinned upstream release mirrored into the artifact store, ships the Owl add-on pinned and installed by Betterbird's enterprise policy, and declares the operator's mail accounts in a trusted setting without credentials; each account signs in once at a person's first use, and one the profile already holds is adopted by its address | ADR 0316, ADR 0317 | +| [`57-building-config-and-provisioning.md`](57-building-config-and-provisioning.md) | **Designed.** The work of ADR 0348 and ADR 0349 as 37 packages, each an issue with its proof: foundations, template files, config in its own module, the provisioner and its client, secrets in the vault, bindings from providers, collections, a module's runtime readers, and removing the old forms; at most five worked at once | ADR 0348, ADR 0349 | ## Not yet written