The confirmation review of 2026-10-09 measured GET /user?sudo=… answer the
operator's own account: the query went to the forge unjudged, and the tools'
token is the site administrator's, so any caller could act as any user.
gitea_api now sends only allow-listed query keys and refuses sudo in any case,
and a review that approves or requests changes is the operator's.
Follow-ups of the review of 2026-10-09: the escape hatch sent the raw path it
had judged in normalised form; a pull request's base could be moved through
it; the merge tool would merge into a branch no rule guards; a rule named for
a branch could undercut the glob rule guarding it; and the delete tool took
any unguarded repository. It now takes only a repository of the tools' own
user with no protection and no commit in 30 days, named in full.
gitea_api took any endpoint with a token minted for the forge's
administrator with write:admin: an agent could drop a branch's protection
or its required checks, set the merge check's status, write to main or
merge its own pull request (novox/hq ADR 0266 §7, the review of
2026-10-09). gitea_api now reads anything but the administration and
writes only what an author writes; protection is only strengthened; a
merge needs every required status a success and merges only that head; a
guarded repository is not deleted; and the token is minted without
write:admin, a kept one re-minted at the next start.
mesh/merge-gate pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh, merge-check-typescript.sh): main's version judged it; the change's judges the pull r…
Removing a rule from the operator's own home changes what their sessions
obey as surely as placing one: an agent could remove the rule that
constrains it (hq issue 340).
mesh/merge-gate pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh, merge-check-typescript.sh): main's version judged it; the change's judges the pull r…
On the nodes where the agent runs as the operator, the home scope wrote the
operator's own ~/.claude, whose rules and skills load in every session, and
any node's tool could be addressed from another (hq issue 340). Take it
only for the serving node, where its home is an agent account's own; never
place a bus item in the operator's home. Ignore the built binary.
Instructions, output styles, skills, commands and subagents for every node
or some reach every session as the mesh's own word, and the tools cannot
tell an agent from the operator; the operator decided they are set at the
terminal until a surface of their own updates them (hq issue 340). They
are settings keys, each text a list of lines; the home scope stays free.
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
A change to a module's tests moves no build (hq ADR 0267), so the planner's modules leave it out; the
TypeScript part also reads the modules the changed files are in.
It had not been run before a merge: its expected list lacked gitea_commit_statuses since that tool came,
and now gitea_contains. The catalogue's TypeScript check, merged in from #151, runs it.
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
The catalogue's check ran only Go. A TypeScript module's change, the forge's among them, ran nothing of
its own before it merged, and a type error would first have shown in its walk. A second part, declared
with mesh-check-also and run in the TypeScript toolchain, type-checks each touched TypeScript module with
the compiler and flags a build uses, against the SDK and Node types the toolchain holds, and runs its
tests as its own npm test does. A module that needs a package the toolchain lacks, or has no tests, is
said by name, never passed. The media catalogue's twin is hq issue 311.
Merge order on a protected trunk was the whole proof; the review asked for ancestry. The gitea module
gains gitea_contains, a read-only compare of two commits (total_commits 0 is contained), and mesh-delivery
asks it once per pair, off the lock like its plan ask, keeping the answer on the delivery. A forge that
cannot say leaves the merge held, not covered. Merge order stays the cheap first filter.
A later delivery now carries a module only when its own check passed (so a merge held without a passing
check is closed only by one that passed), its walk is of its own merge commit on the same trunk, and its
steps say the module reached every machine running it, not one. The words say what is known: a later
merge that contains it was delivered.
mesh/merge-gate pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
A byte-order mark, a flow mapping or a merged key hid a key the line scan
looked for, and @path read any file into every session the item reached
(hq issue 340 review).
mesh/merge-gate pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
The tool runner is not told who called, so a hook, a managed setting, a
permission rule or a tool server registered through these tools could come
from an agent confined on another node and run as the operator there
(hq issue 340). They are set at the controller's terminal now; a skill,
subagent or command that runs something by itself is refused too, kept
hooks and settings are never rendered, and claude_code_registrations
lists what is registered and by which node.
A held delivery asked a person a question with no right answer: four hq merges that build nothing,
merges adding a module the controller sends nowhere, controller merges whose change a later walk of
the same trunk already delivered, and a group member held because its composed check had not yet
answered. Only stop could end them, which marks a delivered change as an error on its pull request.
- A merge whose plan moves no module (nothing, or only a new module sent nowhere) is delivered at once.
- A new final state, covered: every module a merge moves was sent by a finished walk of a later merge
into the same trunk, which contains it. Shown as success on the pull request; a group counts it as
delivered.
- A group member waits published while its composed check is asked and unanswered, and a member held
for it is lifted back to published once it passes for the heads that merged.
Read again each tick, so the deliveries already held end by themselves after the rollout.
Third review of #147. The run writes as root into /var/lib/zfs-scrub (0700, root's), apart from the
operator account's error counts in /var/lib/zfs-tools, and refuses a folder that is a link, open to
others or someone else's, and a file that is a link: a new file opened O_NOFOLLOW|O_EXCL, renamed over.
With mesh-controller #170 a file that asks for a setting counts as trusted
unless it says otherwise, and its settings are set at the controller's
terminal alone (hq issue 339). These two are a person's own notifier look and
folder names in their home, which nothing else trusts.
The second review of #147 found three blockers before the home server.
1. ${setting:} was not filled in a unit's name, so the distribution timers would have reached the
machine as "zfs-scrub-weekly@${setting:scrub-pool}.timer". mesh-controller #171 fills it and refuses
any value that is not a unit-name part. The module's own schedule needs no setting in a name now: the
node-engine's timer for the scheduled process zfs-mesh-scrub fires `zfs-tools scrub-run` every quarter
of an hour, and the run reads the pool, cadence, day and time from the configuration file.
2. The run does what upstream's zfs-scrub@.service does: if a scrub is running it waits for it with
zpool wait -t scrub, otherwise it starts one and waits. It never pauses or stops a scrub, and the unit
has no ExecStop. A resilver defers the scrub. The served slot is kept in /var/lib/zfs-tools.
3. Unassigning left the pool with no schedule. mesh-host #66 (hq ADR 0269, proposed) gives the
distribution timer back running and enabled. The README says not to unassign before it is merged.
Also: the run comes before the distribution timers in the declaration; the day and time are checked
against fixed sets and reach no unit file (an invalid value runs nothing); a due scrub not started an
hour after its time is a warning; the error counts outlive a restart in a small state file.
The safety review of the first version found it safe for the pool's data and asked for these fixes before
it goes on the home server:
- zfs_snapshot: no recursive. Every answer gives the dataset's free space and the pool's capacity, and
says that the snapshot holds every block deleted or overwritten until it is destroyed by hand.
- A command that runs out of time is ended with its whole process group. An act that timed out is
answered from the list, or as "outcome unknown", never as "NOT DONE".
- The watch runs as the operator account.
- The event follower reads ZFS's queue from its start, with a cursor per boot, so the pool's import and
any downtime reach the journal once. It writes "dropped N events", kills zpool before waiting for it,
and exits non-zero after five failed follows in a row.
- Past error counts are a warning, and counts that rise are unhealthy.
- The guard test catches .Run(, exec.Command(, exec.CommandContext( and process starts.
- The fixtures and the parser follow OpenZFS 2.4.4's own printers: human scan sizes under -p, no iostat
section lines, error scrubs, per-vdev rebuild resilvers, paused scrubs.
The operator also asked the module to own the scrub. A timer and a service of the module's own run
zpool scrub -w -- <pool> from the scrub-pool, scrub-cadence, scrub-day and scrub-time settings. In the
same declaration, the distribution's weekly and monthly timers for that pool are held stopped and
disabled, so exactly one schedule scrubs it. zfs_scrub shows the schedule and the next run, with any
resilver's progress. zfs_scrub_start, zfs_scrub_pause and zfs_scrub_stop act with why. The health check
warns of a late or failed scrub and of a running resilver, and is unhealthy for a resilver that finished
with errors.
On the build node, in UTC, the fixture's creation time was the fake clock's own instant, so the test
could not tell the list's time from the clock's. A day and a half earlier is apart from it everywhere.
The home server's pool holds the operator's media library, and no mesh tool could say whether it is
healthy, how full it is, when it was last scrubbed or what snapshots exist; the only way was a login.
This module reads all of it with fixed zfs and zpool commands (list, get, status, iostat, events,
history) and has one act, zfs_snapshot, which only adds a snapshot: refused for an unknown dataset, a
name outside [A-Za-z0-9._-], an existing snapshot or no why; run as sudo -n zfs snapshot -- ds@name;
answered from zfs list afterwards, with any other snapshot that came or went meanwhile; recorded as the
event snapshot.taken. No destroy, rollback, set or send exists, and a test holds every command to its
verb.
Health (ADR 0240): zfs_health on the watch process, unhealthy for a pool not ONLINE, device or data
errors, a pool past 90 % full or no ZFS at all; past 80 % and a scrub older than 35 days are warnings.
Beside it, the event follower (zpool events -f, as root) keeps ZFS's errors in its journal past a
reboot and is judged by its unit, the check the module does not run itself. No seat (ADR 0121), no
package: ZFS is the machine's.
execute runs any command as the operator account, which can become root
without a person. The operator withholds it on the control-node until a
call needs a person's approval (hq ADR 0268); the other machines keep it
for now. The setting execute (serve by default) is written into a
root-owned file the tools bundle reads at start, and anything but serve
withholds the verb, so the machine's runtime does not offer it.
mesh/merge-gate pass: builds postgres, sent nowhere; no bus step; 2 wait(s) for a person; every machine composes with the change as it did without (4 of 4 …
A reader given a privilege WITH GRANT OPTION that passed it on made the
plain revoke fail with dependent privileges exist; the pass still listed the
table as revoked and logged it every minute. The pass now retries such a
change with CASCADE, which reaches only grants that came from the reader,
reads the grants back, reports as revoked only what is gone, and says what
is still held and why when anything is.
A setting changed through mesh-controller.settings rewrote dunstrc, but the
running dunst kept the old font and width until the next login. Declare the
package's dunst.service in the account's manager, without a state, with
reload-on the configuration, so the node-engine runs its ExecReload
(dunstctl reload) and what is on screen stays. Add dunst_reload to do the
same on demand and say the font and width the files set.
mesh/merge-gate pass: builds postgres, sent nowhere; no bus step; 2 wait(s) for a person; every machine composes with the change as it did without (4 of 4 …
A superuser's REVOKE acts as the table owner, so a grant to the reader made
by a role holding GRANT OPTION survived it while the report said revoked.
Each grant is now revoked under SET ROLE to its grantor, the grants are read
back, and anything still held is reported as a failure. The role was
altered every minute with its password in the statement, which a server
logging DDL would record and which could collide with a first query; it is
now altered only when an attribute, a setting or the password has drifted,
one ensure at a time. SECURITY DEFINER functions and views the reader may
use reach data without a grant on it, so every pass lists them. The README
says the name filter is a backstop and that the read-only session default
can be turned off by a statement.
mesh/merge-gate pass: builds postgres, sent nowhere; no bus step; 2 wait(s) for a person; every machine composes with the change as it did without (4 of 4 …
mesh/merge-gate pass: builds postgres, sent nowhere; no bus step; 2 wait(s) for a person; every machine composes with the change as it did without (4 of 4 …
mesh_store_reader held pg_read_all_data, so every caller of mesh-store.query
and postgres_query could read every column the store holds: the signing key
nodes trust, sealed secrets, password hashes. The reader now holds no role;
each minute, on every database, its memberships are revoked and its grants
brought to what the consumer's contribution lists under readable, minus any
column or table whose name says it holds a secret. Before a statement runs,
the reader's own session is asked which secret columns it can read and the
statement is refused if any; a statement naming a known secret table is
refused before it is sent.
mesh/merge-gate pass: builds new: modules/mounts, modules/nfs-server, sent nowhere; no bus step; every machine composes with the change as it did without (…
mesh/merge-gate pass: builds new: modules/mounts, modules/nfs-server, sent nowhere; no bus step; every machine composes with the change as it did without (…
The review of #136 found a share could squash every client to root, be
judged at a symlink's name rather than its folder, sit inside or above a
backup repository deeper than one level, export an unmounted dataset's
empty folder, go to every address in the private network's range, and
stay exported after the module left. Each is now refused or closed, with
a test that reproduced it first:
- a folder owned by uid or gid 0 is refused;
- the path is resolved through symlinks and every check runs on the real
folder, which the export line names;
- a share inside a backup repository (any parent) or holding one within
four levels is refused, and a look cut short by its bound refuses;
- every line carries mp, or mp=<its filesystem's mount point>;
- a share goes only to the nodes this machine's grants setting names and
whose mounts module wants it on the bus, at the lower access, at the
roster's address inside the range; nothing without wants;
- the process is declared before its config, so on unassignment it finds
the config gone, removes its export file and lets the shares go;
- confirm accepts the seat's string "true".
mesh/merge-gate fail: builds new: modules/mounts, modules/nfs-server, sent nowhere; no bus step; a manifest the change touches fails the module check: modu…
A machine mounts what its assignment names: a node's share from the
provision nfs-share, whose path it reads from nfs-server's state on the
bus, and occasional sources (an SMB device, a disk) whose absence is a
state, not a fault. apply writes and arms the unit pairs as root; watch
tries each source and judges the health; the operator's fstab lines are
taken over only through adopt, on their word, once the replacement is
armed. SMB logins are an outside party's secret, given with secret accept.
mesh/merge-gate pass: builds nats, sent nowhere; a bus step; 1 wait(s) for a person; every machine composes with the change as it did without (4 of 4 compo…