hq issue 356 (hq pull request named in the thread once open). On 2026-10-09 at 18:01 the operator typed the node-engine's hand-over on the control-node, as a condition told them to, and the engine on PATH answered "unexpected argument": /usr/bin/nox-mesh-host is the engine a person copied at installation, every delivered engine runs from the versions directory, and nothing ever renewed the file on PATH. And the hand-over itself was a terminal act at the machine with no channel.
Reviewers' checklist
Validate every caller value.AnswerHandOver (internal/link/handover.go) bounds the ask (8 KiB), refuses one that is not a hand-over, refuses nobody asking, a name over one line or 200 bytes or holding NUL, and no directory — before the handler sees anything. handOverFound (cmd/mesh-host/found.go) then judges the path: absolute; filepath.Clean(path) == path (so no .., no doubled or trailing separator); one the node's state records as used as found; on disk a directory and not a link; EvalSymlinks(path) == path (reached through no link). Nothing is recorded unless all pass.
Trust-changing acts terminal-only. The engine hears the ask on mesh.node.<node>.ask.hand-over, which only the controller may publish (mesh.node.> is the controller's grant; the node's grant for the subject and the right to answer are in mesh-controller's PR). The controller runs node hand-over only as its terminal (a node subcommand that is not a read is refused through every verb and through mesh-cli outside the terminal). The engine runs nothing it is sent; its one act is the record the next apply reads, as hand-over typed at the machine already did.
Every test fails with its check removed.TestTheFallbackBecomesACopyOfTheProvedEngine (bytes, mode 0755, running file untouched; already a copy → same inode, no temp file), TestAnEngineRunningFromTheFallbackRenewsNothing (SameFile), TestAMissingFallbackIsPlaced, TestALinkAtTheFallbackIsRefusedAndLeft (link refused, target untouched, link kept), TestAnUnreadableRunningEngineIsSaid, TestTheFallbackPathIsTheLaunchers (internal/upgrade); TestTheHandOverAskAndAnswerKeepTheirFieldNames, TestAHandOverAskIsJudgedBeforeAnythingIsRecorded (seven refusals, each counted as never reaching the handler) (internal/link); TestHandOverRecordsOnlyAFoundDirectory (the subcommand and the controller's ask are one judgement; who asked is recorded), TestAHandOverIsRefusedForEveryPathThatIsNotTheFoundDirectory (nine refusals, nothing recorded, the exact path taken), TestAFoundDirectoryIsStatedUnhealthy (the statement names nox node hand-over <node> <directory> on the control-node, never the binary, never the path) (cmd/mesh-host).
Fail closed. A refusal answers "nothing was handed over" and writes nothing; an engine with no handler refuses; a request with no reply inbox is dropped unrecorded; the fallback renewal refuses a link, a non-regular file and an unreadable running engine, and a failure to renew is said in the journal rather than hidden.
What changes
The engine on PATH is the running build.upgrade.RenewFallback (internal/upgrade/fallback.go): when a build is proved (the mesh took a report under it — the same evidence known-good is written on, proving.heard in cmd/mesh-host/witnessing.go), the engine copies itself over the launcher's fallback (MESH_HOST_BIN, default /usr/bin/nox-mesh-host, the same variable the launcher reads) when the bytes differ: a copy, renamed into place, mode 0755 — never a link (ADR 0012), never a write through one, nothing while the engine runs from the fallback itself. Chosen over an exec-shim: the launcher runs that file as the engine while nothing is delivered (a shim there would run itself), and which version runs is the launcher's policy. The launcher is unchanged.
The hand-over's channel.link.HandOvers / HearHandOvers (internal/link/hearing_nats.go): a request subscription on mesh.node.<node>.ask.hand-over, answered once on the reply it carried with HandOverAnswer{said|refused}; Queue.HandOver is the engine's judgement (handOverFrom), wired in run.go beside the report ask. Field names held on both sides. The engine's own hand-over <dir> stays, for a machine no mesh can reach, through the same handOverFound.
The words (ADR 0272, the operator's rule of 2026-10-09). The statement, the apply's log line and every comment name nox node hand-over <node> <directory> on the control-node; the path stays the resource's target (evidence), never in the reason the operator's channel says. The usage says the binary is a daemon, the operator's command is nox, and the subcommands are install-time and rescue, run as root at the machine; the README says the same and uses the binary's real name.
Checks.make check (fmt, vet, go test ./..., the launcher's, rollback's and roused tests, build) passed locally on this tree before the issue was renumbered 353→356 (comments and the README only since); go test of internal/upgrade, internal/link, cmd/mesh-host and go build ./... pass at e74ee82. Not merged; nothing pushed to a node.
Order. mesh-controller's PR first (it composes the node's grant for the subject and the right to answer; an engine delivered before it has its subscription refused and says so, and nox node hand-over then says nothing on the node answers), then this. mesh-catalog's and hq's PRs in any order. After both are live: nox node hand-over <control-node> <the messenger's data directory> on the control-node, then its next apply.
hq issue 356 (hq pull request named in the thread once open). On 2026-10-09 at 18:01 the operator typed the node-engine's `hand-over` on the control-node, as a condition told them to, and the engine on PATH answered "unexpected argument": `/usr/bin/nox-mesh-host` is the engine a person copied at installation, every delivered engine runs from the versions directory, and nothing ever renewed the file on PATH. And the hand-over itself was a terminal act at the machine with no channel.
**Reviewers' checklist**
- **Validate every caller value.** `AnswerHandOver` (`internal/link/handover.go`) bounds the ask (8 KiB), refuses one that is not a hand-over, refuses nobody asking, a name over one line or 200 bytes or holding NUL, and no directory — before the handler sees anything. `handOverFound` (`cmd/mesh-host/found.go`) then judges the path: absolute; `filepath.Clean(path) == path` (so no `..`, no doubled or trailing separator); one the node's state records as used as found; on disk a directory and not a link; `EvalSymlinks(path) == path` (reached through no link). Nothing is recorded unless all pass.
- **Trust-changing acts terminal-only.** The engine hears the ask on `mesh.node.<node>.ask.hand-over`, which only the controller may publish (`mesh.node.>` is the controller's grant; the node's grant for the subject and the right to answer are in mesh-controller's PR). The controller runs `node hand-over` only as its terminal (a `node` subcommand that is not a read is refused through every verb and through mesh-cli outside the terminal). The engine runs nothing it is sent; its one act is the record the next apply reads, as `hand-over` typed at the machine already did.
- **Every test fails with its check removed.** `TestTheFallbackBecomesACopyOfTheProvedEngine` (bytes, mode 0755, running file untouched; already a copy → same inode, no temp file), `TestAnEngineRunningFromTheFallbackRenewsNothing` (SameFile), `TestAMissingFallbackIsPlaced`, `TestALinkAtTheFallbackIsRefusedAndLeft` (link refused, target untouched, link kept), `TestAnUnreadableRunningEngineIsSaid`, `TestTheFallbackPathIsTheLaunchers` (`internal/upgrade`); `TestTheHandOverAskAndAnswerKeepTheirFieldNames`, `TestAHandOverAskIsJudgedBeforeAnythingIsRecorded` (seven refusals, each counted as never reaching the handler) (`internal/link`); `TestHandOverRecordsOnlyAFoundDirectory` (the subcommand and the controller's ask are one judgement; who asked is recorded), `TestAHandOverIsRefusedForEveryPathThatIsNotTheFoundDirectory` (nine refusals, nothing recorded, the exact path taken), `TestAFoundDirectoryIsStatedUnhealthy` (the statement names `nox node hand-over <node> <directory>` on the control-node, never the binary, never the path) (`cmd/mesh-host`).
- **Fail closed.** A refusal answers "nothing was handed over" and writes nothing; an engine with no handler refuses; a request with no reply inbox is dropped unrecorded; the fallback renewal refuses a link, a non-regular file and an unreadable running engine, and a failure to renew is said in the journal rather than hidden.
**What changes**
1. **The engine on PATH is the running build.** `upgrade.RenewFallback` (`internal/upgrade/fallback.go`): when a build is proved (the mesh took a report under it — the same evidence `known-good` is written on, `proving.heard` in `cmd/mesh-host/witnessing.go`), the engine copies itself over the launcher's fallback (`MESH_HOST_BIN`, default `/usr/bin/nox-mesh-host`, the same variable the launcher reads) when the bytes differ: a copy, renamed into place, mode 0755 — never a link (ADR 0012), never a write through one, nothing while the engine runs from the fallback itself. Chosen over an exec-shim: the launcher runs that file as the engine while nothing is delivered (a shim there would run itself), and which version runs is the launcher's policy. The launcher is unchanged.
2. **The hand-over's channel.** `link.HandOvers` / `HearHandOvers` (`internal/link/hearing_nats.go`): a request subscription on `mesh.node.<node>.ask.hand-over`, answered once on the reply it carried with `HandOverAnswer{said|refused}`; `Queue.HandOver` is the engine's judgement (`handOverFrom`), wired in `run.go` beside the report ask. Field names held on both sides. The engine's own `hand-over <dir>` stays, for a machine no mesh can reach, through the same `handOverFound`.
3. **The words (ADR 0272, the operator's rule of 2026-10-09).** The statement, the apply's log line and every comment name `nox node hand-over <node> <directory>` on the control-node; the path stays the resource's target (evidence), never in the reason the operator's channel says. The usage says the binary is a daemon, the operator's command is `nox`, and the subcommands are install-time and rescue, run as root at the machine; the README says the same and uses the binary's real name.
**Checks.** `make check` (fmt, vet, `go test ./...`, the launcher's, rollback's and roused tests, build) passed locally on this tree before the issue was renumbered 353→356 (comments and the README only since); `go test` of `internal/upgrade`, `internal/link`, `cmd/mesh-host` and `go build ./...` pass at e74ee82. Not merged; nothing pushed to a node.
**Order.** mesh-controller's PR first (it composes the node's grant for the subject and the right to answer; an engine delivered before it has its subscription refused and says so, and `nox node hand-over` then says nothing on the node answers), then this. mesh-catalog's and hq's PRs in any order. After both are live: `nox node hand-over <control-node> <the messenger's data directory>` on the control-node, then its next apply.
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
The file on PATH was the engine a person copied at installation, and every delivered engine ran from the versions directory: the operator typed hand-over and a build from before it answered "unexpected argument". A proved engine now copies itself over that file when the bytes differ (never a link). And the hand-over of a directory used as found (issue 339) gets its channel: the controller's terminal asks this node's engine on mesh.node.<node>.ask.hand-over; the engine judges every value — the ask's size and shape, who asked, the path as it states it, a directory and no link, through no link — and records it or refuses and records nothing. Every text the engine addresses to the operator names the nox line (ADR 0272); the engine's own subcommands are install-time and rescue, and its usage says so.
Deliverynovox/mesh-host@8fc1d72cd546 — failed since 2026-10-09T18:07:48Z
Delivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
tier 0: mesh-host
ace: receives mesh-host
g14: receives mesh-host
novox: receives mesh-host
shanks: receives mesh-host
Transitions
2026-10-09 18:04 (new) → proposed (announced): novox/mesh-host#74's head announced
2026-10-09 18:06 proposed → checked (checked): the verdict names this commit
2026-10-09 18:06 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
2026-10-09 18:07 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
2026-10-09 18:07 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path
2026-10-09 18:07 delivering → failed (failed): its walk failed: a gate on a first machine (what it carried put back), a build, a machine
The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.
<!-- mesh-delivery:view -->
**Delivery** `novox/mesh-host@8fc1d72cd546` — **failed** since 2026-10-09T18:07:48Z
**Delivery plan** — builds mesh-host → ace, g14, novox, shanks; no bus step
- tier 0: mesh-host
- ace: receives mesh-host
- g14: receives mesh-host
- novox: receives mesh-host
- shanks: receives mesh-host
**Transitions**
- 2026-10-09 18:04 (new) → proposed (announced): novox/mesh-host#74's head announced
- 2026-10-09 18:06 proposed → checked (checked): the verdict names this commit
- 2026-10-09 18:06 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
- 2026-10-09 18:07 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
- 2026-10-09 18:07 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path
- 2026-10-09 18:07 delivering → failed (failed): its walk failed: a gate on a first machine (what it carried put back), a build, a machine
The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
The record: hq #256 (issue 356 — renumbered from 353, which another branch took first; the code says 356). The controller's side is mesh-controller #187 (merge first: the node's grant), the catalogue's check mesh-catalog #165.
The record: hq #256 (issue 356 — renumbered from 353, which another branch took first; the code says 356). The controller's side is mesh-controller #187 (merge first: the node's grant), the catalogue's check mesh-catalog #165.
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)
mesh/repo-check: PASS — its merge-check.sh passed
Delivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
tier 0: mesh-host
ace: receives mesh-host
g14: receives mesh-host
novox: receives mesh-host
shanks: receives mesh-host
Run by the build seat on g14 as build-1791563763968800353 (builds --log build-1791563763968800353).
--- the gate: mesh-host
mesh-host: ok
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 0 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T16:31:14Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T16:31:14Z
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-host
--- its merge-check.sh (go toolchain)
go: downloading git.novox.be/novox/mesh-sdk/go v0.1.11-0.20261009143344-f047d0a4a970
ok github.com/novox/mesh-host/cmd/mesh-bootstrap 1.016s
ok github.com/novox/mesh-host/cmd/mesh-host 5.671s
ok github.com/novox/mesh-host/examples 1.017s
ok github.com/novox/mesh-host/internal/accounts 1.025s
ok github.com/novox/mesh-host/internal/apply 2.293s
ok github.com/novox/mesh-host/internal/bootstrap 1.320s
ok github.com/novox/mesh-host/internal/bundle 1.019s
ok github.com/novox/mesh-host/internal/declaration 1.055s
ok github.com/novox/mesh-host/internal/firewall 1.698s
ok github.com/novox/mesh-host/internal/identity 1.051s
ok github.com/novox/mesh-host/internal/image 1.020s
ok github.com/novox/mesh-host/internal/inventory 1.030s
ok github.com/novox/mesh-host/internal/link 10.336s
ok github.com/novox/mesh-host/internal/liveness 2.042s
ok github.com/novox/mesh-host/internal/meshcli 1.342s
ok github.com/novox/mesh-host/internal/network 1.037s
ok github.com/novox/mesh-host/internal/outward 1.029s
ok github.com/novox/mesh-host/internal/profile 1.536s
ok github.com/novox/mesh-host/internal/reachable 1.022s
ok github.com/novox/mesh-host/internal/store 1.084s
ok github.com/novox/mesh-host/internal/system 1.017s
ok github.com/novox/mesh-host/internal/tunnel 1.018s
ok github.com/novox/mesh-host/internal/units 1.011s
ok github.com/novox/mesh-host/internal/upgrade 1.040s
ok github.com/novox/mesh-host/internal/witness 1.475s
ok github.com/novox/mesh-host/packaging 1.011s
? github.com/novox/mesh-host/rootsearch [no test files]
ok github.com/novox/mesh-host/validate 1.011s
**Merge check** at `e74ee82a`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds mesh-host → ace, g14, novox, shanks; no bus step
- tier 0: mesh-host
- ace: receives mesh-host
- g14: receives mesh-host
- novox: receives mesh-host
- shanks: receives mesh-host
Run by the build seat on g14 as `build-1791563763968800353` (`builds --log build-1791563763968800353`).
```
--- the gate: mesh-host
mesh-host: ok
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 0 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T16:31:14Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T16:31:14Z
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-host
--- its merge-check.sh (go toolchain)
go: downloading git.novox.be/novox/mesh-sdk/go v0.1.11-0.20261009143344-f047d0a4a970
ok github.com/novox/mesh-host/cmd/mesh-bootstrap 1.016s
ok github.com/novox/mesh-host/cmd/mesh-host 5.671s
ok github.com/novox/mesh-host/examples 1.017s
ok github.com/novox/mesh-host/internal/accounts 1.025s
ok github.com/novox/mesh-host/internal/apply 2.293s
ok github.com/novox/mesh-host/internal/bootstrap 1.320s
ok github.com/novox/mesh-host/internal/bundle 1.019s
ok github.com/novox/mesh-host/internal/declaration 1.055s
ok github.com/novox/mesh-host/internal/firewall 1.698s
ok github.com/novox/mesh-host/internal/identity 1.051s
ok github.com/novox/mesh-host/internal/image 1.020s
ok github.com/novox/mesh-host/internal/inventory 1.030s
ok github.com/novox/mesh-host/internal/link 10.336s
ok github.com/novox/mesh-host/internal/liveness 2.042s
ok github.com/novox/mesh-host/internal/meshcli 1.342s
ok github.com/novox/mesh-host/internal/network 1.037s
ok github.com/novox/mesh-host/internal/outward 1.029s
ok github.com/novox/mesh-host/internal/profile 1.536s
ok github.com/novox/mesh-host/internal/reachable 1.022s
ok github.com/novox/mesh-host/internal/store 1.084s
ok github.com/novox/mesh-host/internal/system 1.017s
ok github.com/novox/mesh-host/internal/tunnel 1.018s
ok github.com/novox/mesh-host/internal/units 1.011s
ok github.com/novox/mesh-host/internal/upgrade 1.040s
ok github.com/novox/mesh-host/internal/witness 1.475s
ok github.com/novox/mesh-host/packaging 1.011s
? github.com/novox/mesh-host/rootsearch [no test files]
ok github.com/novox/mesh-host/validate 1.011s
```
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
Two holes the review proved. The ask's subject proved nothing — a responder can reach it through a reply — so the engine now verifies the mesh's signature over a fixed context with the key it verifies declarations with, before reading anything, and refuses an ask for another node, expired, good too long, without a nonce, or replayed. And the apply followed a link the hand-over had refused: a component swapped for a link after the record carried root's chmod and chown to its target. A handed-over directory must now still resolve to itself, and is changed only through one descriptor opened from / with no link followed (fchmod, fchown). Tests for the reply guard, the run loop's hook, the fallback renewal on proof and a non-regular fallback; 27 checks each removed in turn, each failing a test.
Review addressed — head fc6c5a9 (main has not moved since b0d03be, so the branch is the merge result).
Forgeable ask (blocking).AnswerHandOver(check, handle, body) now verifies SignedHandOver with m.Signer (the key handleBody verifies declarations with, carried into natsLink.signer) over HandOverContext + the ask's bytes before reading anything out of the ask; then refuses an ask for another node, expired, with no expiry, good for longer than 1m + 1m skew, without a nonce of ≥16 bytes, or whose nonce was taken (process-wide HandOverNonces, each kept until its ask expires — a reconnect is no replay; a restart can replay only within the minute, and recording the same hand-over again is idempotent). An engine with no key believes nothing. HandOverReply is the subscription's whole body: no reply subject → nothing judged, recorded or said. Apply follows a link (blocking). A handed-over directory now goes through applyHandedOver: EvalSymlinks(path) == path at apply time, then giveHandedOver — one descriptor from openUnderAs("/", path, kindDir) (every component openat O_NOFOLLOW|O_DIRECTORY from /, judged by fstat), Fchmod/Fchown on it. Nothing is changed by path. TestAHandedOverDirectoryIsChangedThroughNoLink swaps the parent, the grandparent and the directory itself for links to a victim tree after the hand-over: refused, the victim keeps mode 0700, and the error names which check refused. TestGiveHandedOverFollowsNoLink holds the descriptor walk alone. Test gaps.TestAHandOverIsAnsweredOnlyWhenItCarriesAReply (reply guard), TestALinkThatHearsHandOversIsGivenTheQueues (run.go hook, through serve), TestNatsAHandOverIsAskedOfTheNode (real server: answered on its own subject, another node's unheard, a publish with no reply handled by nobody — run locally against a throwaway nats-server, skipped without MESH_TEST_NATS as the other Nats tests), TestOnlyAnAccountUnderThisBuildProvesIt now asserts renewFallback ran (fallback = this test binary's bytes, said) and that nothing before the proof renewed it — and points MESH_HOST_BIN at a temp file, so a test run as root never writes /usr/bin. TestAFallbackThatIsNotARegularFileIsRefused (a directory at the fallback; a directory as the running engine). The redundant same-file case in RenewFallback is gone (same bytes covers it). Mutations: 27 checks each removed in turn, 27 killed (signature, no key, context, node, expired, too long, nonce shape, replay, nonce prune, by empty/line, no path, size, reply guard, run.go hook, the NATS respond, apply re-check, apply descriptor, handed-over branch, fallback not-regular/self not-regular/link/same-bytes, renewFallback called, path clean/link/through-link). Checks:make check and merge-check.sh exit 0 at fc6c5a9.
Review addressed — head fc6c5a9 (main has not moved since b0d03be, so the branch is the merge result).
**Forgeable ask (blocking).** `AnswerHandOver(check, handle, body)` now verifies `SignedHandOver` with `m.Signer` (the key `handleBody` verifies declarations with, carried into `natsLink.signer`) over `HandOverContext` + the ask's bytes **before reading anything out of the ask**; then refuses an ask for another node, expired, with no expiry, good for longer than 1m + 1m skew, without a nonce of ≥16 bytes, or whose nonce was taken (process-wide `HandOverNonces`, each kept until its ask expires — a reconnect is no replay; a restart can replay only within the minute, and recording the same hand-over again is idempotent). An engine with no key believes nothing. `HandOverReply` is the subscription's whole body: no reply subject → nothing judged, recorded or said.
**Apply follows a link (blocking).** A handed-over directory now goes through `applyHandedOver`: `EvalSymlinks(path) == path` at apply time, then `giveHandedOver` — one descriptor from `openUnderAs("/", path, kindDir)` (every component `openat` O_NOFOLLOW|O_DIRECTORY from /, judged by fstat), `Fchmod`/`Fchown` on it. Nothing is changed by path. `TestAHandedOverDirectoryIsChangedThroughNoLink` swaps the parent, the grandparent and the directory itself for links to a victim tree after the hand-over: refused, the victim keeps mode 0700, and the error names which check refused. `TestGiveHandedOverFollowsNoLink` holds the descriptor walk alone.
**Test gaps.** `TestAHandOverIsAnsweredOnlyWhenItCarriesAReply` (reply guard), `TestALinkThatHearsHandOversIsGivenTheQueues` (run.go hook, through `serve`), `TestNatsAHandOverIsAskedOfTheNode` (real server: answered on its own subject, another node's unheard, a publish with no reply handled by nobody — run locally against a throwaway nats-server, skipped without MESH_TEST_NATS as the other Nats tests), `TestOnlyAnAccountUnderThisBuildProvesIt` now asserts `renewFallback` ran (fallback = this test binary's bytes, said) and that nothing before the proof renewed it — and points `MESH_HOST_BIN` at a temp file, so a test run as root never writes `/usr/bin`. `TestAFallbackThatIsNotARegularFileIsRefused` (a directory at the fallback; a directory as the running engine). The redundant same-file case in `RenewFallback` is gone (same bytes covers it).
**Mutations:** 27 checks each removed in turn, 27 killed (signature, no key, context, node, expired, too long, nonce shape, replay, nonce prune, by empty/line, no path, size, reply guard, run.go hook, the NATS respond, apply re-check, apply descriptor, handed-over branch, fallback not-regular/self not-regular/link/same-bytes, renewFallback called, path clean/link/through-link).
**Checks:** `make check` and `merge-check.sh` exit 0 at fc6c5a9.
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)
mesh/repo-check: PASS — its merge-check.sh passed
Delivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
tier 0: mesh-host
ace: receives mesh-host
g14: receives mesh-host
novox: receives mesh-host
shanks: receives mesh-host
Run by the build seat on ace as build-1791567965774413759 (builds --log build-1791567965774413759).
--- the gate: mesh-host
mesh-host: ok
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 0 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T17:45:02Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T17:45:02Z
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-host
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-host/cmd/mesh-bootstrap 1.013s
ok github.com/novox/mesh-host/cmd/mesh-host 5.471s
ok github.com/novox/mesh-host/examples 1.018s
ok github.com/novox/mesh-host/internal/accounts 1.023s
ok github.com/novox/mesh-host/internal/apply 4.555s
ok github.com/novox/mesh-host/internal/bootstrap 1.509s
ok github.com/novox/mesh-host/internal/bundle 1.018s
ok github.com/novox/mesh-host/internal/declaration 1.045s
ok github.com/novox/mesh-host/internal/firewall 1.663s
ok github.com/novox/mesh-host/internal/identity 1.083s
ok github.com/novox/mesh-host/internal/image 1.019s
ok github.com/novox/mesh-host/internal/inventory 1.020s
ok github.com/novox/mesh-host/internal/link 10.461s
ok github.com/novox/mesh-host/internal/liveness 2.098s
ok github.com/novox/mesh-host/internal/meshcli 1.378s
ok github.com/novox/mesh-host/internal/network 1.077s
ok github.com/novox/mesh-host/internal/outward 1.064s
ok github.com/novox/mesh-host/internal/profile 1.530s
ok github.com/novox/mesh-host/internal/reachable 1.063s
ok github.com/novox/mesh-host/internal/store 1.233s
ok github.com/novox/mesh-host/internal/system 1.066s
ok github.com/novox/mesh-host/internal/tunnel 1.070s
ok github.com/novox/mesh-host/internal/units 1.059s
ok github.com/novox/mesh-host/internal/upgrade 1.181s
ok github.com/novox/mesh-host/internal/witness 4.627s
ok github.com/novox/mesh-host/packaging 1.060s
? github.com/novox/mesh-host/rootsearch [no test files]
ok github.com/novox/mesh-host/validate 1.017s
**Merge check** at `fc6c5a92`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds mesh-host → ace, g14, novox, shanks; no bus step
- tier 0: mesh-host
- ace: receives mesh-host
- g14: receives mesh-host
- novox: receives mesh-host
- shanks: receives mesh-host
Run by the build seat on ace as `build-1791567965774413759` (`builds --log build-1791567965774413759`).
```
--- the gate: mesh-host
mesh-host: ok
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 0 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T17:45:02Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T17:45:02Z
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-host
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-host/cmd/mesh-bootstrap 1.013s
ok github.com/novox/mesh-host/cmd/mesh-host 5.471s
ok github.com/novox/mesh-host/examples 1.018s
ok github.com/novox/mesh-host/internal/accounts 1.023s
ok github.com/novox/mesh-host/internal/apply 4.555s
ok github.com/novox/mesh-host/internal/bootstrap 1.509s
ok github.com/novox/mesh-host/internal/bundle 1.018s
ok github.com/novox/mesh-host/internal/declaration 1.045s
ok github.com/novox/mesh-host/internal/firewall 1.663s
ok github.com/novox/mesh-host/internal/identity 1.083s
ok github.com/novox/mesh-host/internal/image 1.019s
ok github.com/novox/mesh-host/internal/inventory 1.020s
ok github.com/novox/mesh-host/internal/link 10.461s
ok github.com/novox/mesh-host/internal/liveness 2.098s
ok github.com/novox/mesh-host/internal/meshcli 1.378s
ok github.com/novox/mesh-host/internal/network 1.077s
ok github.com/novox/mesh-host/internal/outward 1.064s
ok github.com/novox/mesh-host/internal/profile 1.530s
ok github.com/novox/mesh-host/internal/reachable 1.063s
ok github.com/novox/mesh-host/internal/store 1.233s
ok github.com/novox/mesh-host/internal/system 1.066s
ok github.com/novox/mesh-host/internal/tunnel 1.070s
ok github.com/novox/mesh-host/internal/units 1.059s
ok github.com/novox/mesh-host/internal/upgrade 1.181s
ok github.com/novox/mesh-host/internal/witness 4.627s
ok github.com/novox/mesh-host/packaging 1.060s
? github.com/novox/mesh-host/rootsearch [no test files]
ok github.com/novox/mesh-host/validate 1.017s
```
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
After the first apply the record is no longer used as found, so the directory read as the mesh's own and later applies set its mode and owner by path: a parent swapped for a link after the hand-over took carried root's chmod and chown to its target. The hand-over is now checked before that shortcut, so a handed-over directory always goes through applyHandedOver. The test swaps the parent and the grandparent after an apply already gave the directory its mode; both cases fail with the old order.
The hand-over is now checked before the ours shortcut in applyDirectory: if existed && HandedOver()[handOverKey(r.ID, r.Path)] { return applyHandedOver(...) }. A handed-over directory therefore goes through the re-check and the no-link descriptor on every apply, not only the first. Before this, after the first apply the record was no longer as found, so the directory read as the mesh's own and later applies used chmodDirPath and own by path.
TestAHandedOverDirectoryIsChangedThroughNoLink gains two cases: the parent, and the grandparent, swapped for a link after an apply already gave the directory its mode (the record is checked to be no longer as found first). With the old branch order both new cases fail ("a handed-over directory reached through a link was applied"); with the fix both pass.
make check and merge-check.sh exit 0 at 9a807de. Main has not moved since b0d03be, so the branch is the merge result.
The same by-path gap for ordinary mesh directories outside a home is older than this issue and is not fixed here. It is recorded as hq issue 358 (hq #258).
Confirmation review addressed. Head **9a807de**.
The hand-over is now checked before the `ours` shortcut in `applyDirectory`: `if existed && HandedOver()[handOverKey(r.ID, r.Path)] { return applyHandedOver(...) }`. A handed-over directory therefore goes through the re-check and the no-link descriptor on every apply, not only the first. Before this, after the first apply the record was no longer as found, so the directory read as the mesh's own and later applies used `chmodDirPath` and `own` by path.
`TestAHandedOverDirectoryIsChangedThroughNoLink` gains two cases: the parent, and the grandparent, swapped for a link *after an apply already gave the directory its mode* (the record is checked to be no longer as found first). With the old branch order both new cases fail ("a handed-over directory reached through a link was applied"); with the fix both pass.
`make check` and `merge-check.sh` exit 0 at 9a807de. Main has not moved since b0d03be, so the branch is the merge result.
The same by-path gap for ordinary mesh directories outside a home is older than this issue and is not fixed here. It is recorded as hq issue 358 (hq #258).
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)
mesh/repo-check: FAIL — its merge-check.sh failed: --- FAIL: TestTheSetuidSearchRunsInTheBackgroundAndIsKeptForItsInterval (0.00s)
Delivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
tier 0: mesh-host
ace: receives mesh-host
g14: receives mesh-host
novox: receives mesh-host
shanks: receives mesh-host
Run by the build seat on shanks as build-1791568726990600635 (builds --log build-1791568726990600635).
--- the gate: mesh-host
mesh-host: ok
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 0 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T17:55:02Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T17:55:02Z
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-host
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-host/cmd/mesh-bootstrap 1.016s
ok github.com/novox/mesh-host/cmd/mesh-host 5.504s
ok github.com/novox/mesh-host/examples 1.020s
--- FAIL: TestTheSetuidSearchRunsInTheBackgroundAndIsKeptForItsInterval (0.00s)
ways_test.go:158: searched 1 times, want 2: once at first, once when the result was older than its interval
FAIL
FAIL github.com/novox/mesh-host/internal/accounts 0.025s
ok github.com/novox/mesh-host/internal/apply 4.676s
ok github.com/novox/mesh-host/internal/bootstrap 1.602s
ok github.com/novox/mesh-host/internal/bundle 1.030s
ok github.com/novox/mesh-host/internal/declaration 1.066s
ok github.com/novox/mesh-host/internal/firewall 1.840s
ok github.com/novox/mesh-host/internal/identity 1.102s
ok github.com/novox/mesh-host/internal/image 1.041s
ok github.com/novox/mesh-host/internal/inventory 1.043s
ok github.com/novox/mesh-host/internal/link 10.471s
ok github.com/novox/mesh-host/internal/liveness 2.076s
ok github.com/novox/mesh-host/internal/meshcli 1.360s
ok github.com/novox/mesh-host/internal/network 1.062s
ok github.com/novox/mesh-host/internal/outward 1.048s
ok github.com/novox/mesh-host/internal/profile 1.520s
ok github.com/novox/mesh-host/internal/reachable 1.033s
ok github.com/novox/mesh-host/internal/store 1.212s
ok github.com/novox/mesh-host/internal/system 1.036s
ok github.com/novox/mesh-host/internal/tunnel 1.035s
ok github.com/novox/mesh-host/internal/units 1.030s
ok github.com/novox/mesh-host/internal/upgrade 1.160s
ok github.com/novox/mesh-host/internal/witness 4.771s
ok github.com/novox/mesh-host/packaging 1.032s
? github.com/novox/mesh-host/rootsearch [no test files]
ok github.com/novox/mesh-host/validate 1.028s
FAIL
**Merge check** at `9a807de6`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)
- `mesh/repo-check`: **FAIL** — its merge-check.sh failed: --- FAIL: TestTheSetuidSearchRunsInTheBackgroundAndIsKeptForItsInterval (0.00s)
**Delivery plan** — builds mesh-host → ace, g14, novox, shanks; no bus step
- tier 0: mesh-host
- ace: receives mesh-host
- g14: receives mesh-host
- novox: receives mesh-host
- shanks: receives mesh-host
Run by the build seat on shanks as `build-1791568726990600635` (`builds --log build-1791568726990600635`).
```
--- the gate: mesh-host
mesh-host: ok
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 0 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T17:55:02Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T17:55:02Z
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-host
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-host/cmd/mesh-bootstrap 1.016s
ok github.com/novox/mesh-host/cmd/mesh-host 5.504s
ok github.com/novox/mesh-host/examples 1.020s
--- FAIL: TestTheSetuidSearchRunsInTheBackgroundAndIsKeptForItsInterval (0.00s)
ways_test.go:158: searched 1 times, want 2: once at first, once when the result was older than its interval
FAIL
FAIL github.com/novox/mesh-host/internal/accounts 0.025s
ok github.com/novox/mesh-host/internal/apply 4.676s
ok github.com/novox/mesh-host/internal/bootstrap 1.602s
ok github.com/novox/mesh-host/internal/bundle 1.030s
ok github.com/novox/mesh-host/internal/declaration 1.066s
ok github.com/novox/mesh-host/internal/firewall 1.840s
ok github.com/novox/mesh-host/internal/identity 1.102s
ok github.com/novox/mesh-host/internal/image 1.041s
ok github.com/novox/mesh-host/internal/inventory 1.043s
ok github.com/novox/mesh-host/internal/link 10.471s
ok github.com/novox/mesh-host/internal/liveness 2.076s
ok github.com/novox/mesh-host/internal/meshcli 1.360s
ok github.com/novox/mesh-host/internal/network 1.062s
ok github.com/novox/mesh-host/internal/outward 1.048s
ok github.com/novox/mesh-host/internal/profile 1.520s
ok github.com/novox/mesh-host/internal/reachable 1.033s
ok github.com/novox/mesh-host/internal/store 1.212s
ok github.com/novox/mesh-host/internal/system 1.036s
ok github.com/novox/mesh-host/internal/tunnel 1.035s
ok github.com/novox/mesh-host/internal/units 1.030s
ok github.com/novox/mesh-host/internal/upgrade 1.160s
ok github.com/novox/mesh-host/internal/witness 4.771s
ok github.com/novox/mesh-host/packaging 1.032s
? github.com/novox/mesh-host/rootsearch [no test files]
ok github.com/novox/mesh-host/validate 1.028s
FAIL
```
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
SetuidCache compared the caller's now with a time read from the wall clock when the search finished. When the caller's clock ran behind the wall clock, a search was never due again: TestTheSetuidSearchRunsInTheBackgroundAndIsKeptForItsInterval, whose clock is a fixed date, has failed on main and every branch since 2026-10-09 18:00 UTC. A search's time is now the now it was started at; a result reads at most one search older than it is, which errs towards searching again.
9a807de was rejected by mesh/repo-check, and not because of this change. The failing test is internal/accountsTestTheSetuidSearchRunsInTheBackgroundAndIsKeptForItsInterval, which this branch never touched. The same test fails on main (b0d03be) too.
Cause:SetuidCache.Look compares the caller's now with c.at, but c.at was read from the wall clock (time.Now()) when the search finished. The test's clock is a fixed date (2026-10-08 19:00 UTC). Its look at +24h is due only while the wall clock is earlier than 18:00 UTC on 2026-10-09. From then on the test fails on every branch; it passed at 17:48 and failed at 18:00.
Effect on a machine: it is the same defect. A caller whose clock runs behind the wall clock never searches again.
Fix: a separate commit, head 8fc1d72. A search's time (at, failedAt) is now the now it was started at, so there is one clock, the caller's. A result reads at most one search older than it is, which errs towards searching again. The test fails without the fix and passes with it (5 runs).
merge-check.sh and make check exit 0 at 8fc1d72. Main has not moved.
**9a807de was rejected by mesh/repo-check**, and not because of this change. The failing test is `internal/accounts` `TestTheSetuidSearchRunsInTheBackgroundAndIsKeptForItsInterval`, which this branch never touched. The same test fails on main (b0d03be) too.
**Cause:** `SetuidCache.Look` compares the caller's `now` with `c.at`, but `c.at` was read from the wall clock (`time.Now()`) when the search finished. The test's clock is a fixed date (2026-10-08 19:00 UTC). Its look at +24h is due only while the wall clock is earlier than 18:00 UTC on 2026-10-09. From then on the test fails on every branch; it passed at 17:48 and failed at 18:00.
**Effect on a machine:** it is the same defect. A caller whose clock runs behind the wall clock never searches again.
**Fix:** a separate commit, head **8fc1d72**. A search's time (`at`, `failedAt`) is now the `now` it was started at, so there is one clock, the caller's. A result reads at most one search older than it is, which errs towards searching again. The test fails without the fix and passes with it (5 runs).
`merge-check.sh` and `make check` exit 0 at 8fc1d72. Main has not moved.
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)
mesh/repo-check: PASS — its merge-check.sh passed
Delivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
tier 0: mesh-host
ace: receives mesh-host
g14: receives mesh-host
novox: receives mesh-host
shanks: receives mesh-host
Run by the build seat on g14 as build-1791569060643270655 (builds --log build-1791569060643270655).
--- the gate: mesh-host
mesh-host: ok
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 0 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T17:59:32Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T17:59:32Z
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-host
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-host/cmd/mesh-bootstrap 1.012s
ok github.com/novox/mesh-host/cmd/mesh-host 5.633s
ok github.com/novox/mesh-host/examples 1.016s
ok github.com/novox/mesh-host/internal/accounts 1.020s
ok github.com/novox/mesh-host/internal/apply 2.457s
ok github.com/novox/mesh-host/internal/bootstrap 1.403s
ok github.com/novox/mesh-host/internal/bundle 1.031s
ok github.com/novox/mesh-host/internal/declaration 1.061s
ok github.com/novox/mesh-host/internal/firewall 1.789s
ok github.com/novox/mesh-host/internal/identity 1.065s
ok github.com/novox/mesh-host/internal/image 1.027s
ok github.com/novox/mesh-host/internal/inventory 1.032s
ok github.com/novox/mesh-host/internal/link 10.495s
ok github.com/novox/mesh-host/internal/liveness 2.064s
ok github.com/novox/mesh-host/internal/meshcli 1.352s
ok github.com/novox/mesh-host/internal/network 1.050s
ok github.com/novox/mesh-host/internal/outward 1.035s
ok github.com/novox/mesh-host/internal/profile 1.531s
ok github.com/novox/mesh-host/internal/reachable 1.029s
ok github.com/novox/mesh-host/internal/store 1.097s
ok github.com/novox/mesh-host/internal/system 1.012s
ok github.com/novox/mesh-host/internal/tunnel 1.015s
ok github.com/novox/mesh-host/internal/units 1.007s
ok github.com/novox/mesh-host/internal/upgrade 1.049s
ok github.com/novox/mesh-host/internal/witness 1.442s
ok github.com/novox/mesh-host/packaging 1.008s
? github.com/novox/mesh-host/rootsearch [no test files]
ok github.com/novox/mesh-host/validate 1.007s
**Merge check** at `8fc1d72c`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds mesh-host → ace, g14, novox, shanks; no bus step
- tier 0: mesh-host
- ace: receives mesh-host
- g14: receives mesh-host
- novox: receives mesh-host
- shanks: receives mesh-host
Run by the build seat on g14 as `build-1791569060643270655` (`builds --log build-1791569060643270655`).
```
--- the gate: mesh-host
mesh-host: ok
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 0 module(s) changed against 4 machine(s), as the snapshot of 2026-10-09T17:59:32Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-09T17:59:32Z
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-host
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-host/cmd/mesh-bootstrap 1.012s
ok github.com/novox/mesh-host/cmd/mesh-host 5.633s
ok github.com/novox/mesh-host/examples 1.016s
ok github.com/novox/mesh-host/internal/accounts 1.020s
ok github.com/novox/mesh-host/internal/apply 2.457s
ok github.com/novox/mesh-host/internal/bootstrap 1.403s
ok github.com/novox/mesh-host/internal/bundle 1.031s
ok github.com/novox/mesh-host/internal/declaration 1.061s
ok github.com/novox/mesh-host/internal/firewall 1.789s
ok github.com/novox/mesh-host/internal/identity 1.065s
ok github.com/novox/mesh-host/internal/image 1.027s
ok github.com/novox/mesh-host/internal/inventory 1.032s
ok github.com/novox/mesh-host/internal/link 10.495s
ok github.com/novox/mesh-host/internal/liveness 2.064s
ok github.com/novox/mesh-host/internal/meshcli 1.352s
ok github.com/novox/mesh-host/internal/network 1.050s
ok github.com/novox/mesh-host/internal/outward 1.035s
ok github.com/novox/mesh-host/internal/profile 1.531s
ok github.com/novox/mesh-host/internal/reachable 1.029s
ok github.com/novox/mesh-host/internal/store 1.097s
ok github.com/novox/mesh-host/internal/system 1.012s
ok github.com/novox/mesh-host/internal/tunnel 1.015s
ok github.com/novox/mesh-host/internal/units 1.007s
ok github.com/novox/mesh-host/internal/upgrade 1.049s
ok github.com/novox/mesh-host/internal/witness 1.442s
ok github.com/novox/mesh-host/packaging 1.008s
? github.com/novox/mesh-host/rootsearch [no test files]
ok github.com/novox/mesh-host/validate 1.007s
```
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
hq issue 356 (hq pull request named in the thread once open). On 2026-10-09 at 18:01 the operator typed the node-engine's
hand-overon the control-node, as a condition told them to, and the engine on PATH answered "unexpected argument":/usr/bin/nox-mesh-hostis the engine a person copied at installation, every delivered engine runs from the versions directory, and nothing ever renewed the file on PATH. And the hand-over itself was a terminal act at the machine with no channel.Reviewers' checklist
AnswerHandOver(internal/link/handover.go) bounds the ask (8 KiB), refuses one that is not a hand-over, refuses nobody asking, a name over one line or 200 bytes or holding NUL, and no directory — before the handler sees anything.handOverFound(cmd/mesh-host/found.go) then judges the path: absolute;filepath.Clean(path) == path(so no.., no doubled or trailing separator); one the node's state records as used as found; on disk a directory and not a link;EvalSymlinks(path) == path(reached through no link). Nothing is recorded unless all pass.mesh.node.<node>.ask.hand-over, which only the controller may publish (mesh.node.>is the controller's grant; the node's grant for the subject and the right to answer are in mesh-controller's PR). The controller runsnode hand-overonly as its terminal (anodesubcommand that is not a read is refused through every verb and through mesh-cli outside the terminal). The engine runs nothing it is sent; its one act is the record the next apply reads, ashand-overtyped at the machine already did.TestTheFallbackBecomesACopyOfTheProvedEngine(bytes, mode 0755, running file untouched; already a copy → same inode, no temp file),TestAnEngineRunningFromTheFallbackRenewsNothing(SameFile),TestAMissingFallbackIsPlaced,TestALinkAtTheFallbackIsRefusedAndLeft(link refused, target untouched, link kept),TestAnUnreadableRunningEngineIsSaid,TestTheFallbackPathIsTheLaunchers(internal/upgrade);TestTheHandOverAskAndAnswerKeepTheirFieldNames,TestAHandOverAskIsJudgedBeforeAnythingIsRecorded(seven refusals, each counted as never reaching the handler) (internal/link);TestHandOverRecordsOnlyAFoundDirectory(the subcommand and the controller's ask are one judgement; who asked is recorded),TestAHandOverIsRefusedForEveryPathThatIsNotTheFoundDirectory(nine refusals, nothing recorded, the exact path taken),TestAFoundDirectoryIsStatedUnhealthy(the statement namesnox node hand-over <node> <directory>on the control-node, never the binary, never the path) (cmd/mesh-host).What changes
upgrade.RenewFallback(internal/upgrade/fallback.go): when a build is proved (the mesh took a report under it — the same evidenceknown-goodis written on,proving.heardincmd/mesh-host/witnessing.go), the engine copies itself over the launcher's fallback (MESH_HOST_BIN, default/usr/bin/nox-mesh-host, the same variable the launcher reads) when the bytes differ: a copy, renamed into place, mode 0755 — never a link (ADR 0012), never a write through one, nothing while the engine runs from the fallback itself. Chosen over an exec-shim: the launcher runs that file as the engine while nothing is delivered (a shim there would run itself), and which version runs is the launcher's policy. The launcher is unchanged.link.HandOvers/HearHandOvers(internal/link/hearing_nats.go): a request subscription onmesh.node.<node>.ask.hand-over, answered once on the reply it carried withHandOverAnswer{said|refused};Queue.HandOveris the engine's judgement (handOverFrom), wired inrun.gobeside the report ask. Field names held on both sides. The engine's ownhand-over <dir>stays, for a machine no mesh can reach, through the samehandOverFound.nox node hand-over <node> <directory>on the control-node; the path stays the resource's target (evidence), never in the reason the operator's channel says. The usage says the binary is a daemon, the operator's command isnox, and the subcommands are install-time and rescue, run as root at the machine; the README says the same and uses the binary's real name.Checks.
make check(fmt, vet,go test ./..., the launcher's, rollback's and roused tests, build) passed locally on this tree before the issue was renumbered 353→356 (comments and the README only since);go testofinternal/upgrade,internal/link,cmd/mesh-hostandgo build ./...pass ate74ee82. Not merged; nothing pushed to a node.Order. mesh-controller's PR first (it composes the node's grant for the subject and the right to answer; an engine delivered before it has its subscription refused and says so, and
nox node hand-overthen says nothing on the node answers), then this. mesh-catalog's and hq's PRs in any order. After both are live:nox node hand-over <control-node> <the messenger's data directory>on the control-node, then its next apply.Delivery
novox/mesh-host@8fc1d72cd546— failed since 2026-10-09T18:07:48ZDelivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
Transitions
The commit's note under
refs/notes/mesh-plankeeps every transition:git log --notes=mesh-plan.The record: hq #256 (issue 356 — renumbered from 353, which another branch took first; the code says 356). The controller's side is mesh-controller #187 (merge first: the node's grant), the catalogue's check mesh-catalog #165.
Merge check at
e74ee82amesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
Run by the build seat on g14 as
build-1791563763968800353(builds --log build-1791563763968800353).Review addressed — head
fc6c5a9(main has not moved sinceb0d03be, so the branch is the merge result).Forgeable ask (blocking).
AnswerHandOver(check, handle, body)now verifiesSignedHandOverwithm.Signer(the keyhandleBodyverifies declarations with, carried intonatsLink.signer) overHandOverContext+ the ask's bytes before reading anything out of the ask; then refuses an ask for another node, expired, with no expiry, good for longer than 1m + 1m skew, without a nonce of ≥16 bytes, or whose nonce was taken (process-wideHandOverNonces, each kept until its ask expires — a reconnect is no replay; a restart can replay only within the minute, and recording the same hand-over again is idempotent). An engine with no key believes nothing.HandOverReplyis the subscription's whole body: no reply subject → nothing judged, recorded or said.Apply follows a link (blocking). A handed-over directory now goes through
applyHandedOver:EvalSymlinks(path) == pathat apply time, thengiveHandedOver— one descriptor fromopenUnderAs("/", path, kindDir)(every componentopenatO_NOFOLLOW|O_DIRECTORY from /, judged by fstat),Fchmod/Fchownon it. Nothing is changed by path.TestAHandedOverDirectoryIsChangedThroughNoLinkswaps the parent, the grandparent and the directory itself for links to a victim tree after the hand-over: refused, the victim keeps mode 0700, and the error names which check refused.TestGiveHandedOverFollowsNoLinkholds the descriptor walk alone.Test gaps.
TestAHandOverIsAnsweredOnlyWhenItCarriesAReply(reply guard),TestALinkThatHearsHandOversIsGivenTheQueues(run.go hook, throughserve),TestNatsAHandOverIsAskedOfTheNode(real server: answered on its own subject, another node's unheard, a publish with no reply handled by nobody — run locally against a throwaway nats-server, skipped without MESH_TEST_NATS as the other Nats tests),TestOnlyAnAccountUnderThisBuildProvesItnow assertsrenewFallbackran (fallback = this test binary's bytes, said) and that nothing before the proof renewed it — and pointsMESH_HOST_BINat a temp file, so a test run as root never writes/usr/bin.TestAFallbackThatIsNotARegularFileIsRefused(a directory at the fallback; a directory as the running engine). The redundant same-file case inRenewFallbackis gone (same bytes covers it).Mutations: 27 checks each removed in turn, 27 killed (signature, no key, context, node, expired, too long, nonce shape, replay, nonce prune, by empty/line, no path, size, reply guard, run.go hook, the NATS respond, apply re-check, apply descriptor, handed-over branch, fallback not-regular/self not-regular/link/same-bytes, renewFallback called, path clean/link/through-link).
Checks:
make checkandmerge-check.shexit 0 atfc6c5a9.Merge check at
fc6c5a92mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
Run by the build seat on ace as
build-1791567965774413759(builds --log build-1791567965774413759).Confirmation review addressed. Head
9a807de.The hand-over is now checked before the
oursshortcut inapplyDirectory:if existed && HandedOver()[handOverKey(r.ID, r.Path)] { return applyHandedOver(...) }. A handed-over directory therefore goes through the re-check and the no-link descriptor on every apply, not only the first. Before this, after the first apply the record was no longer as found, so the directory read as the mesh's own and later applies usedchmodDirPathandownby path.TestAHandedOverDirectoryIsChangedThroughNoLinkgains two cases: the parent, and the grandparent, swapped for a link after an apply already gave the directory its mode (the record is checked to be no longer as found first). With the old branch order both new cases fail ("a handed-over directory reached through a link was applied"); with the fix both pass.make checkandmerge-check.shexit 0 at9a807de. Main has not moved sinceb0d03be, so the branch is the merge result.The same by-path gap for ordinary mesh directories outside a home is older than this issue and is not fixed here. It is recorded as hq issue 358 (hq #258).
Merge check at
9a807de6mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)mesh/repo-check: FAIL — its merge-check.sh failed: --- FAIL: TestTheSetuidSearchRunsInTheBackgroundAndIsKeptForItsInterval (0.00s)Delivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
Run by the build seat on shanks as
build-1791568726990600635(builds --log build-1791568726990600635).9a807dewas rejected by mesh/repo-check, and not because of this change. The failing test isinternal/accountsTestTheSetuidSearchRunsInTheBackgroundAndIsKeptForItsInterval, which this branch never touched. The same test fails on main (b0d03be) too.Cause:
SetuidCache.Lookcompares the caller'snowwithc.at, butc.atwas read from the wall clock (time.Now()) when the search finished. The test's clock is a fixed date (2026-10-08 19:00 UTC). Its look at +24h is due only while the wall clock is earlier than 18:00 UTC on 2026-10-09. From then on the test fails on every branch; it passed at 17:48 and failed at 18:00.Effect on a machine: it is the same defect. A caller whose clock runs behind the wall clock never searches again.
Fix: a separate commit, head
8fc1d72. A search's time (at,failedAt) is now thenowit was started at, so there is one clock, the caller's. A result reads at most one search older than it is, which errs towards searching again. The test fails without the fix and passes with it (5 runs).merge-check.shandmake checkexit 0 at8fc1d72. Main has not moved.Merge check at
8fc1d72cmesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
Run by the build seat on g14 as
build-1791569060643270655(builds --log build-1791569060643270655).