Reconcile: adopt initialization's consolidated HQ as canonical, re-home this session's new work #24
@@ -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) |
|
||||
| [`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.
|
||||
Reference in New Issue
Block a user