Files
hq/03-DESIGN/01-to-be/17-raising-a-mesh.md
T
jschoubben 33a00d5656 Adopt the glossary's vocabulary in the mutable design docs
"control plane" -> controller and "substrate" -> foundation throughout
03-DESIGN, 00-META and the README, with 06-the-control-plane.md and
07-the-substrate.md renamed to 06-the-controller.md and 07-the-foundation.md.
The immutable 02-DECISIONS records keep their original wording (and links to
them are unchanged) — a term retired here may still appear there, which the
glossary explains how to read.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 18:48:52 +02:00

204 lines
13 KiB
Markdown

---
layer: to-be
status: in-progress
code:
- mesh-host cmd/mesh-bootstrap
- mesh-host internal/bootstrap
- mesh-lab test/integration/whole-mesh-full.test.ts
updated: 2026-09-12
decisions:
- 02-DECISIONS/0067-genesis-is-a-pivot.md
- 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
- 02-DECISIONS/0005-the-node-host.md
- 02-DECISIONS/0010-delivery.md
---
# Raising a mesh
How a mesh comes into existence on machines that have none, and how a machine joins one that
already exists. This is the procedure an operator runs. It is not a description of the lab, and
the lab does not have one of its own.
## Two moments, and only two
A mesh is raised once and joined many times, and the two are not variations of each other.
**Genesis** happens on one machine, when there is no mesh. Nothing can be asked, nothing can be
granted, and nothing has been published anywhere. It is the only moment at which the ordinary
rules cannot all hold at once, and it is resolved by a pivot
([ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md)).
**Joining** happens on every machine after the first, and it is ordinary. A mesh exists, so it can
be asked for a token and told what the machine should be. Joining installs the host and nothing
else: no temporary anything, no foundation raised by hand, no registry.
Confusing the two is what produced a procedure that only ever worked in a fixture. A bed that
raises four machines the same way has not tested genesis at all — it has tested joining, four
times, with the first one hand-fed.
## What changed, and what did not
*2026-09-13.* The installer carries a builder now, and builds the controller 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
controller. 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.**
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 builder** inside it — not the controller
([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 — 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 controller from. A machine that is not ready is told what is missing rather
than half-changed.
**Then it loads the carried builder and builds the controller 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 controller 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 foundation and a temporary controller, and waits for that controller to
answer.** At this point the machine is a mesh of one node with nothing joined to it.
**Then the machine joins the mesh it is itself running.** It enrols, and an agent runs on it. Being
heard from once is not the same as an agent running, and the installer checks both, because
enrolling is itself the thing that makes a mesh hear from a machine.
**Then it installs a registry**, so the mesh has somewhere to keep its own images.
**Then it publishes the controller's image to that registry**, which is the moment the image
first receives a digest assigned by something other than itself. This is the carrying step, and it
is the same step for all three things the build loop cannot produce for itself — the controller,
the registry, and the builder. The rule and its closed list are in
[`12-a-module-repository`](12-a-module-repository.md#the-three-the-loop-cannot-build-and-there-are-only-three).
**Then it installs the controller again, as an ordinary module pinned to that digest, and drops
the temporary one.** The pivot is complete: what raised the mesh is gone, and what runs is a module
like any other. From here the mesh can build and roll out its own upgrades, including to the thing
that runs it.
## After the pivot, and still part of installing
Genesis ends with a mesh of one that runs, and that is not the same as a mesh that works. What it
has is a controller, a store, a queue and a registry. What it cannot yet do is **produce
anything** — and almost every module in the catalogue is waiting to be produced, because a manifest
names what its artifacts are and nothing has made them.
So installing continues:
**The builder arrives, and installing is what brings it.** It is a module like any other and is
assigned to a machine like any other, but it cannot be built by the thing it is — see
[`12-a-module-repository`](12-a-module-repository.md#the-three-the-loop-cannot-build-and-there-are-only-three).
So it is carried, and it is already here: it is what built the controller. The last step of
installing publishes it into the mesh's own registry, installs it as an ordinary module pinned to
that digest, and issues it a broker account — the same two acts the controller went through,
plus the one thing only a builder needs. The account is issued before the machine is sent anything,
because a builder that arrives without its credential starts, finds nothing it may read, and waits,
which looks exactly like a builder with no work.
**The core modules are built.** Each is named by a repository and a path
([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)), and the builder is
asked for each in turn: it clones, reads the manifest at that path, produces what it declares,
publishes each artifact into the mesh's own registry, and hands back the module with its artifacts
pinned and the commit recorded. The mesh records that, and from then on the module is described by
something it made rather than by a placeholder.
**The controller is built like the rest.** It was carried in and published once, which got the
mesh running; building it from its own repository and path is what makes it upgradeable. The first
time that happens is the moment the mesh stops depending on the installer for anything.
**And then the catalogue.** Every module with source of its own is built the same way. Until this
has happened a mesh can install only what is public or carried, which is the foundation and little
else.
Only after all of that is the ordinary loop available: change a module's source, the mesh notices
its own copy is older than the source, rebuilds it, and rolls it out. That loop is what makes
moving services across one at a time possible, so it is part of installing rather than something
that comes later.
What remains after *that* belongs to somebody else: adding machines, and deciding what they run.
## Joining
A machine joins with the host binary and a token. It does not raise a foundation, does not install a
registry, and is never enrolled twice. The mesh already knows how to tell a machine what to be;
joining is the point at which a machine starts listening.
## Where the line falls
The installer owns everything that is the mesh's own. The lab owns only what is the lab's: raising
virtual machines, giving them addresses that resolve nowhere, and injecting faults.
**The lab runs the installer. It does not describe installation.** This is the whole point. A
second description kept in step with the first is the arrangement that already failed — the fixture
invented a registry that exists in no production, and hid two separate faults for as long as it
existed. Anything the lab must do that the installer does not is either a lab concern or a hole in
the installer, and saying which is a decision, not a convenience.
## What is not yet true
Stated plainly, because a document that implies otherwise is worse than none.
**The installer is not packaged.** It builds from source. An operator raising a first machine still
needs a toolchain and a working tree, which is most of the burden the installer exists to remove.
**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.
**Nothing asks for the core modules yet.** Installing ends with a builder that works — a raised
machine will build the shared base and a module on top of it when asked — and nothing asks. The
paragraphs above describing the catalogue being built are a thing somebody now types, rather than a
thing that cannot happen.
**A module's declaration still has to be copied onto the machine by hand.** The installer reads the
registry's and the controller's manifests from a checkout somebody put there. The controller's
now lives in the controller'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
avoids the question by carrying the image it needs, which makes this a joining problem and a
pulling problem, not a genesis one.
**The SDK still comes from a git URL, and the ordering that fixes it is decided but not built.**
[ADR 0076](../../02-DECISIONS/0076-the-sdk-is-a-published-package.md) settles that the package
registry (gitea) comes up and the SDK is published into it *before* the base toolchain is built, so
the toolchain resolves the SDK by version rather than cloning it — closing
[issue 053](../../04-ISSUES/053-the-sdk-is-pinned-twice-and-the-two-disagree/00-report.md). The
builder already knows how to be handed a package-registry credential and inject it into a build; what
is not yet wired is the genesis step that raises gitea and publishes the SDK ahead of the base, and
the toolchain's own manifest still names the SDK by a git URL. Until both land, the base build clones
the SDK inside `docker build`, which is slow and names a branch head rather than a version.
## How these rules are checked
| Rule | Checked by |
|---|---|
| Genesis works on a machine that is not the lab | The bed raises its first machine by running the installer, not around it. A bed that stops doing so fails its own acceptance check. |
| The other machines join, and are not re-raised | The bed gives them the host binary and a token only. A second enrolment of the first machine is a failure, not a no-op. |
| 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 | The installer carries it and installs it as its last step, 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 controller a mesh runs is one it built | The genesis bed asserts the running controller 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 controller 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 | 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. **Done by hand on a raised machine, not yet by a bed** — it built the shared base and then a module naming that base. Nothing automated asserts it, which makes this the weakest check on this page. |