Merge pull request 'Two graphs, the builder's arrival, and two findings from building on a live mesh' (#37) from feat/two-graphs-and-the-build-chain into main

This commit was merged in pull request #37.
This commit is contained in:
2026-09-13 11:17:48 +02:00
5 changed files with 307 additions and 22 deletions
@@ -0,0 +1,94 @@
---
topic: the tiers
status: accepted
date: 2026-09-13
deciders: jochen
reconstructed: false
extends: 0070-the-catalogue-owns-the-module-graph.md
---
# 73. The installer carries a builder, and the registry stays where it is
## Context
[ADR 0070](0070-the-catalogue-owns-the-module-graph.md) decided that genesis builds rather than
carries, and [ADR 0071](0071-where-genesis-gets-its-source.md) settled where it clones from. Two
questions were left open, and the design record names them as the one gap that stops a fresh mesh
from being able to produce anything at all: **how the builder arrives**, and **what it publishes
into**.
Today the installer carries the control plane's image inside itself. That works, and it is why
genesis needs no registry: nothing is ever fetched, because the one image that matters is already
present. The cost is that the mesh which results holds an artifact it did not make, cannot rebuild,
and knows nothing about — no version, no source, no edges. That is the same shape as the fault
[issue 044](../04-ISSUES/044-the-runtime-every-module-builds-on-cannot-be-built-by-the-mesh/00-report.md)
recorded for the shared runtime, and fixing it there while shipping it here on every new mesh would
be a strange place to stop.
## Decision
**The installer carries a builder, and nothing else.** One artifact, not a growing set. It clones
the source at a named commit, checks what it got ([ADR 0071](0071-where-genesis-gets-its-source.md)),
and produces the control plane from the same repository and path that any later rebuild of it would
use. What raises the mesh is therefore the same thing that will maintain it, and there is no second
mechanism kept in step with the first.
**The registry does not move, and the argument for moving it does not survive being made.**
It was put this way: a produced image has to be put somewhere before anything can fetch it, so the
registry must now precede the control plane, and
[ADR 0033](0033-the-substrate-is-a-store-and-a-broker.md)'s answer to that question has to flip.
It does not, because the premise is false. **The thing that builds the image and the machine that
runs it are the same machine.** A built image is already in that machine's container runtime, and
the temporary control plane names it exactly as it names a carried one — by the digest of its own
configuration, a local identity that requires nothing to have served it. Building changes where the
bytes came from. It does not change where they are.
| | is it substrate? | must it precede the control plane? |
|---|---|---|
| the store | yes | yes — there is nowhere else to put the control plane's state |
| the broker | yes | yes — the control plane reaches a machine only over it |
| the image registry | yes — it cannot grant itself a repository | **still no** — the first machine neither fetches the control plane nor needs to, whether the image was carried in or made here |
So the registry stays where [ADR 0033](0033-the-substrate-is-a-store-and-a-broker.md) put it:
substrate by role, ordinary by delivery, installed by the temporary control plane as its first act.
The bundle carries two services and a control plane, as it did. **What publishes into the registry
is unchanged too** — the existing step that pushes the control plane's image into it, which is the
moment that image first receives a digest assigned by something other than itself. It now pushes
something this mesh built rather than something it was handed.
## Consequences
**Genesis gains one step and changes no others.** A build happens before the image is loaded. The
pivot described in [ADR 0067](0067-genesis-is-a-pivot.md) survives exactly as written, because the
step it pivots on never cared where the image came from.
**A fresh mesh can produce from the moment it exists.** The builder is present before the control
plane is, so the core modules, the catalogue and the builder's own module can be built in the
ordinary way rather than waiting for somebody to carry them in. The paragraphs in
[`17-raising-a-mesh`](../03-DESIGN/01-to-be/17-raising-a-mesh.md) that describe this were describing
something that could not start; they can start now.
**Genesis needs more of the outside world.** Carrying an image needed nothing but the installer.
Building one needs the source, and whatever the build itself reaches for. This is a real cost and
is not waved away: it makes genesis fail in more ways, all of them at a step that says what it was
doing. It is accepted because the alternative is a mesh that cannot rebuild its own control plane,
which fails in exactly one way, silently, later, and for ever.
**A pre-built bundle remains possible and is not this.** Nothing here forbids delivering artifacts
rather than building them; it fixes where they may come from. A bundle of pre-built core modules is
an **export of a mesh that built them**, carrying what the catalogue knows about each alongside the
artifact itself — so that loading one leaves the graph in the state building would have left it. A
bundle that carries images without that is the thing this decision rejects, whoever ships it.
## What this does not decide
**Whether the builder's own module is carried or built.** It builds everything else; what installs
*it* as an ordinary module afterwards, so that it too can be upgraded, is the same closed-list
question [`12-a-module-repository`](../03-DESIGN/01-to-be/12-a-module-repository.md) already holds,
and is unchanged by this.
**How a machine authenticates to a registry that asks it to.** Genesis raises its own and reaches it
over the loopback, so this remains a joining problem
([issue 042](../04-ISSUES/042-nothing-gives-a-node-an-account-for-a-registry/00-report.md)).
+1
View File
@@ -105,6 +105,7 @@ python3 00-META/checks/index.py fail if stale
- **0070** — [The catalogue owns the module graph, and genesis builds rather than carries](0070-the-catalogue-owns-the-module-graph.md)
- **0071** — [Genesis clones from a mesh, and checks what it got](0071-where-genesis-gets-its-source.md)
- **0072** — [Two graphs, and a build chain that orders itself](0072-two-graphs-and-the-build-chain.md)
- **0073** — [The installer carries a builder, and the registry stays where it is](0073-the-installer-carries-a-builder.md)
### What runs on them, and how it gets there
+40 -22
View File
@@ -36,34 +36,44 @@ Confusing the two is what produced a procedure that only ever worked in a fixtur
raises four machines the same way has not tested genesis at all — it has tested joining, four
times, with the first one hand-fed.
## Genesis is decided to change, and has not yet
## What changed, and what did not
*2026-09-12.* [ADR 0070](../../02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md) settles
that the installer carries an **init builder** rather than the control plane's image, and that the
core modules — control plane, catalogue, builder — are **built on the machine** before a mesh exists
to install anything. One thing is carried, and it is a builder rather than a result, which is what
gives the builder and the catalogue a route they did not have.
*2026-09-13.* The installer carries a builder now, and builds the control plane it raises. Three
records settle it: [ADR 0070](../../02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md) that
genesis builds rather than carries, [ADR 0071](../../02-DECISIONS/0071-where-genesis-gets-its-source.md)
where it clones from, and [ADR 0073](../../02-DECISIONS/0073-the-installer-carries-a-builder.md) how
the builder arrives — which also records an argument that failed. It was put that a produced image
must be published before anything can fetch it, so the registry would have to come up before the
control plane. It does not: the machine that builds the image is the machine that runs it, and a
local image is named by the digest of its own configuration exactly as a carried one is. **Building
changes where the bytes came from, not where they are.**
**The section below describes what the installer does today**, which is to carry the control plane's
image and publish it once there is a registry. It is kept as written because it is true of the
program that exists, and replacing it with the intention would leave nothing describing the thing
anybody actually runs. The order changes when the init builder is built; the pivot does not.
So the pivot is unchanged, the registry is where it was, and one step was added before the bundle is
written. What follows describes the program that exists.
## Genesis
The installer is a single program carrying the control plane's image inside it. That is what makes
genesis possible without a network to fetch from and without a registry to name: the image is
present because the installer is present.
The installer is a single program carrying **the builder** inside it — not the control plane
([ADR 0073](../../02-DECISIONS/0073-the-installer-carries-a-builder.md)). What cannot be fetched is
the thing that does the fetching, so that is what is carried; everything else is made here.
It proceeds in one direction, and every step is safe to run again.
**First it refuses to start if the machine is not ready.** A container runtime, the ability to
write where it must write, the host binary where it expects it. A machine that is not ready is told
what is missing rather than half-changed.
write where it must write, the host binary where it expects it — and a repository and a commit to
build from, because an installer told nothing would raise a store and a broker and then have
nothing to raise a control plane from. A machine that is not ready is told what is missing rather
than half-changed.
**Then it loads the carried image and describes what the machine will become.** The image is named
by the digest of its own configuration — content-addressed and unforgeable, and requiring nothing
to have served it. This is legal precisely where nothing could have served one, and nowhere else.
**Then it loads the carried builder and builds the control plane with it**, from a repository on a
mesh that already exists and a named commit ([ADR 0071](../../02-DECISIONS/0071-where-genesis-gets-its-source.md)).
This is the same repository and path every later rebuild of the control plane will use, so what
raises the mesh is the same thing that will maintain it.
**Then it describes what the machine will become.** The image it just made is named by the digest
of its own configuration — content-addressed and unforgeable, and requiring nothing to have served
it. That is legal precisely where nothing could have served one, and it is why building here needs
no registry: the machine that made the image is the machine that will run it.
**Then it raises the substrate and a temporary control plane, and waits for that control plane to
answer.** At this point the machine is a mesh of one node with nothing joined to it.
@@ -149,9 +159,16 @@ needs a toolchain and a working tree, which is most of the burden the installer
**A mesh cannot say how it was raised.** Nothing afterwards can contradict a claim that a machine
was brought up the supported way, so the rule that it must be is, today, unenforced.
**The init builder is not built.** Until it is, the installer carries the control plane's image and
nothing gives a fresh mesh a builder or a catalogue, so the paragraphs above describing the core
modules being built describe something that cannot yet start.
**The builder's own module is not installed by genesis.** The installer carries a builder and uses
it, so a fresh mesh is raised on something it built; but nothing afterwards installs that builder as
an ordinary module on the machine, so the mesh cannot yet be asked to build anything else. The
paragraphs above describing the core modules being built can start now — a builder exists and has
somewhere to publish — and nothing yet starts them.
**A module's declaration still has to be copied onto the machine by hand.** The installer reads the
registry's and the control plane's manifests from a checkout somebody put there. The control plane's
now lives in the control plane's own repository, which the installer clones anyway, so this is a
thing that can be removed rather than a thing that must be designed.
**A machine has no account for a registry that asks for one.** The mesh grants a consumer a
credential for a database; it does not yet do so for the store its own images live in. Genesis
@@ -167,6 +184,7 @@ pulling problem, not a genesis one.
| Every step may be run again | The installer is re-run against a raised machine and must change nothing and report why. |
| An image is named exactly | A machine refuses a bundle naming an image by tag. The refusal is exercised, not assumed. |
| The installer is what installed this | **Nothing.** See above. |
| The builder can arrive on a fresh mesh | **Nothing.** There is no route for it yet. |
| The builder can arrive on a fresh mesh | The installer carries it, and the genesis bed raises a machine by running the installer. A bed that raises one any other way fails its own acceptance check. |
| The control plane a mesh runs is one it built | The genesis bed asserts the running control plane is pinned to a digest this mesh's own registry serves, for an image built from a named repository and commit — not one the installer carried. |
| A core module is built rather than only carried | The control plane is rebuilt from its own repository and path, and the running mesh is upgraded to the result — the same path any module takes. |
| Installing produced a mesh that can produce | After installing, a module with source of its own is asked for and comes back pinned to a digest this mesh's registry assigned, not to a placeholder. |
@@ -0,0 +1,112 @@
---
status: resolved
opened: 2026-09-13
located-in: [mesh-tools, mesh-sdk]
fixed-by: mesh-tools feat/run-once-entry, mesh-sdk feat/adr-ref-resync
amended-design:
---
# 044 — The runtime every module builds on cannot be built by the mesh
## Symptom
Every module written in the mesh's own toolchain is compiled on top of one shared base image — the
tool runtime. The mesh can build the modules. It cannot build the base.
Two things stop it, and each is enough on its own:
- The base's container definition copies a compiled output directory that is not in the repository.
A clean clone has the sources and no compiled output, so the definition refers to something that
is not there.
- The base's dependency on the mesh's own software development kit resolves to a **sibling checkout
on disk**, not to anything a clone can fetch:
```
"node_modules/@novox/mesh-sdk" -> { "resolved": "../mesh-sdk", "link": true }
```
Both mean the same thing: the base can only be produced on a workstation that happens to have the
neighbouring repositories laid out beside it, and has run a compile step by hand first.
Observed while building four modules through the mesh from a repository and a path. All four
succeeded, and all four recorded that they were built on top of the same base — an image reference
that no module in the mesh produced, because no module could have.
## Why this matters
**This is the one artifact the whole build chain rests on, and it is the one artifact outside the
chain.** Three separate consequences, all visible today:
- *The base is pinned by hand.* Every module's container definition names it as a literal digest
written by a person. Nothing checks that digest against anything.
- *Nothing can tell when it moves.* A build records what it was built on top of, so an artifact is
stale when anything beneath it moved. That rule cannot fire for the base: the graph has edges
pointing at it, and no version on the other end, because no build ever registered one. The mesh
is therefore unable to answer "what must be rebuilt" for the change that would reach *every*
module at once.
- *Nobody can check what is in it.* The published base and the definition in the repository have
already drifted: the definition names one operating system base, and the image actually serving
every module is built on a different one. This was found by running the image, not by reading
anything, and it went unnoticed for exactly as long as nobody could rebuild it.
A rule the design states — an artifact is stale when anything it was built against moved — is
unenforceable for the artifact it matters most for. That is the shape of a wrong rule, not a
missing feature: everything downstream reports "nothing to rebuild" and is believed.
## How it would be checked
The check is the fix's own test: clone the repository alone, into an empty directory, and build it.
Nothing else present, no neighbouring checkouts, no compile run first. If that produces the base,
the mesh can produce it too, because that is all the mesh does.
## Open questions
- Where should the software development kit come from, for a build that has only one repository?
The mesh already knows how to run a package registry as a module, and it already publishes
container images to one of its own — so this may be the same answer twice rather than a new one.
- Is the base a module like any other, or is it one of the few things a new mesh must arrive
carrying? It behaves like both: everything is built on top of it, and it is built out of the same
sources as everything else.
- If it becomes an ordinary module, is there a circularity? **No** — checked rather than assumed:
the builder is a compiled program built from its own sources and a language toolchain, and does
not stand on this base at all. Only modules written in the mesh's scripted toolchain do. So the
thing that would build the base is not one of the things built on it, and the chain has an end.
## Resolved, 2026-09-13
The base is a module now, built from its own repository and nothing else.
Three things were in the way, and the third was not visible until the first two were gone:
- **The toolkit could not be fetched.** It is named by a pinned commit of its own repository rather
than by a folder on somebody's disk. Both repositories turned out to be readable without a
credential, so no package registry was needed after all — which is why this was smaller than it
looked.
- **The toolkit shipped no compiled output.** Every one of its entry points pointed into a directory
that is not in source control. It declares the hook that is meant to compile it on install, and
the package manager in use does not run that hook — so the build compiles it explicitly instead.
Relying on a hook firing would have failed the same invisible way this issue is about.
- **The base is also the compile environment.** Every module's recipe starts from this image and
invokes the compiler out of it. A first attempt dropped the build tools to make a smaller image,
which broke every module built on it. They stay, and the recipe now says why.
The drift this issue reported is also closed: the recipe claimed one operating system while serving
another, and now states what is actually in service. Changing that was deliberately *not* folded in
— a module already builds against it with that system's package manager, and moving the ground
under every module at the same moment as making it buildable would be two changes wearing one
commit.
**Checked the way the report said it should be.** The repository was cloned alone, with nothing
beside it, and built by the mesh. Then every module was moved onto the result, and the base changed
for real: all three went stale, each naming the base and the commits it moved between, each listed
once. Changing only a comment in the base — a new commit, a byte-identical image — correctly makes
nothing stale, which is a second bug this exercise found and fixed: staleness was comparing commits
when it should compare artifacts, so the mesh would have rebuilt itself entirely to arrive back
exactly where it started.
## What is still true
A module names its base as a literal fingerprint in its own recipe. So the mesh can now *say* that
three modules must be rebuilt, and rebuilding them still produces the old base until somebody edits
that fingerprint. Noticing is solved; acting on it is the build chain driving itself, which is
separate work.
@@ -0,0 +1,60 @@
---
status: open
opened: 2026-09-13
located-in: []
fixed-by:
amended-design:
---
# 045 — A container keeps the values it started with
## Symptom
A module reads its database credentials from a file the mesh composes. The file was written, then
rewritten with a different value once the mesh had more to say — the provider's address was not
known the first time and was the second.
The container went on running with the first version, indefinitely, and reported nothing. Its own
view:
```
DATABASE_URL=postgresql://…@:20000/… what the container holds
DATABASE_URL=postgresql://…@<provider>:20000/… what the file on disk says
```
A module may declare that it restarts when one of its files changes, and this one now does. That
did not help: the restart fires when the file changes *during an apply*, and the change had already
happened before the declaration naming it arrived. Applying again is a no-op, because nothing
changed that time either. The only thing that recovered it was removing the module from the machine
and putting it back.
## Why this matters
**Reconciliation compares intentions, not what is actually running.** Two intentions in a row can
both be applied successfully and still leave a container holding values from neither, because a
container captures its environment once, when it is created, and nothing afterwards re-reads it.
The failure is silent in the worst way: the container is up, the machine reports success, the mesh
reports every module current, and the thing inside is using a credential the mesh no longer
believes in. Nothing in the system is in a position to notice — the host knows what it wrote, and
the module knows what it read, and no one compares the two.
It is not specific to credentials. Any composed value delivered through a file a container reads at
start has the same shape, which is most of what the mesh delivers.
## How it would be checked
A machine reporting a module as running should be able to say *what it is running with* — not what
was last written for it. The check is then a comparison the mesh can make on every heartbeat rather
than a property of one apply: for each container, does what it holds match what the files it was
built from now say. A mismatch is a drift the mesh can act on, and today it is not expressible at
all.
## Open questions
- Should the host recreate such a container on its own, or report the drift and let the mesh decide?
Recreating is the obvious repair and is also an unannounced restart of a running service.
- Is "the files a container was created from" something the host already knows, or does a
declaration have to say it? A restart-on list is close to this, but it is written by the module
author and is therefore exactly as complete as they remembered to make it.
- Does the same gap exist for values the mesh delivers by other means than a file?