Commit Graph
5 Commits
Author SHA1 Message Date
jschoubben f06eea5fa3 declaration: an access is mounted, and the host owns nothing about it
The tenth shape (novox/hq ADR 0051). Shared, pre-existing data — a media
library, a download spool several modules use — is the operator's, not
the mesh's. A `directory` resource is the host's own: it creates it,
chowns it, sets its mode and removes it when empty. An access is the
opposite on every axis.

Add the `access` type to the vocabulary. Its applier confirms the path is
present and changes nothing: it does not create, chown, reconcile or set
a mode. Absent is refused clearly — the operator must provide it — rather
than created, because a bind mount whose source is missing is made as
root by the container runtime with the wrong ownership (04-ISSUES/026).
Undeclaring an access forgets the record and never touches the path,
which is the data loss ADR 0030 prevents, on a directory the mesh never
made.

Full hosts speak it (it gates a bind mount, which needs the container
runtime); the vocabulary guard test records the decision that made it the
tenth shape. Unit tests cover present, absent-refused, and
undeclared-left-alone.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 22:19:33 +02:00
jschoubben a7a2a48615 A full host implements the network shape, and something checks that it does
The shape was added everywhere except the one list that decides whether
a host can actually apply it, so every declaration carrying a network
was refused whole — correctly, and with the reason stated:

  resource "umami.net" is a network, and the arch host does not
  implement that shape

The mesh behaved as designed throughout. A host that applied the parts
it understood would leave a machine that looks configured and is not, so
it refused the lot and said why. What was missing was anybody reading
the host's log.

The vocabulary test did not catch it because it checks what the language
has, not what a host can do — those are different lists and only one of
them was updated. There is now a test that a host claiming to do
everything implements every shape the language has. It fails with the
message above when the registration is removed.

A network needs the same runtime a container does, so it belongs to a
full host and not to the portable floor.
2026-09-01 00:36:17 +02:00
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 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