0133 — a module owns its migrations and the mesh owns when they run. A container declares what must run before it; the mesh derives the gated step from the resource it precedes, so the image, the environment and the credentials come from the one place they are described. The module owns the SQL, the dialect and the lock; the mesh owns the moment and refuses to start a version whose step failed. Per node, with no level: a step that ran once somewhere leaves every other machine ungated, and 'once, mesh-wide' is what holding a seat already means. 0134 — the pipeline is observable from a merge to an artifact and goes dark at the machine. What a node now runs, and what it refused, become facts under the control plane's own seat, emitted when what a machine runs changes rather than on every convergence pass. Design 32's lifecycle carries both; issue 133 points at them as what ends the matter it opened.
127 lines
8.2 KiB
Markdown
127 lines
8.2 KiB
Markdown
---
|
|
topic: the mesh
|
|
status: accepted
|
|
date: 2026-09-28
|
|
deciders: jochen
|
|
reconstructed: false
|
|
---
|
|
|
|
# 134. The mesh says what it applied
|
|
|
|
## Context
|
|
|
|
The pipeline is observable on the bus from a merge to an artifact, and modules already plug into it:
|
|
the forge emits `pull.merged`, the build machine's seat emits `built`, the catalogue emits `registered`,
|
|
`upgraded` and `rebuild-needed`, providers emit `postgres.database.provisioned` and its siblings. Things
|
|
consume them today — the catalogue consumes `built`, each provider consumes its own provisioning events,
|
|
model-usage consumes `*.usage.*`, the audit logger consumes `**`. Nothing had to be invented for any of
|
|
that; subscribing *is* plugging in.
|
|
|
|
**It goes dark at the moment it touches a machine.** A host applies a declaration and reports to the
|
|
control plane on the control branch, which only the control plane may read — correctly, because a report
|
|
carries what a machine is and enrolment travels the same way. So nothing on the mesh says *this machine
|
|
now runs version Y of module Z*, or that it refused to, or why.
|
|
|
|
What that cost on 2026-09-28, in one morning:
|
|
|
|
- A build result the store refused was visible only to whoever was waiting on that build's reply. For
|
|
three quarters of an hour the mesh built things and recorded none of them, while the overview said
|
|
every module was current ([issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)).
|
|
- A module crash-looping at start — 338 restarts — was found by reading a container's logs by hand.
|
|
Nothing on the bus said the mesh's graph had stopped learning.
|
|
- A run-once step that fails now stops an upgrade by design ([ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md)),
|
|
and the same silence would cover it: the version simply would not appear.
|
|
|
|
**And the one thing the control plane does emit is refused by its own account.** Answering a catalogue
|
|
that asks to catch up, it publishes each recorded build under `mesh.mod.control-plane.event.…` — a
|
|
module namespace for a module that does not exist. Its own permissions refuse it, so a catalogue that
|
|
restarts gets nothing and keeps its gap. The control plane has facts to state and nowhere to state
|
|
them.
|
|
|
|
## Decision
|
|
|
|
**The mesh emits the deploy half of the pipeline as facts on the bus.** What a machine now runs, and
|
|
what it refused to run and why. Both are facts about the mesh doing its work, in the same form as every
|
|
other fact on the bus, so anything that wants them subscribes the way the catalogue subscribes to
|
|
`built`.
|
|
|
|
**The control plane states them, as the holder of the `mesh-controller` seat.** Its facts live under the
|
|
seat's own namespace, which is where a role's events belong
|
|
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md),
|
|
[ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md)) and which survives the control plane being
|
|
replaced. That is also what gives the catch-up replay a subject it may publish instead of an invented
|
|
module namespace.
|
|
|
|
**Emitted when what a machine runs changes, not on every convergence pass.** A host reconciles
|
|
continuously and reports each time; a fact per pass would be a fact per minute per machine that says
|
|
nothing. The report carries the declaration it applied and what changed, so the control plane has what
|
|
it needs to speak only when there is something to say.
|
|
|
|
**A refusal is a fact with a subject in it** — which machine, which resource, and the reason as the host
|
|
gave it. A refusal that names only the machine is the silence this record is about, one level up.
|
|
|
|
**Reports stay where they are.** A node's report remains control traffic that only the control plane
|
|
reads. The deploy facts are derived from it, which makes them second-hand on purpose: one emitter, one
|
|
ordering, and no widening of the narrowest account in the mesh.
|
|
|
|
## Options considered
|
|
|
|
1. **Leave it as it is, and let whatever cares ask the control plane.** Rejected: asking for a fact that
|
|
already arrives is the shape the mesh removed everywhere else, and nothing can react at the moment a
|
|
machine changes — which is exactly when a graph, an audit or an operator wants to know.
|
|
2. **Each node emits its own facts.** Rejected: it widens every host's account to an event namespace, and
|
|
a host's authority is deliberately the narrowest in the mesh. Its report already reaches the one thing
|
|
that can speak for it.
|
|
3. **Widen who may read the control branch.** Rejected: that branch carries what machines say *to* the
|
|
control plane, enrolment included. Widening its readers widens that too, for an unrelated reason.
|
|
4. **A registry of deploy hooks** — something registers interest and is called. Rejected: an event is
|
|
already the mechanism; there is nothing to register, and a callback is an address the mesh spent
|
|
[issue 102](../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
|
|
learning not to keep.
|
|
5. **Put the facts on the node's own declaration stream.** Rejected: that stream is last-per-subject by
|
|
design, so a machine away for an hour gets exactly the current declaration and nothing older. A
|
|
history of what happened cannot live in a stream built to forget.
|
|
|
|
## Consequences
|
|
|
|
**The audit logger gets the deploy half for nothing**, because it consumes everything.
|
|
|
|
**A failure becomes visible where the mesh is watched** rather than where someone happened to be
|
|
looking. That answers the open question [issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)
|
|
left about a record the store refused.
|
|
|
|
**The catch-up replay stops being a burst of events.** With the control plane able to state its own
|
|
facts, replaying history as if it were happening now is a choice rather than the only option — and the
|
|
better shape is the question the catalogue is actually asking, answered once
|
|
([design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)).
|
|
|
|
**The facts are second-hand.** The control plane says what a machine reported, so a machine that cannot
|
|
reach the bus produces no fact at all. Absence is not health, and what a machine was last heard from
|
|
stays the place that says so.
|
|
|
|
**The events stream carries more.** Bounded by emitting on change rather than on every pass, and each
|
|
fact is small; the stream's own limits remain what keeps it finite.
|
|
|
|
## How this is checked
|
|
|
|
- **The control plane's grant names exactly the subjects it emits**, derived from its seat like every
|
|
other principal's, and the composed user list is compared against a golden file — so a fact it cannot
|
|
publish fails a test rather than a catalogue's replay.
|
|
- **A convergence that changed nothing emits nothing.** A test with two identical reports and one
|
|
expected fact, because the failure this guards against is a fact per minute per machine.
|
|
- **A refusal names its resource.** A test where a host reports a failed resource and the emitted fact
|
|
carries which one and why, not merely that something went wrong.
|
|
- **What the mesh emits is what something consumes.** The subject a module declares it consumes derives
|
|
to the subject the control plane publishes — the same agreement test that already keeps the
|
|
controller's own subscriptions honest.
|
|
|
|
## References
|
|
|
|
- [ADR 0041](0041-events-are-a-relationship.md) — an event is a relationship, not a call
|
|
- [ADR 0126](0126-a-module-declares-its-own-seats.md) — an event is addressed to its emitter, because the emitter's identity is the meaning
|
|
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md) — a role's events belong to the role
|
|
- [ADR 0083](0083-one-push-leaves-the-mesh-consistent.md) — a report is held for the store rather than lost
|
|
- [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) — the gate whose failure this makes visible
|
|
- [`03-DESIGN/01-to-be/32-what-a-module-declares.md`](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §6 — the lifecycle, which ends today at a report nobody else may read
|
|
- Measured 2026-09-28: 45 minutes of builds recorded nowhere with the overview reporting health; a module at 338 restarts found by hand; the control plane's only emitted event refused by its own permissions
|