diff --git a/03-DESIGN/01-to-be/16-module-coverage.md b/03-DESIGN/01-to-be/16-module-coverage.md new file mode 100644 index 0000000..12b794f --- /dev/null +++ b/03-DESIGN/01-to-be/16-module-coverage.md @@ -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. diff --git a/03-DESIGN/01-to-be/README.md b/03-DESIGN/01-to-be/README.md index cab4a7b..fda651a 100644 --- a/03-DESIGN/01-to-be/README.md +++ b/03-DESIGN/01-to-be/README.md @@ -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) | | [`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) | +| [`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 diff --git a/04-ISSUES/021-a-consumer-on-the-providers-machine-is-given-no-credential/00-report.md b/04-ISSUES/021-a-consumer-on-the-providers-machine-is-given-no-credential/00-report.md new file mode 100644 index 0000000..8df93be --- /dev/null +++ b/04-ISSUES/021-a-consumer-on-the-providers-machine-is-given-no-credential/00-report.md @@ -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.