Both halves had one cause: the mesh knew something and did not say it. Who a consumer is now comes from one derivation, sent to the provider in its grant and to the consumer in its binding, so the two agree by construction. The provisioners use the name they are given and refuse to invent one, because a name of their own would create a login the consumer could never guess while everything reported success. Bound values reach the file that needs them through the symmetric twin of the sealed placeholder — simpler, because they are not secret, so the control plane fills them in and the host gains nothing. The lab run meant to prove this failed in a way that looked like the fix being wrong: rotation could not authenticate against a real database. The cause was the suite rebuilding the control plane's image and not the provisioner's, so an image built that minute ran against a provisioner built the day before. That is 005's family and is recorded with the issue, because the misleading part is worth more than the fix.
153 lines
8.3 KiB
Markdown
153 lines
8.3 KiB
Markdown
---
|
|
layer: to-be
|
|
status: designed
|
|
code: []
|
|
updated: 2026-09-01
|
|
decisions:
|
|
- 02-DECISIONS/0009-modules-and-the-graph.md
|
|
- 02-DECISIONS/0005-the-node-host.md
|
|
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
|
|
---
|
|
|
|
# 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](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md)) |
|
|
| **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](../../02-DECISIONS/0005-the-node-host.md)): 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](../../02-DECISIONS/0009-modules-and-the-graph.md)): 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` carries a `verify` and is exactly this shape. **It is not available to a module**: the
|
|
link may not carry a command to run ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)), and a
|
|
module's resources reach a machine over the link. So a health check needs a way to say *ask this
|
|
and expect that* without saying *run this* — closer to a `listens` entry than to an action.
|
|
|
|
**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`](../../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md),
|
|
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 could not build a connection string**
|
|
([`023`](../../04-ISSUES/023-a-consumer-cannot-build-a-connection-string/00-report.md), fixed). It
|
|
had its password in the right shape and the host, port and user name were out of reach: the user
|
|
name was invented by the provisioner and recorded nowhere, and the bound values sat in a JSON
|
|
document that an application reading `KEY=value` cannot use.
|
|
|
|
The asymmetry was backwards, which is what made it worth stating. **The secret is the hard case** —
|
|
the mesh must not be able to read it — and the secret was the part that already arrived. The host
|
|
and port are ordinary facts held in the clear, and they were the ones stuck. Both halves came from
|
|
the same thing: the mesh knew something and did not say it.
|
|
|
|
## 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.
|
|
|
|
**A rule was enforced only at the far end.** A module may not declare an action, and the host
|
|
refused one correctly — but the control plane accepted it into the catalogue, resolved it and
|
|
pushed it, so the refusal arrived on a machine with nothing tying it back to the manifest. The
|
|
rule held; it was just unusable, which is the same shape as the network shape that cost five
|
|
failing tests before anyone read the host's log. It is now refused where it is written.
|