"control plane" -> controller and "substrate" -> foundation throughout 03-DESIGN, 00-META and the README, with 06-the-control-plane.md and 07-the-substrate.md renamed to 06-the-controller.md and 07-the-foundation.md. The immutable 02-DECISIONS records keep their original wording (and links to them are unchanged) — a term retired here may still appear there, which the glossary explains how to read. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
177 lines
10 KiB
Markdown
177 lines
10 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 `process`, which the mesh writes the unit for. *Was: a `file` for the unit and a `service` for its state — which made every module author write unit syntax, and is why `process` exists ([`18`](18-building-a-module.md))* |
|
|
| **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` — an `image` from a Dockerfile, or a `bundle`, which names a language and lets the mesh choose the toolchain ([`18`](18-building-a-module.md)) |
|
|
|
|
## 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. The largest entry turned out not to be a gap at all, which
|
|
is left in place rather than deleted: the first framing of it was wrong in an instructive way, and
|
|
a checklist that quietly loses its biggest item reads as though nobody looked.
|
|
|
|
### Tool servers — 56 modules — *not a gap*
|
|
|
|
Over half the modules ship a `tools/` directory that becomes tools an agent can call on that node.
|
|
This was written up as the largest single gap. **It is expressible with what exists**, and the
|
|
first framing of it was wrong in a way worth keeping: *a module provides `tools`, the session
|
|
requires them* does not work, because a requirement has exactly one answer and 56 modules offering
|
|
tools would be 56 answers to one question.
|
|
|
|
Turn it around and it fits exactly. The session **provides** `tool-host`; every module offering
|
|
tools **requires** it and **contributes** where its tools are. Many-to-one is what `contributes`
|
|
has always been, and the session receives all of them in one file:
|
|
|
|
```
|
|
given: { from: gitea, values: { at: … } }
|
|
{ from: minio, values: { at: … } }
|
|
{ from: umami, values: { at: … } }
|
|
```
|
|
|
|
Verified by resolving it, not by reading the code. It also only became possible today: until
|
|
[`022`](../../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md) was
|
|
fixed, several modules on one node requiring the same thing was refused outright.
|
|
|
|
What remains is not vocabulary but a decision about **what a tool server is** — a container the
|
|
module already runs, and what the session does with the list. That is work, not a missing shape.
|
|
|
|
### 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.
|
|
|
|
**An image store is a module, and was written up here as something the mesh does.** It was
|
|
considered for the foundation and removed, because the test is not *can it grant itself one* —
|
|
nearly anything passes that — but whether the controller needs it before it can give its first
|
|
instruction. It does not. So a registry somebody runs for their own images is the same module as
|
|
the one the mesh runs for its own: it offers a place to push, and claims that role once per
|
|
machine.
|
|
|
|
**A rule was enforced only at the far end.** A module may not declare an action, and the host
|
|
refused one correctly — but the controller 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.
|