Files
hq/03-DESIGN/01-to-be/17-raising-a-mesh.md
T
jochen 6c8943d934
mesh/merge-gate head 6c8943d93453 merged into trunk 963f0d0ecd9f — pass: the change touches no module of the mesh's graph
mesh/repo-check head 6c8943d93453 merged into trunk 963f0d0ecd9f — pass: its merge-check.sh passed
mesh/delivery delivered
Give ADR 0338 its home in to-be 17: the store genesis raises is the postgres module's server (issue 609)
2026-10-13 07:11:36 +02:00

19 KiB

layer, status, code, updated, decisions
layer status code updated decisions
to-be in-progress
mesh-host cmd/mesh-bootstrap
mesh-host internal/bootstrap
mesh-lab test/integration/whole-mesh-full.test.ts
2026-10-13
ADR 0100
ADR 0101
ADR 0103
ADR 0067
ADR 0069
ADR 0006
ADR 0005
ADR 0010
ADR 0036
ADR 0336
ADR 0338

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).

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 node-engine 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 that genesis builds rather than carries, ADR 0071 where it clones from, and ADR 0073 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). 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 node-engine 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). 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.

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.

Genesis on a machine in use

2026-09-22, ADR 0100. A control-node that is already running a predecessor mesh is raised adopted, said so by the operator. The operator stops the predecessor's control on it first — the daemons that write its configuration — and its services keep running. Every port the foundation binds is an input to genesis, checked free before anything is raised, refused with the name of what holds it; the ports given become the node's settings for the foundation's modules, so adopting the foundation as modules keeps them, and every reader of them — the filter, the base ruleset, the private network's endpoint, the addresses consumers are given — reads them there. On the control-node measured, the store's and the bus's usual ports were free and the registry's, the broker's management port and, as the private network's hub, its port were held. Genesis also checks the private network's range does not overlap a tunnel the predecessor runs. Genesis adopted does not load the base ruleset: the machine's own firewall already filters; the mesh opens its foundation's ports through it and keeps the store from outside with a table of its own that only refuses (08-connectivity). A converged genesis refuses a machine in use — a container running, or a port listening on an address other than loopback that is neither ssh's nor held by one of the operating system's own network daemons (ADR 0101) — and names every one it counted, so a forgotten flag cannot close a working machine. A machine raised adopted stays adopted when genesis is run again (ADR 0103): the installer reads the mode from what the machine records, refuses a run without the flag on an adopted machine, and refuses the flag on a converged one. How it is checked: the adoption bed raises genesis converged on a machine in use and asserts the refusal, then adopted with the registry's port held and asserts the refusal names its holder, then with another port given asserts the foundation comes up, stays on that port once adopted as modules, and the machine's service is still reachable.

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. 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 builder is assigned before the package registry exists, and is handed the forge genesis raised (ADR 0336). The builder's module, build-agent, requires the package registry (npm-package-registry) only optionally (ADR 0330), so assigning it on a mesh where nothing provides one is not refused: the requirement is listed unbound by plan and node, and binds with nobody's act on the composition after the gitea module is assigned. Genesis is not reordered for it. The forge raised at the packages step is a plain container, not the gitea module, and provides nothing, and the gitea module needs the store, the vault and a route, which come after the base.

So the packages step, once it has raised the forge and made its account mesh-builder, seals the builder on this machine one own-secret, the genesis document (genesis-package-registry): the forge's address, the port this machine gave the package registry, the npm path, and the account with its password. The manifest declares it as issued outside the mesh, so no other machine is ever given one. The sdk step publishes the SDK with that account and the base step resolves it with the same one. Nothing else in genesis needs the registry.

A real binding wins over the genesis document whenever both are present, and every build's log says which of the two it resolved from. A build that needs the registry (a package artifact, an image whose Dockerfile names .npmrc, a TypeScript artifact with an @novox/ dependency) on a builder that has neither fails before anything is built or published. Its outcome names the machine, says the requirement is unbound there, and names the gitea module as what provides it. It never builds without the credential and never resolves the mesh's scope from the public registry. Go artifacts, images that do not name .npmrc and upstream images need no registry. The gitea module's own server is an upstream image, so building the forge needs no forge.

The store genesis raises is the postgres module's server, so adopting it keeps it (ADR 0338, the store's twin of the bus's ADR 0333). The bundle raises the store under the name, on the image, in the data directory and with the superuser password file that the catalogue's postgres module declares, and every step of the bundle and of the installer reaches it by that name. At the store step the module declares the container the bundle raised, so the store is adopted where it runs and is not restarted. The module does not change, so no running mesh's store moves. How it is checked: a test reads the module's manifest and holds the bundle to it, the module's image is measured initialising a new store on a data directory made as the bundle makes it, and the bed that raises genesis from bare passes the store step and reaches the broker step.

The core modules are built. Each is named by a repository and a path (ADR 0069), 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 node-engine 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. A machine in use joins adopted: the token says so, the operator has stopped the predecessor's control on it first, and from then on it keeps what it has until each module is taken (ADR 0100).

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 builder's manifests from a checkout somebody put there. The controller's is no longer among them: it lives in the controller's own repository (ADR 0069), the build the installer runs reports it with the artifact resolved, and genesis registers that (issue 072). The builder's manifest belongs in the same repository and is still read from the catalogue; that is the last of this.

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 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. 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.

The genesis credential outlives the binding. Once the gitea module is assigned the builder resolves the registry from its binding, but the genesis document and the mesh-builder account are still there, so the mesh holds two credentials for one registry. Withdrawing them is not decided; the issue tracker holds it as issue 602.

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 node-engine 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 builder is assigned before the package registry exists The build-agent manifest's own test holds the requirement optional and the genesis document declared and issued outside the mesh; the controller's resolution test over the real catalogue assigns the builder with no provider, lists the requirement unbound, and binds it once gitea is offered. The builder's tests refuse a package, an .npmrc image and an @novox/ TypeScript build with neither binding nor genesis document, before anything is built, and build a Go artifact; they read the genesis document only without a binding. The installer's test seals the document at the packages step and gives the builder no serves setting and no npm-password. Not yet proved live: a genesis bed from bare passing the builder, packages, sdk and base steps.
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.