Commit Graph
4 Commits
Author SHA1 Message Date
jschoubben c57087d75d A user, bytes, and an archive — because most of what people install is
not a service

A shell, a terminal, a chat client, a desktop are a package plus
configuration in somebody's home. A mesh with no notion of a user can own
/etc and nothing anybody looks at, which is most of the reason to manage
a machine at all.

Three shapes, and the vocabulary test asserts the count precisely because
widening it widens what a compromised control plane can express:

  user     a login, its shell and its groups
  archive  a set of files, fetched by digest and unpacked
  (file)   gains `bytes` for what is not text, and `owner`

`user` also makes "zsh is my login shell" declared state. chsh is a
command, the link may not carry one, and a shell settable only by hand is
a shell the mesh cannot manage.

Groups are additive and never pruned — usermod without --append REPLACES
them, 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.

The archive is the one place this host reaches out on its own; everywhere
else it holds one outbound connection and fetches nothing. So it carries
the discipline the bootstrap already uses for images: pinned by digest,
and the digest checked before a single file is written.

Two decisions in the unpacker worth naming:

- 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. Found by the test: the first version quietly
  relocated it.
- symlinks and device nodes are refused rather than skipped, or an
  archive that needed 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 it is allowed
to write.
2026-08-30 03:22:38 +02:00
jschoubben ee2648188d Repoint ADR references after HQ consolidated 65 records to 23
96 comments across the two repos named records that no longer exist. Each now
points at the consolidated record that holds its reasoning -- ADR 0034 (a test
defends a decision) is 0017, the eight host records are 0005, the four lab
records are 0016.

Worth noting for next time: these are references from outside HQ, so renumbering
there is not free. It cost 38 files here.
2026-08-28 23:33:44 +02:00
jschoubben ebba16ce4a Per-system bundles, and Android's start problem closed by narrowing it
Two gaps.

The bundle's contents are per system even though its mechanism is not, so there
are now three: substrate-arch.lock, substrate-alpine.lock and
substrate-android.lock. All three are embedded and a host reads only the one it
was built for. Arch and Alpine remain placeholders -- the closure for a one-node
mesh is still research 011/012's open question, and inventing it here would be
worse than an honest placeholder.

Android's is not a placeholder. It says a partial host cannot raise a mesh and
why: every step of a bootstrap is a package, a container, or an action against
one, and those are exactly the shapes it refuses. So a partial host can JOIN a
mesh and cannot BE the first node. That belongs where somebody looking for the
android bundle will find it.

Also separated two things that were being conflated: "this system has no
bundle" and "this system was never built". Loading a bundle for debian is not
ErrEmpty, and the test asserts they differ.

0062 -- a host may be episodic. There is no way to keep a process running on an
ordinary Android device: init needs root, a foreground service can be killed
for memory. The answer is not to fight that. It is that being killed IS
disconnection, which ADR 0036 already made an ordinary situation -- and
everything the design does for a laptop that closes is what an episodic host
needs, at a shorter period. An authoritative local store, reconcile on start,
last-heard-from reported without an alarm.

So the gap closes by requiring less rather than building something. No keep-
alive, no Android daemon, no fighting the platform's process management.

Two consequences recorded rather than glossed. Last-heard-from is a much weaker
signal on an episodic host, so a healthy phone reads as a dead server unless
the reader knows which kind it is looking at. And a declaration may take a long
time to land, which makes 0058's separation of outstanding from failed
load-bearing rather than tidy.

Left open deliberately: how an episodic host is actually started, and -- first --
what an Android node is for. Building the start mechanism before deciding that
would be building it for nobody.
2026-08-28 01:24:06 +02:00
jschoubben 02f1fcc865 Three hosts: arch, alpine and android
ADR 0060, built. `make hosts` produces mesh-host-arch, mesh-host-alpine and
mesh-host-android, each pinned to its system at link time.

The claim that "almost all of it is shared" held up. All 36 existing apply
tests pass unchanged -- the only edit was naming which system they run against,
which was previously implicit. What moved into internal/system is two appliers'
worth of code and the probes that go with them.

Each system's differences are real and needed re-deriving rather than
translating:

apk reports absence by EMPTY OUTPUT and exits zero either way, where pacman
exits non-zero. Reading apk's exit code the way pacman's is read reports every
package as installed. That is the single most dangerous difference between the
two and it is invisible until it bites.

OpenRC has no LoadState, so "the service does not exist" is read from its prose
rather than a field. Same distinction, different evidence -- and this is exactly
what an interface spanning both would have had to drop, which is why 0060
rejected one.

OpenRC has no is-enabled either. Boot state comes from the runlevel listing:
"does it start at boot" becomes "does it appear in rc-update show default".

Android is a partial host and that is the point. It implements file, directory
and action -- the shapes needing only a filesystem and a way to run something --
and refuses the other three by name, before anything is applied. Its unreachable
appliers return ErrUnsupported rather than a zero value, so "unreachable" fails
loudly if it stops being true.

A host also confirms it is on the machine it was built for, once, at the start.
The alpine host on this Arch machine says "this machine is not Alpine" instead
of failing later inside a package manager that is not there. And a host built
without -X main.builtFor refuses everything, naming the hosts that exist.

Two test problems found by injecting faults. One injection did not compile, so
the check now reports that separately from a pass. The other passed with the
behaviour removed: the missing-service assertion matched "does not exist", which
the FALL-THROUGH error also contains because it echoes the raw output. It now
asserts the diagnosis, which only the correct branch produces.

Verified with the real binaries: android refuses a package naming what it does
support; alpine on Arch refuses the machine; arch applies and is idempotent; a
system-less build refuses everything.
2026-08-28 01:08:11 +02:00