Merge pull request 'Issue 339: the merge order against mesh-host #61, used-as-found as a behaviour change, the review's follow-ups' (#236) from issues/339-review-follow-ups into main

This commit was merged in pull request #236.
This commit is contained in:
2026-10-08 23:55:39 +00:00
2 changed files with 106 additions and 6 deletions
@@ -75,9 +75,9 @@ pushing.
that holds one. A `settings set` or `settings clear` line through `command` is refused. The layer is
unchanged after every refusal. At the terminal the same placement is taken, but the machine's own trees
(`/`, `/etc`, `/usr`, `/var/lib`, `/home`, the node-engine's state and the rest) are refused from
anywhere. A line break, carriage return or NUL in any setting is refused, keys included. PEM blocks
alone may hold lines, because a provider serves its authority's root as a setting. Each of these tests
fails on the commit before the fix.
anywhere. A line break, carriage return, any other line end or NUL in any setting is refused, keys
included. Only step-ca's `root` may hold lines, as certificates that decode and parse (since the review's
follow-up; at first any PEM block was taken). Each of these tests fails on the commit before the fix.
- **In the node-engine's tests, through `Apply`.** These are refused, each as a failed resource with its
reason:
- a directory, access or mount source that is one of the machine's roots or holds one;
@@ -80,10 +80,110 @@ that the mesh did not record making at that path, is **used as found**: its owne
changed, the outcome says so, and it is never removed. That is ADR 0182 applied beyond the home. The record
of every directory on a running machine already names its path, so nothing applied today changes.
### The two pull requests are independent
### The order the pull requests merge in
The controller's tests do not compose with the node-engine, and the engine's guard needs nothing from the
controller. Either can merge first. Each closes the hole on its own side.
*Corrected 2026-10-09, after review.* The first version of this section said either pull request could merge
first. That is true of the two pull requests for this issue: the controller's tests do not compose with
the node-engine, and the engine's guard needs nothing from the controller. It is not true of the node-engine
pull request for ADR 0266's agent account (`mesh-host` #61), which carries a guard of its own over the same
appliers and the same files. The two conflict, so the order is:
1. The node-engine's fix for this issue (`mesh-host` #63) merges first.
2. `mesh-host` #61 is then rebased onto it. It drops its own copy of the guard and adds its rules for below a
home (an archive, or a file written into, below the home of an account that never becomes root) to the
guard #63 placed, `internal/apply/placement_guard.go`.
The controller's fix (`mesh-controller` #168) merged on 2026-10-09. The controller's pull request for ADR 0266
(`mesh-controller` #164) is rebased onto it in the same way.
### A behaviour change: a directory found on a machine is used as found
Before this fix, the node-engine gave every declared directory the declared owner and mode, whether the mesh
had made it or not. Now a directory that was there before the mesh first applied it, at that path, keeps its
owner and mode, and is never removed. That is the safe default. It is also a new way for a module to fail
quietly: a module whose data directory was found with another owner cannot write it. Its container fails,
and nothing says why, except one line in the apply's outcome.
No directory on a running machine is affected today, because each already has a record of the mesh
applying it at its path. The first to be affected would be:
- a module newly assigned to a machine where its directory already exists;
- a placement moved onto a directory that already exists.
Until the node-engine's follow-up (below) lands, the only trace is that one outcome line.
### The review's follow-ups (2026-10-09)
- **`mesh-controller`:**
- The one setting that may hold lines is narrowed to step-ca's `root`, holding certificates that
`encoding/pem` decodes (label CERTIFICATE, no headers) and `x509.ParseCertificate` parses.
- Vertical tab, form feed, NEL and the Unicode line and paragraph separators are refused alongside line
breaks, carriage returns and NUL.
- The spool, `/opt`, the container runtimes' data (`/var/lib/docker`, `/var/lib/containers`,
`/var/lib/containerd`) and any `.ssh` are added to the paths no placement or access may name.
- **What a provider serves, and what a trusted file asks for, is the terminal's too.** A setting overrides any
key a provider serves (`Settle`), and what is served is what every consumer connects to and believes:
a database's or a registry's port, an object store's scheme, an identity provider's issuer and token path,
a model's name. Through the settings verb, any caller could point every consumer at a listener of its own
and collect what they present. The third review found this; the first fix had listed two modules by hand.
The controller now derives the keys a verb may not change from the module's manifest (`TerminalKeys`):
- `places` and `accesses`;
- every key under the module's `serves`, and every setting a served value asks for (`${setting:…}`);
- every setting a file asks for, **unless the file says `"trusted": false`**. An unmarked file counts as
trusted (fail closed, the fourth review): a file root or a consumer trusts — a logind drop-in, an env file
that says which uid a container runs as, a script run as root — must never be changed through a verb,
and saying nothing must not be the unsafe answer. `"trusted": false` is the opt-out, for a file nothing
but its own person reads. The controller takes the field out before the declaration.
`module check` lists and counts the unmarked files, so an author can opt out a harmless one. It never
refuses them, because counting them as trusted is already the safe reading; the refusal date the third
review's fix had is gone. On 2026-10-09 the catalogue had 9 unmarked files and the media catalogue 13.
Two are opted out in a catalogue change: the notifier's look and a person's folder names. The other 20
stay trusted.
**A rollback hazard, and its fix.** The node-engine parses a declaration strictly, so a `trusted` it did not
know would cost a node its whole declaration. The controller takes it out, but an older controller, or
one rolled back to, would pass it on. So the node-engine now accepts `trusted` on a file and does nothing
with it (in `mesh-host` #65), and the catalogue change that sets it waits until the controller with this
fix is live.
- **`mesh-host`, a directory used as found is said:**
- a WARNING line in every apply's log;
- its module's resource of kind `directory`, stated unhealthy on every look, with its owner, its mode and
what was declared. The controller raises that as the module's condition, naming the directory.
- `mesh-host hand-over <dir>`, run by a person at the machine, hands it to the mesh. The next apply gives
it the declared owner and mode. A hand-over names the resource it was for, so it lapses if another
declaration later points another resource at the same path.
**The gate reads it as a wait for a person**, the way it reads a wait for a new login
([ADR 0254](../../02-DECISIONS/0254-a-wait-for-a-persons-new-login-is-not-a-gates-failure-and-each-module-of-a-send-keeps-its-own-pass.md)).
- **Its own condition kind**, `directory-used-as-found`, raised when every unhealthy resource of the module
on that machine is a directory used as found. It is the operator's to resolve, a warning however long it
stays open, never urgent, and cleared on the first statement that no longer says it. If anything else of
the module is unhealthy beside it, the module's ordinary unhealthy condition is raised instead, with the
directory among its resources.
- **The gate never reads that condition as a fault of the build**, in both places it reads conditions:
the module's own (`judgeHealth`) and the machine's (`aboutTheMachine`). This is the same exemption the
relogin kind has, and it holds even when the condition was raised after the send.
- **From the health statement**, the node-engine keeps when it first found the directory (`found_at`)
across applies. A directory found before the send being judged is no fault of that build: the module's
gate passes, and its verdict carries the wait ("uses … as found and waits for a person to hand it over").
An urgent fix to that module still reaches every machine. A test holds this with the condition open and
raised after the send.
- **A directory found by that very send** was brought by it, so the gate holds the module and, at its
bound, puts it back.
- **Not done yet:** found before or after the send is judged by the node-engine's clock against the
controller's. A large clock skew between the two could misjudge it. Comparing by the declaration's
sequence instead is left for later.
- **`mesh-host`, the guard:** the same paths as the controller are added. The container runtime's data root
is asked of the running runtimes first (`docker info`, `podman info`), and read from their configuration
beside that: `data-root` in dockerd's `daemon.json` or a `--config-file`; `--data-root` (or the older `-g`
and `--graph`) on the unit's `ExecStart`, continued over lines, quoted, or through `Environment=` and
`EnvironmentFile=`, in the unit and its drop-ins; and `graphroot` in podman's `storage.conf`, as a basic or
a literal string. The asking and the extra forms are in a further pull request stacked on the first. `/var/lib/containerd` is refused as well. Two stores are not read. A containerd configured
elsewhere is not a runtime this mesh runs. Podman's rootless stores sit below each account's home, where
only that account's directories are placed anyway. An account's `.ssh` is refused as an access or a mount, and as a
directory anywhere but below its own account's home.
### Step 3: what the live layers hold