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..953f5f58 --- /dev/null +++ b/03-DESIGN/01-to-be/57-building-config-and-provisioning.md @@ -0,0 +1,306 @@ +--- +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 and this design. At most five packages are worked at once, each by its own +agent in its own worktree; a package starts only when every package it depends on is delivered, not +merely merged. Every pull request is reviewed independently at its head and merged only with both +required checks green (`mesh/merge-gate`, `mesh/repo-check`). + +**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. +Every package below keeps the mesh composing as it does today until a module is moved to the new form. + +## 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:}` 116 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 | 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 — +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 holds the same links as `depends-on`, so the packages ready to start are those whose +linked packages are all resolved. 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*; *setting* +is named on *config value*'s *Not:* line. **Proof:** `words.py` passes and fails on a planted *settings*. + +## 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 what ADR 0348's check list names (a value without type or description, a default failing its +validation, an untrusted object, an untrusted value falling back to a trusted key, a placeholder naming +an undeclared value, a mesh word 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; the controller reads `mesh-config_resolved` +where it read its layers, 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 g14's dunstrc says +10; a mesh font size, then g14's node font size, then dunst's own value on g14 each appear in g14's +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 g14 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 file written twice in a day's journal on g14. + +## 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 export is copied into the vault as it is, +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). **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 (no rotation), 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. **Proof:** the controller's test that its code opens no secret outside genesis; the absence of the +removed code. + +**R2 (issue 683) — The record says what runs.** *hq. A day.* The as-is designs are written, designs 27 and 32 are +amended, 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 is run when the package is delivered, and its line here gains the date and the +commit. 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