Two graphs, the builder's arrival, and two findings from building on a live mesh #37
@@ -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)).
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
+112
@@ -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?
|
||||
Reference in New Issue
Block a user