"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
12 KiB
layer, status, code, updated, decisions
| layer | status | code | updated | decisions | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| to-be | proposed |
|
2026-09-15 |
|
The installation, in full
Every step from a machine with nothing to a mesh that maintains itself. Written out explicitly because it is the procedure everything else depends on, and because the parts that do not exist yet are easier to see beside the parts that do.
17-raising-a-mesh argues why it is shaped this way. This says what
happens, in order, with each step's name as the installer prints it.
What must be true before anything starts
| why | |
|---|---|
| a container runtime | the foundation is containers, and the installer refuses without one |
| the host binary, where the installer expects it | it is what the machine becomes |
| a repository and a commit to build from | the installer carries a builder, not a controller, so it must be told what to make (ADR 0073) |
| a way out to the internet | the store, the broker and the registry are pulled from it |
| a reachable git host, and a commit it serves | what is cloned is the trust anchor for everything this mesh will ever run (ADR 0071) |
| the address other machines will reach this one on | a token carries it verbatim; the installer refuses to guess |
Phase one — a machine becomes a mesh of one
Twelve steps, run by one program, each safe to run again.
| # | step | what happens | true afterwards |
|---|---|---|---|
| 1 | preflight |
everything above is checked | the machine is not half-changed by a missing prerequisite |
| 2 | load |
the carried builder image is loaded | the mesh has the one thing it cannot fetch — the thing that does the fetching |
| 3 | build |
the builder clones the named repository at the named commit and builds the controller | what will run is something this mesh made and can make again |
| 4 | bundle |
the foundation template is written out, with the built controller's id in place of the placeholder | the machine has a description of what it will become |
| 5 | apply |
store, broker, schemas, and a temporary controller are raised | a mesh of one exists and answers |
| 6 | verify |
the controller is asked, rather than assumed | it replies, and says it has no machines |
| 7 | enrol |
the machine joins the mesh it is itself running | the mesh has one node, and an agent runs on it |
| 8 | registry |
the mesh's own artifact store is installed | there is somewhere to keep what this mesh makes |
| 9 | publish |
the controller's image is pushed into it | the image has a digest something other than itself assigned |
| 10 | controller |
it is installed again, as an ordinary module pinned to that digest | what runs is a module like any other |
| 11 | retire |
the temporary controller is dropped | the pivot is complete — what raised the mesh is gone |
| 12 | builder |
the carried builder is published, installed as a module, and issued a broker account | the mesh can produce |
Steps 8 to 11 are the pivot (ADR 0067). Before them the controller is something the installer put there; after them it is something the mesh holds a record of and can upgrade. The account in step 12 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.
Phase two — a mesh of one becomes a mesh that works
Genesis ends with a mesh that runs, which is not the same as a mesh that works. It has a control plane, a store, a broker, a registry and a builder. It holds no module graph, has no private network, no packet filter, and cannot resolve a name. Calling that "installed" is what let the catalogue be missing from a test for weeks without anything complaining.
| # | step | what happens | why it is here |
|---|---|---|---|
| 13 | the shared base is built | the toolchain and runtime every module with code of its own stands on | nothing else with code can be built until it exists |
| 14 | a store module is built and run | a database provider, which the foundation's store is not | the foundation's store is the controller's own memory, and offers nothing to anything |
| 15 | the catalogue is built and run | the module graph | without it the mesh cannot say what it holds, what a change reaches, or what must be rebuilt |
| 16 | the catalogue asks for what it missed | the builds made before it existed are replayed | on a fresh mesh those are always the base, the store and the catalogue itself (issue 050) |
| 17 | the controller is rebuilt from its own repository | and rolled out through the module path | the moment the mesh stops depending on the installer for anything |
| 18 | networking is assigned and the node placed |
a private network, and names | assigning installs the module; placing says where this machine is on it. Both, or the names file is written empty |
| 19 | the packet filter is assigned | rules generated from what modules declared | until this, every rule the mesh computes has never been applied to anything |
Phase three — machines arrive
Only now. A machine joining a mesh that cannot build anything proves that enrolment works, which was never the doubtful part.
| # | step | what happens |
|---|---|---|
| 20 | a node record is made and a token issued | one-time, carrying the broker's address and the fingerprint to pin |
| 21 | the machine enrols and runs its agent | being heard from once is not an agent running; both are checked |
| 22 | it is placed on the private network | or nothing can bind a consumer on it to a provider elsewhere |
| 23 | modules are assigned to it | and it pulls what it needs from the mesh's registry |
What is not yet true
Stated plainly, because a procedure that implies otherwise is worse than none.
Steps 13 to 19 are not the installer's. They are things somebody types. The installer ends at step 12, and everything that turns a running mesh into a working one is manual — which is why a test had to be written to find out they were missing.
Step 23 does not work for a second machine. It has no account on the registry (issue 042) and no reason to trust a registry serving plain HTTP over the network (issue 048). Both are invisible on a mesh of one, where the registry is loopback.
There is no private package registry, and ADR 0014 assumes one: a module consumes its dependencies, the mesh's own shared library included, from it. At step 13 nothing has installed one, so the first build of the shared base resolves the SDK some other way — today by a git URL, which is issue 053.
The installer does not install the host's unit, though one exists. mesh-host/packaging/ ships
nox-mesh-host.service and two companions; the installer declines to place them because a unit file
is a packaging decision. So an install that does nothing further leaves a machine whose containers
come back after a reboot and whose agent does not — it runs the right things and can no longer be
told anything.
This is a gap in packaging, not in the mesh, and the distinction matters: the lab's
--host-in-background says in its own help that it does not survive a reboot, so a lab run failing
this is the lab being honest rather than the mesh being broken. What is missing is the step that
puts the shipped unit on the machine.
Nothing asserts a mesh was installed this way. A claim that a machine was brought up by this procedure cannot be contradicted by anything afterwards.
What a finished mesh holds
Twelve, and after the pivot none of them is a specialty. Every row is a module the mesh built, holds a version of, and can upgrade — which is the whole claim, and is not true today for the first two.
| # | module | provides | note |
|---|---|---|---|
| 1 | postgres |
postgres-database |
the controller's own records and every module's. One server, not two |
| 2 | lavinmq |
amqp |
the broker every node dials, and what modules are granted vhosts on |
| 3 | mesh-control |
claims the-controller |
decides what runs where |
| 4 | distribution |
artifact-store |
what the mesh built, pinned by digest — the module is the software (Distribution), the provision is the job |
| 5 | builder |
— | turns source into artifacts |
| 6 | mesh-tools |
build inputs | the base everything with code compiles against. Runs nowhere |
| 7 | mesh-catalog |
the module graph | what is held, what a change reaches, what must be rebuilt |
| 8 | networking |
private-network, naming |
requirements only — assigning it brings mesh-wireguard and mesh-names |
| 9 | dnsmasq + one of resolved-split-dns / resolv-conf |
wildcard-resolution |
names that actually resolve, on top of mesh-resolver's data |
| 10 | step-ca |
acme-ca |
certificates for .internal |
| 11 | firewall |
claims the-packet-filter |
rules generated from what modules declared |
| 12 | gitea |
package-registry |
where a module's dependencies come from (ADR 0014), and the git host |
Why it is twelve and not thirteen
The foundation's store and the postgres module are the same module. They were two rows while the
foundation was a different kind of thing: a store raised from a bundle cannot provide
postgres-database, so anything wanting a database needed a second server. That is visible on any
mesh built today — mesh-store and postgres, two containers, the same image.
The naming rule settles which name survives (ADR 0040):
Where the consumer speaks a protocol — a database's wire protocol and query dialect — the interface is the protocol. "database" is not a capability; the protocol is. Never false genericity: a name must not promise a swap the contract cannot deliver.
So there is no store module. The controller is coupled to postgres — its own queries use
distinct on and on conflict, which are not portable — and calling it store would advertise a
swap that would fail the first time somebody tried it.
The same applies to the broker, with a different outcome: amqp is a protocol and more than
one implementation speaks it, so amqp is a legitimate provision and lavinmq is one provider of
it. The foundation's broker and the lavinmq module collapse the same way.
What this costs, and it is the last specialty
Adopting the store and the broker as modules is issue 051, and it is the only part of this that has not been designed. The two hard parts:
- upgrading a store the controller is reading from — a rollout where the thing being replaced is the thing holding the record of the rollout
- upgrading a broker over the broker — the instruction arrives on what it replaces, so the machine must finish without being able to report progress
Both are operations with windows, and a machine rebooting inside one is an operator's problem during an operation rather than a reason not to do it.