--- layer: to-be status: designed code: - mesh-control internal/builder - mesh-control internal/catalogue/build.go updated: 2026-08-30 decisions: - 02-DECISIONS/0009-modules-and-the-graph.md - 02-DECISIONS/0010-delivery.md - 02-DECISIONS/0005-the-node-host.md --- # A module repository, and what builds it **Designed from what the mesh needs, not from what came before.** The system this replaces has a concept of *features* — several independently-deployable units inside one module — and it is deliberately absent here. ## Features are unnecessary, and that closes an open prerequisite [ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) lists *named features with per-node opt-in* as a prerequisite, on the grounds that without it "every independently deployable unit inside a context becomes a module again and the count returns." **The premise was right and the remedy already exists in another form.** What features were for is three things the mesh now does separately: | features did | what does it here | |---|---| | several deployable units in one thing | **several modules**, which is what they are | | turning one on for one node | **assignment**, which is per node already | | keeping related things together | **`requires`**, and a module with requirements and no files of its own | `networking` is exactly that last row: it ships nothing, requires a private network and name resolution, and assigning it brings both. So the module count does not return, because the thing that made it return — *a module is expensive, so put several things in one* — is gone. A module here is cheap: a manifest and, usually, nothing else. ## One file at the root `module.json`, and a convention somebody can look for beats a setting somebody has to find. It says what the module is, what it provides and requires, what it claims, what capabilities it needs, what it puts on a machine — and, if anything must be produced from the source, what to build. ## The manifest in the repository is not the manifest the mesh holds A resource names an artifact: ``` {"id": "dotfiles", "type": "archive", "artifact": "config", "path": "…"} ``` and the built manifest names the thing: ``` {"id": "dotfiles", "type": "archive", "source": "…/blobs/sha256:…", "digest": "sha256:…"} ``` **Two documents on purpose.** A digest is not knowable until something is built, so a repository carrying one is a repository whose file is wrong the moment anybody edits anything — and the mesh would be pinning a value nobody could have checked. The built manifest is derived, and the record of *which commit it was derived from* is what makes "is this current?" answerable without building it again. The word `artifact` never reaches a machine. The host's decoder is strict and would refuse it, at the worst possible moment. ## The builder runs on a node **Not in the control plane, and this is the same boundary as everywhere else.** Building needs a container runtime and a working tree; what the control plane may send a machine is bounded by the declaration language ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)), and *run this build* is not in it. The alternative — the control plane holding a container socket — would make it the one component that can do anything on any machine, which is the property the whole design is arranged to avoid. So the builder is a module a node runs, given work over the broker like anything else. Today it is a command a person runs on such a machine; the mesh records the result identically either way, which is what makes the change from one to the other uninteresting. ## Three properties that are decisions - **A fresh clone every time.** A build reusing a working tree can succeed because of something a previous build left behind, and that is a build nobody can reproduce. - **Archives are packed deterministically** — sorted, and carrying no timestamps, ownership or original paths. Two builds of one commit must produce one digest, or nothing downstream can tell *this changed* from *this was built again*, and every rebuild looks like a change to every machine holding it. - **Nothing is published until everything is built.** Half a module in the store, under a digest the mesh never records, is reachable, unreferenced, and indistinguishable from something in use. ## Where artifacts go **The registry the bootstrap already pulls from**, for both images and archives. An OCI registry is a content-addressed blob store that also understands images, and an archive is a content-addressed blob. An object store beside it is the right answer for objects that are *mutable*, need per-reader access, or are not build output. None of that describes a digest-pinned archive, and running a second service for one kind of immutable blob is two things to run, two to back up, and two ways for an artifact to be missing. **Overturnable without touching anything else**: a manifest carries a URL and a digest, and neither says what served it.