diff --git a/02-DECISIONS/0073-the-installer-carries-a-builder.md b/02-DECISIONS/0073-the-installer-carries-a-builder.md new file mode 100644 index 0000000..47fbc8a --- /dev/null +++ b/02-DECISIONS/0073-the-installer-carries-a-builder.md @@ -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)). diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 758393a..31bc462 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.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 diff --git a/03-DESIGN/01-to-be/17-raising-a-mesh.md b/03-DESIGN/01-to-be/17-raising-a-mesh.md index c3d3680..069f4de 100644 --- a/03-DESIGN/01-to-be/17-raising-a-mesh.md +++ b/03-DESIGN/01-to-be/17-raising-a-mesh.md @@ -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. | diff --git a/04-ISSUES/044-the-runtime-every-module-builds-on-cannot-be-built-by-the-mesh/00-report.md b/04-ISSUES/044-the-runtime-every-module-builds-on-cannot-be-built-by-the-mesh/00-report.md new file mode 100644 index 0000000..f5ca865 --- /dev/null +++ b/04-ISSUES/044-the-runtime-every-module-builds-on-cannot-be-built-by-the-mesh/00-report.md @@ -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. diff --git a/04-ISSUES/045-a-container-keeps-the-values-it-started-with/00-report.md b/04-ISSUES/045-a-container-keeps-the-values-it-started-with/00-report.md new file mode 100644 index 0000000..dbf387e --- /dev/null +++ b/04-ISSUES/045-a-container-keeps-the-values-it-started-with/00-report.md @@ -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://…@: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?