An abort or a timeout kills only the local ssh. A non-pty sshd command gets no SIGHUP, so the remote
add can still write its line after the undo's remove. The broker now marks install done only after
`await install(...)` returns. When it never returned, the undo records hostLineLeft with the tag, so
grant.failed and status name the line, hosts.remove refuses until `mosaic hosts sweep` clears it.
The :432 test comment now matches. Row a's enroll does no remote add, so it has no such race.
Candidate gizmoduck/row249-on-8a04 05595794b (Gizmoduck), manifest 3a969bb136bc, approved by
Duckworth in round 1 (rev 2889-2890 request c30068, verdict c30070). Gate on Sage's slot 14:53Z
(sage-2026): grant-executor.test.mjs whole 39/0 and bus 598/0 with no stub left, and C1, which
drops the `if (!installed)` branch, fails G8 and both G23 tests. Webby's N1 on X5 `sleep 30` is
deferred by Duckworth (DEFERRED.md).
5.8 KiB
Row 249 build notes (Gizmoduck)
Gitea #1751, Vikunja P19 445 under #436. Brief: docs/plans/2026-10-20_host-credentials.md, section
"Machine grant row b follow-up: the undo can't prove the remote add has ended" (6de1ebc62). Reviewer
Duckworth, gate owner Sage. Branch gizmoduck/row249-on-8a04, on row 248's landing 8a04f4aa4 (sage-2012).
No file under packages/bus/src changed between 6d5545d44 and 8a04f4aa4, so the brief's src line numbers
hold on the base. Line numbers below are on this branch.
Correction to the brief
The brief, copying gizmoduck-0139, puts the sweep's host-unknown refusal at broker.mjs:1350. On
6d5545d44 it is :1349. On this branch it is :1354.
The change
#runHostGrant in packages/bus/src/broker.mjs:
- :943 adds
installed = falsenext toinstalling. - :956 sets
installed = trueon the line afterawait this.#hostGrant.install(...), before thestop()that follows it. An abort that lands after install returned still ends revoked, with no hint, because install returning means every request it sent has exited. - :1025, in the catch, inside
if (installing)and after the remove:if (!installed) extra = { hostLineLeft: tag };. The remove still runs first and its result doesn't matter. A remove that throws sets the same hint as before (:1021).
The rule keys on install not returning, not on signal.aborted. That covers an abort (host-run.mjs:136,
SIGKILL to the local group), a timeout (host-run.mjs:131-134, SIGTERM, then SIGKILL after graceMs) and
a dropped connection, which fails the request with no signal at all. #hostGrantFailed (:845) already
adds "A line tagged X may be left on the host. Run: mosaic hosts sweep " when hostLineLeft is
set (:852), so the message needed no change.
False hints. install is one call that runs list, maybe remove, then add (host-grant.mjs:329-333).
Any failure inside it now records the hint, including a list that exits 1 before any add was sent, and
an abort that lands before the first ssh starts (the G23 X5 case at "a revoke right after the spend").
The brief accepts this for an add that exits nonzero. The other cases are the same kind: the line isn't
on the host, and a sweep that doesn't list the tag drops the hint (G24, "a left tag no longer on the
host", broker.mjs:1358). Narrowing it would need install to report which request was in flight, which is
a change to host-grant.mjs's API that the brief doesn't ask for.
Row a's enroll
The race can't happen there, because enrollment sends nothing to the host. #runEnrollment
(broker.mjs:772-842) writes the record, calls this.#hostGrant.enroll(business, host) (:800) and
commits the fingerprint. enroll (host-grant.mjs:247-286) runs ssh-keygen locally and writes the
private half to its file. It opens no ssh session. The installer line goes on the host by hand: the
notice tells ops-01 to write it, then to run mosaic hosts check. The undo at :835 is unenroll, which
deletes the local key file. There is no remote add to outlive a kill, and no remote undo to race with.
Tests
packages/bus/tests/grant-executor.test.mjs:
lateAdd(:451) is the shared tail. After the grant ends it asserts the code,hostLineLefton grant.failed, the sweep line in the notice, and that the undo's remove was the last request. Then it writesrelease, so the held add writes its line and exits 0, and waits for the line ingranted. It checks there's no record,hosts.statuslists the tag inhostLineLeft,hosts.removerefuses host-line-left,hosts.sweepreturnsremoved: [tag]and the line is gone, thenhosts.removesucceeds.- :468, the abort case.
hold-add, the human approves, the add holds,abortClaimaborts the executor's claim. Ends revoked. - :480, the timeout case. A 30 s wait is too long for the file, and making the timeout an option would
add a src seam, so the test wraps
installthroughopts.wrapinstead. The wrapper passes install its own AbortController signal, waits forholding, aborts it, which SIGKILLs the local ssh group as the timer does, then throws as a timed-out request does. The executor's signal isn't aborted, so it ends host-failed. Nothing under src changes for it. - G8 (:283) changes. fail-list and fail-add now carry
hostLineLeftand the sweep line. noop-add fails at verify, after install returned, so it keeps no hint and the old message. - The comment at :436 (:432 at
6d5545d44) now saysskipmakes the held add exit without writing and that a killed session's add can still write. G13 has no such comment.
Both new tests write release, so their held stub exits before t.after removes the dir. Row 248's dir
check (:67, :72) is the backstop.
Mutant
/mnt/storage/scratch/gizmo-235-out/mutants-249.py, on row 235's runner. C1 removes the :1025 line and
runs grant-executor.test.mjs. Both new tests and G8 should fail under it. --check matches its anchor
once.
Runs
On Sage's grant sage-2015, at 8457af076 (7cee1a1ab plus this file), TMPDIR /mnt/storage/scratch/g249t:
| Run | Window | Result |
|---|---|---|
| grant-executor.test.mjs whole | 13:29:01-13:29:21Z | 39 of 39 pass |
| C1 | 13:29:21-13:29:40Z | killed: both row 249 tests and G8 fail on hostLineLeft undefined |
After C1 the worktree was clean and broker.mjs matched HEAD. After the runs, no node, sshd or sh process had a
cwd under the worktree or g249t, and no fake/add or fake/remove stub was running. The bus suite runs on
Sage's gate.
Rebase onto 8a04f4aa4 (sage-2012)
The first commit, 02bfd184b, sat on bc579f7a8 as gizmoduck/row249, which was never pushed. Row 248 landed
as 8a04f4aa4, changing grant-executor.test.mjs at :54, :67 and :72. gizmoduck/row249-on-8a04 starts at
8a04f4aa4 and cherry-picks 02bfd184b with no conflict. 248's lines stay as landed. The patch-id is the
same on both (c33823941e61). Test line numbers moved down by one, and this file was updated for that.