022 turned out to have a silent half worth recording: the provider refuses loudly and names the modules, which reads as a decision, while the consuming node does not refuse at all. Three modules wanting one database produce one need, so two of them get no credential file and each starts and fails to authenticate with nothing saying why. 023 is what remained after fixing it. A consumer now gets its own password, in whatever shape its configuration wants, and still cannot connect: the user name is invented by the provisioner and recorded nowhere in the mesh, and the host and port sit in a JSON binding that an application reading KEY=value cannot use. The asymmetry is backwards and the coverage document now says so. The secret is the hard case, because the mesh must not be able to read it, and the secret is the part that arrives. The host and port are ordinary facts the mesh holds in the clear, and they are the ones stuck. Keycloak, Gitea, Mailu and MinIO all parse and resolve and none of them can start. This is what stands between the module set and a running one.
7.5 KiB
layer, status, code, updated, decisions
| layer | status | code | updated | decisions | |||
|---|---|---|---|---|---|---|---|
| to-be | designed | 2026-09-01 |
|
What a module must be able to say
Measured, not guessed. 127 manifests in the system being replaced were read and every key counted, then set against what the new manifest can express. This document is the coverage checklist: what is already sayable, what is deliberately not, and what is missing.
Surveyed 2026-09-01. Counts are modules, not occurrences, unless stated.
Already sayable
| what it says | used by | how it is said here |
|---|---|---|
| depends on another module | 65 | requires naming a module. A requirement naming a module means that module, not anything providing the name |
| system packages | 28 | the package shape |
| a container | 48 | the container shape, pinned by digest |
| systemd units | 17 | a file for the unit, a service for the state it should be in |
| how to reach it | 17 | serves, with the mesh adding which machine and where |
| a public name | 11 | requiring route and contributing the name |
| ports it opens | 11 | listens, from which filtering is computed |
| data directories, and who owns them | 38 | directory with owner; never removed while holding anything (ADR 0030) |
| what it provides and requires | 15 + 2 | provides / requires, named for what the consumer is coupled to |
| restart when something changes | 11 | restart-on |
| a generated credential | 20 | own-secrets, sealed to the machine |
| a value that differs per node | 43 | settings, and computed for what only the mesh knows |
| images built from source | 2 | build.artifacts |
Deliberately not sayable
Stage hooks — 36 modules. Arbitrary code at install, configure and start. The link may not carry an action (ADR 0005): what may be pushed is bounded by form, and a command to run is not a form. A module needing setup logic ships a program that reads what the mesh delivered and reconciles — which is what the provisioners are, and they are ~350 lines each including the reasoning.
Flavours — 6 modules. Variants of one module. Retired in favour of claims
(ADR 0009): two display servers are two
modules that both claim the seat, and adding a third changes nothing anywhere else. What is lost
is extends chains, which were doing inheritance and are better as separate modules.
Missing, and what each would take
Ordered by how many modules need it.
Tool servers — 56 modules
The largest single gap. Over half the modules ship a tools/ directory that becomes tools an
agent can call on that node. Nothing in the new manifest says this module offers tools.
A module can already run the server — it is a container or a service. What is missing is the
convention that makes it reachable: something has to know the tools exist and route to them. That
is plausibly not a manifest feature at all but a provision — a module provides tools, the
session on that node requires them — which would need no new vocabulary. Not yet decided.
Schema migrations — 14 modules
A module with a database needs its schema brought up to date before it runs. The mesh does this for its own contexts and has no way for a module to declare it. The provisioner pattern covers it — a program that runs migrations and exits — but nothing expresses this must happen before that starts, which is the actual requirement.
Configuration merging — 18 modules, 134 files
Files assembled from a module's default plus per-node overrides, with a strategy (replace,
merge) and a format (toml, yaml, json). Settings already merge into a file's content; what
is missing is format-aware merging.
And it should stay missing. A mechanism that understands TOML will be asked for YAML, then INI — which is how the arrangement being replaced became something nobody could hold in their head. The module knows its own format because it wrote the rest of the file.
Health checks — 7 modules
{type: port|url, expect: …}. The mesh knows whether a container is running, which is not the
same as whether it answers — a distinction this project has paid for twice already. An action
with a verify is exactly this shape, but actions may not arrive over the link, so a module
cannot declare one.
This is the gap most worth closing, because running and answering being conflated is a class of fault, not an inconvenience.
Theme knobs — 3 modules, 101 values
{theme: {kind: color|font|string, label}} — declared so a ricing tool can offer them. Settings
already carry the value; what is missing is the metadata saying a value is presentable and
what kind it is. Small, self-contained, and only interesting once something presents them.
Event routing — 2 modules
{routing-key: tool}, generating a consumer. Two modules; wait for a third before deciding.
Publishing a package — 6 modules
Modules published to a registry and consumed as libraries. This is a build output the mesh does not deliver to a node, so it may not belong here at all.
What checking the coverage found
Two faults, both surfaced by asking what a real node looks like rather than what a test does. Neither is about the vocabulary; both are about the machinery under it.
A credential belonged to a machine, not to a module
(022,
fixed). A node running three services against one database could not be planned at all — and on
the consumer's side did not refuse, it just gave two of the three no credential. Every scenario
written to date had one consumer per node, which is the natural shape of a small test and not the
shape of a machine.
A consumer still cannot build a connection string
(023, open). It
is given its own password in whatever shape its configuration needs, and the host, port and user
name are still out of reach: the user name is invented by the provisioner and recorded nowhere,
and the bound values sit in a JSON document that an application reading KEY=value cannot use.
The asymmetry is worth stating, because it is backwards. The secret is the hard case — the mesh must not be able to read it — and the secret is the part that now arrives. The host and port are ordinary facts held in the clear, and they are the ones stuck.
What the survey found that is not about coverage
Declaration and reality had drifted in the system being replaced. Several live provisions are brokered by modules whose manifests declare nothing — a speech-to-text engine served to a consumer on another node, an object-store bucket held by a module whose manifest mentions only its database. A manifest that does not have to be true stops being true, which is the argument for resolution refusing rather than warning.
Two derivations of the same fact. A module's kind was computed in two places from different evidence — one from the manifest, one from what is on disk — producing different labels for the same module. There is one derivation here, and there should stay one.
A live listing returned credentials in plaintext. Not a coverage question, but the reason sealing is worth its inconvenience.