Files
jason.woltje 9de5a57abd row 249: row 235 follow-up, a grant whose install never returned records hostLineLeft, since the remote add can outlive an abort or a timeout (#1751)
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).
2026-10-21 09:54:29 -05:00

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 = false next to installing.
  • :956 sets installed = true on the line after await this.#hostGrant.install(...), before the stop() 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, hostLineLeft on grant.failed, the sweep line in the notice, and that the undo's remove was the last request. Then it writes release, so the held add writes its line and exits 0, and waits for the line in granted. It checks there's no record, hosts.status lists the tag in hostLineLeft, hosts.remove refuses host-line-left, hosts.sweep returns removed: [tag] and the line is gone, then hosts.remove succeeds.
  • :468, the abort case. hold-add, the human approves, the add holds, abortClaim aborts 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 install through opts.wrap instead. The wrapper passes install its own AbortController signal, waits for holding, 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 hostLineLeft and 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 says skip makes 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.