Design 57: the work of ADR 0348 and ADR 0349 as packages, each an issue, in dependency order
mesh/merge-gate head 11294372b769 merged into trunk fd416cef91bf — pass: the change touches no module of the mesh's graph
mesh/repo-check head 11294372b769 merged into trunk fd416cef91bf — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request

So config and provisioning are built in small, provable steps with at most five worked at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 23:12:23 +00:00
co-authored by Claude Opus 5.5
parent fd416cef91
commit 11294372b7
2 changed files with 307 additions and 0 deletions
@@ -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/<name>"` 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:<key>}`, 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.<node>.<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.<node>.>` 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.
+1
View File
@@ -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