Reconcile: adopt initialization's consolidated HQ as canonical, re-home this session's new work #24

Merged
jschoubben merged 177 commits from reconcile-init-into-main into main 2026-09-05 10:27:11 +00:00
3 changed files with 192 additions and 0 deletions
Showing only changes of commit 36d342f176 - Show all commits
+120
View File
@@ -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.
+1
View File
@@ -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
@@ -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.