What a module must be able to say, measured against 127 that exist
Every manifest in the system being replaced was read and every key counted, then set against what the new one can express. Three findings worth more than the table. **The most-used key was already covered and I expected a gap.** Depending on another module — 65 manifests, the commonest thing any of them says — is a requirement naming a module, which already means that module rather than anything providing the name. **The largest real gap is tool servers: 56 modules, over half.** A module can already run one; what is missing is anything saying it offers tools. That is plausibly a provision rather than new vocabulary, which would need nothing added — not yet decided, and recorded as undecided. **The gap most worth closing is health, at seven modules.** The mesh knows a container is running, which is not whether it answers, and this project has paid for that distinction twice. An action with a verify is exactly the right shape and may not arrive over the link, so a module cannot declare one. Two things are missing deliberately and say so: stage hooks, because the link may not carry an action and a module needing setup ships a program; and flavours, retired in favour of claims. Config merging is missing and should stay missing. A mechanism that understands TOML gets asked for YAML, then INI, which is how the thing being replaced became unholdable. Also records what the survey found that is not about coverage: manifests that had stopped matching what was actually brokered, one fact derived in two places giving two answers, and a live listing returning credentials in plaintext.
This commit is contained in:
@@ -0,0 +1,120 @@
|
|||||||
|
---
|
||||||
|
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`
|
||||||
|
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 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.
|
||||||
@@ -25,6 +25,7 @@ document is written and this one's status becomes `implemented`.
|
|||||||
| [`13-credentials-and-their-rotation.md`](13-credentials-and-their-rotation.md) | Credentials, and moving them without a consumer holding one the provider does not know about | [ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md), [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) |
|
| [`13-credentials-and-their-rotation.md`](13-credentials-and-their-rotation.md) | Credentials, and moving them without a consumer holding one the provider does not know about | [ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md), [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) |
|
||||||
| [`14-model-access.md`](14-model-access.md) | Model access as a provision, and what a licence is bound to | [ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md), [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) |
|
| [`14-model-access.md`](14-model-access.md) | Model access as a provision, and what a licence is bound to | [ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md), [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) |
|
||||||
| [`15-the-agent-session.md`](15-the-agent-session.md) | One mechanism started twice — a node's session and the mesh's | [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md), [ADR 0026](../../02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md) |
|
| [`15-the-agent-session.md`](15-the-agent-session.md) | One mechanism started twice — a node's session and the mesh's | [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md), [ADR 0026](../../02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md) |
|
||||||
|
| [`16-module-coverage.md`](16-module-coverage.md) | What a module must be able to say, measured against 127 that exist | [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md), [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) |
|
||||||
|
|
||||||
## Not yet written
|
## Not yet written
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-09-01
|
||||||
|
located-in: [mesh-control]
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 021 — A consumer on the provider's machine is given no credential
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
A module that requires something answered **on the same machine** resolves cleanly and is given
|
||||||
|
**no credential at all**. Two modules, zero needs:
|
||||||
|
|
||||||
|
```
|
||||||
|
postgres provides postgres-database, grants /var/lib/postgres/grants
|
||||||
|
keycloak requires postgres-database, secrets /var/lib/keycloak/database.env
|
||||||
|
→ modules: 2, needs: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing is refused and nothing is reported. The consumer's `secrets:` path is simply never
|
||||||
|
written, and whatever reads it fails later, somewhere else.
|
||||||
|
|
||||||
|
## Where it comes from
|
||||||
|
|
||||||
|
The world a node resolves against is **every other node**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
for _, n := range nodes {
|
||||||
|
if n.Name == exclude { continue }
|
||||||
|
```
|
||||||
|
|
||||||
|
So a provider on the same machine is never a `Provider` in `world.Offered`, never becomes a
|
||||||
|
`Needed`, and the credential loop — which walks `resolved.Needs` — has nothing to walk. Every step
|
||||||
|
is individually reasonable and the sum is a silent gap.
|
||||||
|
|
||||||
|
## Why it was not noticed
|
||||||
|
|
||||||
|
**Everything proven so far was cross-machine.** The lab's provisioner scenarios put the consumer on
|
||||||
|
one node and the provider on another, which is the interesting case for a *mesh* and the rare case
|
||||||
|
in practice. The first module to want a database on its own machine was the first real one.
|
||||||
|
|
||||||
|
The postgres provisioner even records the assumption in passing — *"Node is empty for a module on
|
||||||
|
this machine, which is asking for something local and is not this provisioner's business"* — which
|
||||||
|
reads as a deliberate exclusion of local consumers.
|
||||||
|
|
||||||
|
## Why the assumption is wrong
|
||||||
|
|
||||||
|
It holds for a process on the machine reaching a unix socket, where the operating system can vouch
|
||||||
|
for who is calling. **It does not hold for containers**, which is how nearly everything runs here: a
|
||||||
|
module's containers reach a provider's containers over TCP on a shared network, and the database
|
||||||
|
asks for a password exactly as it would from another machine.
|
||||||
|
|
||||||
|
**The machine is not a trust boundary once both sides are containers.** Treating it as one gives
|
||||||
|
the most common arrangement — a service and its database on one node — the weakest handling.
|
||||||
|
|
||||||
|
## What it is not
|
||||||
|
|
||||||
|
Not the same as [`020`](../020-a-certificate-is-issued-and-never-collected/00-report.md) or a
|
||||||
|
provisioner defect. The provisioner never sees these consumers because the mesh never records
|
||||||
|
them as consumers.
|
||||||
|
|
||||||
|
## What a fix has to keep
|
||||||
|
|
||||||
|
- **A local consumer still appears in the provider's grants**, so its provisioner creates the role
|
||||||
|
or bucket or client, exactly as for a remote one.
|
||||||
|
- **The credential is still sealed**, to the one node that is both ends. The mesh holding a
|
||||||
|
readable secret for local consumers would be a hole opened for convenience.
|
||||||
|
- **Refusing must stay refusing.** A requirement nothing answers is still refused; this is about a
|
||||||
|
requirement that *was* answered.
|
||||||
Reference in New Issue
Block a user