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:
2026-08-30 03:36:57 +02:00
parent eab870fff9
commit a625e6c709
2 changed files with 145 additions and 1 deletions
+41 -1
View File
@@ -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.