A module repository, and two more shapes the host speaks
Designed with no reference to what came before, which was asked for. The system this replaces has features — several deployable units inside one module — and they are deliberately absent. That closes something ADR 0001 has been carrying as an open prerequisite. It lists "named features with per-node opt-in" as required, or "every independently deployable unit becomes a module again and the count returns". The premise was right and the remedy already exists in another form: several modules, assignment per node, and a module with requirements and no files of its own. `networking` is exactly that. The count does not return because what made it return — a module is expensive, so put several things in one — is gone. A module here is a manifest and usually nothing else. The manifest in a repository names artifacts; the manifest the mesh holds names digests. Two documents, because a digest is not knowable until something is built and a repository carrying one is wrong the moment anybody edits anything. The builder runs on a node. Building needs a container runtime and a working tree, and what the control plane may send a machine is bounded by the declaration language. A control plane holding a container socket would be the one component that can do anything anywhere. And the host's vocabulary grew from six shapes to eight — user and archive — with the reasoning for each and for the refusals that came with them. The count is asserted by a test precisely because every addition widens what a compromised control plane can express.
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user