diff --git a/03-DESIGN/01-to-be/05-the-node-host.md b/03-DESIGN/01-to-be/05-the-node-host.md index f438af7..6bf4646 100644 --- a/03-DESIGN/01-to-be/05-the-node-host.md +++ b/03-DESIGN/01-to-be/05-the-node-host.md @@ -2,7 +2,7 @@ layer: to-be status: in-progress code: [mesh-host] -updated: 2026-08-27 +updated: 2026-08-30 decisions: - 02-DECISIONS/0019-how-this-repository-works.md - 02-DECISIONS/0004-a-node-and-how-it-joins.md @@ -243,3 +243,43 @@ Each decision above owes a test: a node whose local state is discarded so the mesh re-derives it, and does not decide it. - **What may expire.** An identity needing refresh to stay valid would make a laptop fail for being a laptop ([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)). + +## What was added to the vocabulary, and why each cost was worth paying + +*Written 2026-08-30. Every addition widens what a compromised control plane can express, so the +count is asserted by a test and a change to it is a decision rather than a convenience.* + +Six shapes raised the substrate. Two more exist because most of what a person installs is not a +service: + +| | why | +|---|---| +| **user** | a shell, a terminal, a chat client, a desktop are a package plus configuration **in somebody's home**. A mesh with no user owns `/etc` and nothing anybody looks at | +| **archive** | a theme is hundreds of files. Inlining them makes every declaration enormous and rewrites all of them when one changes | + +And `file` gained two fields: `bytes`, because a wallpaper is not a string, and `owner`, because a +file in a home belongs to somebody. + +**`user` also makes a login shell declared state.** `chsh` is a command, the link may not carry +one, and a shell that could only be set by hand is a shell the mesh cannot manage — which is most +of the reason to manage a machine. + +### The refusals that came with them + +- **A file says what is in it exactly once.** `content`, `bytes` and `sealed` are exclusive, so + *what is in this file* is answerable by looking rather than by knowing which field wins. +- **Groups are added, never pruned.** The tool that sets them replaces the set unless told + otherwise, which would silently remove every group that makes a login able to use the machine. + A machine's own groups are not the mesh's to know about. +- **An archive is pinned by digest, checked before a single file is written.** This is the one + place the host reaches out on its own — everywhere else it holds one outbound connection and + fetches nothing — so the only thing making those bytes safe to unpack is that they hash to what + was declared. +- **An entry naming a path outside the archive is refused, not sanitised.** Rewriting it to land + inside would put a file somewhere nobody asked for and report success. The first implementation + quietly relocated it, and a test caught that. +- **Symlinks and device nodes are refused rather than skipped**, or an archive needing one arrives + silently incomplete. + +**A partial host does archives and refuses users**: an archive needs a filesystem and a way to +fetch; a user needs a user database the host is allowed to write. \ No newline at end of file diff --git a/03-DESIGN/01-to-be/12-a-module-repository.md b/03-DESIGN/01-to-be/12-a-module-repository.md new file mode 100644 index 0000000..1802d05 --- /dev/null +++ b/03-DESIGN/01-to-be/12-a-module-repository.md @@ -0,0 +1,104 @@ +--- +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.