Document the whole module surface, and keep it true with a test

A reference table goes stale the day somebody adds a field, so this one points at
mesh-catalog/modules/showcase — a module that uses all of it — and a test that
fails when it stops doing so. Read the module when the table disagrees with it.

Two rows in the coverage survey were stale because of this week's work: systemd
units were a file plus a service, which made every author write unit syntax and
is why "process" exists; and building from source was images only, where a
bundle now names a language and lets the mesh choose the toolchain.

And the two rows at the bottom of the resource table are the interesting ones.
"action" is refused to modules outright — the link may not carry a command, so a
module needing something done ships a program that reconciles. "service"
installs no unit by design, right for software shipping one and wrong for code
the mesh built, which has none until the mesh writes it.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
2026-09-15 13:06:16 +02:00
parent 8a7328c282
commit 411f0680b8
3 changed files with 81 additions and 2 deletions
@@ -225,9 +225,16 @@ answered to the asker and kept nowhere.
| kind | is |
|---|---|
| **image** | built from a Dockerfile in this repository |
| **bundle** | this module's own code, compiled by the toolchain its language implies, then packed |
| **archive** | a directory in this repository, packed |
| **upstream** | an image somebody else built, mirrored into the mesh's own registry |
**The second was added later and is why most modules now need no Dockerfile.** An archive packs a
directory as it stands, so shipping compiled output meant compiling somewhere first — and that
meant every module repeating a recipe that is easy to get wrong in ways that fail elsewhere. The
whole surface a module has, and a module that exercises all of it, are in
[`18-building-a-module`](18-building-a-module.md).
**The third exists because a module usually runs software it did not write.** A database module
ships configuration and a provisioner and does not build a database. Naming the upstream reference
directly would need every machine to reach a public registry, and would pin to a tag its owner can
+2 -2
View File
@@ -24,7 +24,7 @@ checklist: what is already sayable, what is deliberately not, and what is missin
| **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 |
| **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 |
@@ -33,7 +33,7 @@ checklist: what is already sayable, what is deliberately not, and what is missin
| **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` |
| **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
@@ -148,3 +148,75 @@ makes the drift worse while hiding it.
| Nothing is announced that was not made | A build made to fail announces nothing, and the catalogue's graph is unchanged afterwards. |
| A build keeps what it was told | A catalogue started after a build asks for what it missed and receives the manifest and the artifacts stood on, not a summary. |
| The toolchain is the mesh's, not the author's | Changing the toolchain makes every module built against it stale, and each names what moved. |
## The whole surface, in one place
**A reference, and it is kept true by a test rather than by care.** A table like this goes stale the
day somebody adds a field, so `mesh-catalog/modules/showcase` is a module that uses all of it, and
`TestTheShowcaseModuleIsAValidManifest` fails when it stops doing so. Read the module when this
disagrees with it.
### What a module declares
| field | is |
|---|---|
| `module`, `version`, `slug` | its identity; the slug is the short name generated names are built from |
| `capabilities` | what a machine must have for this to run there |
| `provides` | **the shared seat** — several modules may fill one capability and coexist |
| `claims` | **the exclusive seat** — two modules claiming one thing in a scope cannot both be assigned there |
| `serves` | what a consumer must know to connect. The mesh fills in the assigned port ([ADR 0038](../../02-DECISIONS/0038-the-mesh-assigns-the-port.md)) |
| `requires` | what must be provided by something on the same node |
| `binds` | where the mesh writes what a requirement resolved to |
| `secrets` | where the mesh seals the credential for a requirement |
| `own-secrets` | secrets that are the module's own — a superuser, a broker account |
| `emits` / `consumes` | the event graph: 1:many, broadcast, no credential |
| `listens` | the port **its own software** uses, and from where. Filtering is computed from these |
| `contributes` / `receives` | values one module adds to another's configuration, and the other half |
| `accesses` | operator-owned paths it may use and must not own ([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)) |
| `certificate` | a certificate for a name it serves |
| `grants` | credentials it must create for its consumers |
| `filtering` | rules beyond its own ports |
| `computed` | marks a module the control plane generates rather than an author writing |
| `build.artifacts` | what it produces |
### What it builds
| kind | is |
|---|---|
| `bundle` | its own code, compiled by the toolchain its language implies, then packed |
| `archive` | a directory, packed as it stands |
| `image` | built from a Dockerfile — for software that needs a particular base |
| `upstream` | somebody else's image, mirrored and pinned by a digest this mesh assigned |
### What it puts on a machine
| resource | is | a module may |
|---|---|---|
| `directory` | a directory with a mode and an owner | ✅ |
| `file` | literal content, with `${bound:…}` and `${secret:…}` filled in | ✅ |
| `user` | a login | ✅ |
| `access` | a pre-existing path it may use and must not own | ✅ |
| `archive` | files fetched by digest and unpacked | ✅ |
| `package` | a package that must be present | ✅ |
| `network` | a named container network | ✅ |
| `container` | an image, in three modes | ✅ |
| `process` | **its own code**, in three modes | ✅ |
| `service` | an **existing** unit put into a state | for software shipping its own unit |
| `action` | a command to run | ❌ **refused** — [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) |
**The two at the bottom are the interesting rows.** `action` is refused outright: the link may not
carry a command, so a module needing something done ships a program that reconciles — which is what
a run-once `process` is. `service` installs no unit by design, which is right for software that
ships one and wrong for code the mesh built, which has no unit until the mesh writes it.
### How its code runs, and what that code can be
| mode | is | | shape | loaded by |
|---|---|---|---|---|
| *(default)* | a unit restarted when it exits | | tools | a tool host, over the broker |
| `run-once` | run to completion; what follows is gated on it | | event consumer | the same host, reacting |
| `schedule` | a timer; a missed fire happens when the machine returns | | provisioner | invoked when a consumer is granted |
| | | | a process | the machine's supervisor |
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
right number: they are loaded by a tool host, and a tool host is a process that stays up.