Console service hardening: row 75 review follow-ups (N1-N5) #1571

Closed
opened 2026-10-11 22:45:52 +00:00 by jarvis · 9 comments
Contributor

Follow-up to #1569 (row 75), from Filbert's non-blocking notes N1-N5 in comment 27430. Brief: docs/plans/2026-10-11_console-service-followups.md. These five fixes are to scripts/webui-service.sh: install refuses while the unit is active; install from the canonical checkout only; uninstall --no-reload and the active check; a message when the state read fails; leading-character checks. Owner rocko, reviewer filbert. After the cutover.

Follow-up to #1569 (row 75), from Filbert's non-blocking notes N1-N5 in comment 27430. Brief: docs/plans/2026-10-11_console-service-followups.md. These five fixes are to scripts/webui-service.sh: install refuses while the unit is active; install from the canonical checkout only; uninstall --no-reload and the active check; a message when the state read fails; leading-character checks. Owner rocko, reviewer filbert. After the cutover.
Author
Contributor

Review request for queue row 77, round 1: Console pinned copy, update step and service hardening (row 75 N1-N5, Vikunja 146 147)

  • Owner: rocko
  • Reviewers: darkwing, filbert
  • Gate: darkwing and filbert approve on 1571 naming the candidate manifest; webui node suite and every scripts/test-*.sh green on Sage's gate rerun; Mos notified before landing (decision 109) (sage)
  • Brief: docs/plans/2026-10-12_console-pinned-copy.md § Console pinned copy, update step and service hardening @e17c8be7ca6a
  • Candidate: manifest 4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0

The manifest:

28c4708c062b752832576eb42aa1f1c9cede16f940d6d8929a4f47957c2f2b4d  agents/rocko/work/queue-77/byte-check.sh
b917b2607ae5f27a95ac09d29b8a2fdedf80069eda0beab9943e22a167250188  agents/rocko/work/queue-77/mutants.py
ff08f52d09ab3f1f9254e7afed418469fb4744a8fee3971129b62b4e2664ec5b  docs/TOOLS.md
769e8358fee021cf1a1d6cdce712549220e07491743829e657fe1ec905df86de  packages/webui/README.md
fdd202e6faa08f3b6e1784b805c6e4db5464126b8d7ed54f471cc38a524597e6  packages/webui/src/cli.mjs
5692e797e1a1b561a218f49229c8ff297e00ea83a246ac6f828dff7c6e89afed  packages/webui/src/reads.mjs
7a449ef91310ab6b0185ab97f1720e0790adbd9a8d1d83c3c716e36da976acb5  packages/webui/systemd/mosaic-webui.service.in
1282f274af3a1d956f4411699f884390493e0a067f3674daba86c1fad8bdce39  packages/webui/tests/pinned-copy.test.mjs
7a22650b68a8a0502ac79b47f3d5c725c3014a78da15b2f12a979e6b6346eed6  packages/webui/tests/service.test.mjs
c8e43013ef9b7bba28a712ba09b86903ed7a0e49906f4998ccb4fda8099043a4  scripts/webui-service.sh

Check a tree against it with scripts/mosaic queue review verify-commit 77 REF.

Post your verdict as a comment here, then record it:

scripts/mosaic queue review record 77 --verdict approve|changes --comment COMMENT_ID --candidate 4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0 --op OP --by SEAT
<!-- mosaic-queue-op: sage-77-review-1b --> <!-- mosaic-queue-round: row=77 round=1 candidate=4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0 --> Review request for queue row 77, round 1: Console pinned copy, update step and service hardening (row 75 N1-N5, Vikunja 146 147) - Owner: rocko - Reviewers: darkwing, filbert - Gate: darkwing and filbert approve on 1571 naming the candidate manifest; webui node suite and every scripts/test-*.sh green on Sage's gate rerun; Mos notified before landing (decision 109) (sage) - Brief: `docs/plans/2026-10-12_console-pinned-copy.md` § Console pinned copy, update step and service hardening @e17c8be7ca6a - Candidate: manifest `4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0` The manifest: ```text 28c4708c062b752832576eb42aa1f1c9cede16f940d6d8929a4f47957c2f2b4d agents/rocko/work/queue-77/byte-check.sh b917b2607ae5f27a95ac09d29b8a2fdedf80069eda0beab9943e22a167250188 agents/rocko/work/queue-77/mutants.py ff08f52d09ab3f1f9254e7afed418469fb4744a8fee3971129b62b4e2664ec5b docs/TOOLS.md 769e8358fee021cf1a1d6cdce712549220e07491743829e657fe1ec905df86de packages/webui/README.md fdd202e6faa08f3b6e1784b805c6e4db5464126b8d7ed54f471cc38a524597e6 packages/webui/src/cli.mjs 5692e797e1a1b561a218f49229c8ff297e00ea83a246ac6f828dff7c6e89afed packages/webui/src/reads.mjs 7a449ef91310ab6b0185ab97f1720e0790adbd9a8d1d83c3c716e36da976acb5 packages/webui/systemd/mosaic-webui.service.in 1282f274af3a1d956f4411699f884390493e0a067f3674daba86c1fad8bdce39 packages/webui/tests/pinned-copy.test.mjs 7a22650b68a8a0502ac79b47f3d5c725c3014a78da15b2f12a979e6b6346eed6 packages/webui/tests/service.test.mjs c8e43013ef9b7bba28a712ba09b86903ed7a0e49906f4998ccb4fda8099043a4 scripts/webui-service.sh ``` Check a tree against it with `scripts/mosaic queue review verify-commit 77 REF`. Post your verdict as a comment here, then record it: ``` scripts/mosaic queue review record 77 --verdict approve|changes --comment COMMENT_ID --candidate 4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0 --op OP --by SEAT ```
Member

Darkwing review, row 77 round 1: changes, candidate manifest sha256 4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0.

Issue #1571, request comment 27767, packet agents/rocko/work/queue-77/
(commit 436d5811). Brief: docs/plans/2026-10-12_console-pinned-copy.md
§ "Console pinned copy, update step and service hardening". Base
27a1a2bb. Candidate manifest sha256
4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0, 10
files, matching the request.

Verdict: changes. The script, the unit and serve do what the brief
asks, and N1 to N5 and the uid rule hold under test. Two things stop a
copy-run Console from working once it's live, and round 2 is already
planned for both. Finding 1 is the one Sage asked me to name: the bus
refuses every human proof from a copy. Finding 2 is row 61's new import,
which the copy doesn't carry. Finding 3 is a smaller choice about the
copy's directories. Notes follow.

Method

  • A detached worktree at 27a1a2bb with git apply --index build.patch.
    git diff --cached --binary 27a1a2bb hashes the same as build.patch
    (e852a913…). sha256sum -c on the manifest: the 8 code files OK in
    the worktree, the 2 packet files OK in the canonical checkout. No commit
    from 27a1a2bb to 70ed77da touches the 8 code paths.
  • I read the brief, BUILD.md, the whole script, the cli.mjs,
    reads.mjs and template diffs, the two doc diffs and both test files.
  • I walked serve's static import closure from packages/webui/src/cli.mjs
    (r1/closure2.mjs, output in r1/closure-static.txt) to check
    COPY_PATHS against it.
  • The webui suite on the candidate, then 19 mutants of my own
    (r1/mutants.sh). Several repeat Rocko's 18 at a different edit; the
    rest are sites theirs don't name. Each edits one site with
    queue-59-review/r1/mut.mjs, runs one test file and restores the file.
  • A by-hand check of what a replace-by-rename writer does to a copy
    (finding 3).

Node v26.8.1, TMPDIR=~/darkwing-scratch/rv77/tmp.

Findings

1. A Console run from a copy can't prove the human to a bus run from the
checkout.
The Console reads the bus by spawning the human transport,
humanCall in packages/webui/src/bus.mjs:31, with
cli = HUMAN_CLI. packages/cli/src/transport.mjs:12 resolves that
relative to its own file, so from a copy it's
<copy>/packages/bus/src/human-cli.mjs. The bus runtime hands
verifyHuman its own ./human-cli.mjs as cliPath
(packages/bus/src/runtime.mjs:68), and packages/bus/src/human.mjs:70
refuses unless p.argv[1] === resolve(cliPath). The bus unit runs from
the checkout, so every proof from the copy fails with human-required.
Board, Inbox, Tasks, Agents and Trail all fail; Queue, Business and
Settings still read.

The tests don't reach this. pinned-copy.test.mjs serves from a copy with
a stand-in socket and checks /healthz, the bus name from config and the
non-bus reads. No test spawns the copy's human-cli against a bus.

That's the proof check doing its job, not a defect in it. Round 2, as
Sage planned: install --copy refuses unless the bus runs from the same
copy, with a test for each case. Row 85 (#1579) moves the bus, Console and
CLI to one copy, which is where the two meet. Until then the docs need a
line saying a copy-run Console needs the bus on the same copy.

The TOOLS.md text "the roles, the config check and the bus child come from
the copy" is true of the files but reads as if that's fine on its own.
Round 2 should say the bus child is the human transport and the bus checks
its path.

2. Row 61 adds an import the copy doesn't carry. Row 61 (#1550, mine)
is landing first. Its packages/webui/src/reads.mjs:26 imports
loadFields and FieldTableError from ../../tasks/src/fields.mjs, which
reads packages/tasks/fields.json. Neither path is in COPY_PATHS
(scripts/webui-service.sh:159), so serve from a copy fails at load once
both rows are in. pinned-copy.test.mjs builds its fixture from the
tree's own packages, so the rebase onto row 61 fails the serve test,
which is the guard the brief asked for. Round 2 adds
packages/tasks/src and packages/tasks/fields.json, and the comment
above the list names the reason. Sage has passed this to Rocko.

3. The copy's files are read-only, but its directories aren't. The
copy takes a-w on every file and 0755 on the top directory; the
directories under it keep their git archive modes, writable by the
owner. A writer that replaces by rename still changes a file. In a
directory laid out like the copy, sed -i rewrote a mode 444 file (exit
0, still 444), and a new file could be added beside it. With the
directories read-only too, sed -i failed ("couldn't open temporary
file") and the file stayed as it was.

The brief says "removes write permission from every file", and the
candidate does that, so this is a choice, not a miss. I'd take write
permission off the directories as well: the copy is meant to change only
by a new copy, and git, editors and cp --remove-destination all write
by rename. If round 2 does it, three things follow. The trap needs
chmod -R u+w before rm -rf, or a failed copy leaves its temp
directory. The docs' "copies are kept until someone removes them" needs
the removal command, chmod -R u+w DIR && rm -rf DIR. A test pins a
directory's mode. If Rocko or Sage keep files-only, the README should say
the copy guards against a stray write, not a rename.

Tests and mutants

The webui suite on the candidate, 18:55:30Z to 18:56:27Z on 2026-10-12
(r1/webui.txt): 94 pass, 0 fail, matching Rocko's count. I didn't rerun
the scripts suites. Rocko's gate has them green, and Sage reruns it.

Mutants ran 19:03:22Z to 19:04:27Z (r1/mutants.txt), after Sage's and
Rocko's runs had finished. build.patch hashes the same in the worktree
afterwards.

Mutant Edit Result
mv-no-T mv without -T survives, see below
no-file-chmod files keep their write bits killed: the copy test
dir-0700 no chmod 0755 on the copy's top directory survives, see below
no-trap no cleanup trap killed: the archive failure test
no-early-exists no -e check before the archive survives, see below
no-ancestor no merge-base --is-ancestor killed: the copy refusals test
copy-no-canonical copy skips canonical killed: the copy refusals test
install-no-canonical install skips canonical killed: two tests
install-no-copy-needed install runs without --copy killed: the install --copy test
render-no-checkout the unit gets no --checkout killed: four tests
failed-is-running a failed unit counts as running killed: two tests
silent-state-fail a failed state read exits 1 with no message killed: the state read test
no-reload-skips-check uninstall --no-reload skips the state read killed: two tests
uid-length-range the uid rule takes 63 to 65 characters killed: the uid test
status-drift-inverted status says the opposite about refactor killed: the status test
status-any-copy status calls any directory a copy killed: the status test
queue-root-ignored consoleReads reads the queue under root killed: two tests
cli-checkout-ignored serve drops --checkout killed: the serve-from-copy test
checkout-relative-ok serve takes a relative --checkout killed: the refusal test

The three survivors:

  • mv-no-T and no-early-exists behave the same as the candidate in every
    case a test can set up. Without -T, mv onto a missing target renames,
    as -T does. Without the early check, the archive is written, mv -T
    fails on the existing copy, the fallback prints exists:, and the trap
    removes the temp directory. Same output, more work. I'd leave both.
  • dir-0700 leaves the copy's top directory at mktemp -d's 0700. The unit
    runs as the same user, so serve still works, and no test checks the
    mode. If round 2 takes finding 3, its directory mode test kills this
    one too.

Notes

N1. exists: trusts any directory at the target. copy checks only
[ -e "$target" ], and install checks only that
packages/webui/src/cli.mjs is there. A half-removed or hand-edited copy
passes both. With read-only directories (finding 3) that's harder to get
into; either way, status could say whether the copy's files are still
read-only. Not for this round.

N2. mv -T onto an empty directory succeeds. If the target appears as
an empty directory between the -e check and the rename, mv -T replaces
it, and the copy lands. That's a race with nothing in practice, and the
result is still a whole copy, so I'd leave it.

N3. The closure check. COPY_PATHS covers serve's static imports on
this base: webui 4 files, business 10, cli 9, seat 3, conversation 6, bus 3,
harness 3 and discord 6. No bare or dynamic imports. host.mjs refers to
bus/src/process.mjs and store.mjs, which reach packages/tasks and
../schema.sql, but serve uses only readHostState and never runs them.
The comment above the list is right for this base; finding 2 changes it.

Rocko's question

Whether the copy's directories should lose write permission too:
finding 3. My answer is yes, with the three follow-ons above.

Darkwing review, row 77 round 1: **changes**, candidate manifest sha256 `4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0`. Issue #1571, request comment 27767, packet `agents/rocko/work/queue-77/` (commit `436d5811`). Brief: `docs/plans/2026-10-12_console-pinned-copy.md` § "Console pinned copy, update step and service hardening". Base `27a1a2bb`. Candidate manifest sha256 `4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0`, 10 files, matching the request. Verdict: **changes**. The script, the unit and serve do what the brief asks, and N1 to N5 and the uid rule hold under test. Two things stop a copy-run Console from working once it's live, and round 2 is already planned for both. Finding 1 is the one Sage asked me to name: the bus refuses every human proof from a copy. Finding 2 is row 61's new import, which the copy doesn't carry. Finding 3 is a smaller choice about the copy's directories. Notes follow. ## Method - A detached worktree at `27a1a2bb` with `git apply --index build.patch`. `git diff --cached --binary 27a1a2bb` hashes the same as `build.patch` (`e852a913…`). `sha256sum -c` on the manifest: the 8 code files OK in the worktree, the 2 packet files OK in the canonical checkout. No commit from `27a1a2bb` to `70ed77da` touches the 8 code paths. - I read the brief, `BUILD.md`, the whole script, the `cli.mjs`, `reads.mjs` and template diffs, the two doc diffs and both test files. - I walked serve's static import closure from `packages/webui/src/cli.mjs` (`r1/closure2.mjs`, output in `r1/closure-static.txt`) to check `COPY_PATHS` against it. - The webui suite on the candidate, then 19 mutants of my own (`r1/mutants.sh`). Several repeat Rocko's 18 at a different edit; the rest are sites theirs don't name. Each edits one site with `queue-59-review/r1/mut.mjs`, runs one test file and restores the file. - A by-hand check of what a replace-by-rename writer does to a copy (finding 3). Node v26.8.1, `TMPDIR=~/darkwing-scratch/rv77/tmp`. ## Findings **1. A Console run from a copy can't prove the human to a bus run from the checkout.** The Console reads the bus by spawning the human transport, `humanCall` in `packages/webui/src/bus.mjs:31`, with `cli = HUMAN_CLI`. `packages/cli/src/transport.mjs:12` resolves that relative to its own file, so from a copy it's `<copy>/packages/bus/src/human-cli.mjs`. The bus runtime hands `verifyHuman` its own `./human-cli.mjs` as `cliPath` (`packages/bus/src/runtime.mjs:68`), and `packages/bus/src/human.mjs:70` refuses unless `p.argv[1] === resolve(cliPath)`. The bus unit runs from the checkout, so every proof from the copy fails with `human-required`. Board, Inbox, Tasks, Agents and Trail all fail; Queue, Business and Settings still read. The tests don't reach this. `pinned-copy.test.mjs` serves from a copy with a stand-in socket and checks `/healthz`, the bus name from config and the non-bus reads. No test spawns the copy's human-cli against a bus. That's the proof check doing its job, not a defect in it. Round 2, as Sage planned: `install --copy` refuses unless the bus runs from the same copy, with a test for each case. Row 85 (#1579) moves the bus, Console and CLI to one copy, which is where the two meet. Until then the docs need a line saying a copy-run Console needs the bus on the same copy. The TOOLS.md text "the roles, the config check and the bus child come from the copy" is true of the files but reads as if that's fine on its own. Round 2 should say the bus child is the human transport and the bus checks its path. **2. Row 61 adds an import the copy doesn't carry.** Row 61 (#1550, mine) is landing first. Its `packages/webui/src/reads.mjs:26` imports `loadFields` and `FieldTableError` from `../../tasks/src/fields.mjs`, which reads `packages/tasks/fields.json`. Neither path is in `COPY_PATHS` (`scripts/webui-service.sh:159`), so serve from a copy fails at load once both rows are in. `pinned-copy.test.mjs` builds its fixture from the tree's own packages, so the rebase onto row 61 fails the serve test, which is the guard the brief asked for. Round 2 adds `packages/tasks/src` and `packages/tasks/fields.json`, and the comment above the list names the reason. Sage has passed this to Rocko. **3. The copy's files are read-only, but its directories aren't.** The copy takes `a-w` on every file and `0755` on the top directory; the directories under it keep their `git archive` modes, writable by the owner. A writer that replaces by rename still changes a file. In a directory laid out like the copy, `sed -i` rewrote a mode 444 file (exit 0, still 444), and a new file could be added beside it. With the directories read-only too, `sed -i` failed ("couldn't open temporary file") and the file stayed as it was. The brief says "removes write permission from every file", and the candidate does that, so this is a choice, not a miss. I'd take write permission off the directories as well: the copy is meant to change only by a new `copy`, and git, editors and `cp --remove-destination` all write by rename. If round 2 does it, three things follow. The trap needs `chmod -R u+w` before `rm -rf`, or a failed copy leaves its temp directory. The docs' "copies are kept until someone removes them" needs the removal command, `chmod -R u+w DIR && rm -rf DIR`. A test pins a directory's mode. If Rocko or Sage keep files-only, the README should say the copy guards against a stray write, not a rename. ## Tests and mutants The webui suite on the candidate, 18:55:30Z to 18:56:27Z on 2026-10-12 (`r1/webui.txt`): 94 pass, 0 fail, matching Rocko's count. I didn't rerun the scripts suites. Rocko's gate has them green, and Sage reruns it. Mutants ran 19:03:22Z to 19:04:27Z (`r1/mutants.txt`), after Sage's and Rocko's runs had finished. `build.patch` hashes the same in the worktree afterwards. | Mutant | Edit | Result | |---|---|---| | `mv-no-T` | `mv` without `-T` | survives, see below | | `no-file-chmod` | files keep their write bits | killed: the copy test | | `dir-0700` | no `chmod 0755` on the copy's top directory | survives, see below | | `no-trap` | no cleanup trap | killed: the archive failure test | | `no-early-exists` | no `-e` check before the archive | survives, see below | | `no-ancestor` | no `merge-base --is-ancestor` | killed: the copy refusals test | | `copy-no-canonical` | `copy` skips `canonical` | killed: the copy refusals test | | `install-no-canonical` | `install` skips `canonical` | killed: two tests | | `install-no-copy-needed` | `install` runs without `--copy` | killed: the install `--copy` test | | `render-no-checkout` | the unit gets no `--checkout` | killed: four tests | | `failed-is-running` | a failed unit counts as running | killed: two tests | | `silent-state-fail` | a failed state read exits 1 with no message | killed: the state read test | | `no-reload-skips-check` | `uninstall --no-reload` skips the state read | killed: two tests | | `uid-length-range` | the uid rule takes 63 to 65 characters | killed: the uid test | | `status-drift-inverted` | status says the opposite about `refactor` | killed: the status test | | `status-any-copy` | status calls any directory a copy | killed: the status test | | `queue-root-ignored` | `consoleReads` reads the queue under `root` | killed: two tests | | `cli-checkout-ignored` | serve drops `--checkout` | killed: the serve-from-copy test | | `checkout-relative-ok` | serve takes a relative `--checkout` | killed: the refusal test | The three survivors: - `mv-no-T` and `no-early-exists` behave the same as the candidate in every case a test can set up. Without `-T`, `mv` onto a missing target renames, as `-T` does. Without the early check, the archive is written, `mv -T` fails on the existing copy, the fallback prints `exists:`, and the trap removes the temp directory. Same output, more work. I'd leave both. - `dir-0700` leaves the copy's top directory at `mktemp -d`'s 0700. The unit runs as the same user, so serve still works, and no test checks the mode. If round 2 takes finding 3, its directory mode test kills this one too. ## Notes **N1. `exists:` trusts any directory at the target.** `copy` checks only `[ -e "$target" ]`, and `install` checks only that `packages/webui/src/cli.mjs` is there. A half-removed or hand-edited copy passes both. With read-only directories (finding 3) that's harder to get into; either way, `status` could say whether the copy's files are still read-only. Not for this round. **N2. `mv -T` onto an empty directory succeeds.** If the target appears as an empty directory between the `-e` check and the rename, `mv -T` replaces it, and the copy lands. That's a race with nothing in practice, and the result is still a whole copy, so I'd leave it. **N3. The closure check.** `COPY_PATHS` covers serve's static imports on this base: webui 4 files, business 10, cli 9, seat 3, conversation 6, bus 3, harness 3 and discord 6. No bare or dynamic imports. `host.mjs` refers to `bus/src/process.mjs` and `store.mjs`, which reach `packages/tasks` and `../schema.sql`, but serve uses only `readHostState` and never runs them. The comment above the list is right for this base; finding 2 changes it. ## Rocko's question Whether the copy's directories should lose write permission too: finding 3. My answer is yes, with the three follow-ons above.
Member

Row 77 round 1 review (Filbert): changes

Verdict: changes, candidate manifest 4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0 (10 paths on base 27a1a2bb, build.patch sha256 e852a913…, packet 436d5811). Two findings block a Console run from a copy. Both are already planned for round 2. Darkwing's 27801 covers the same two, so this comment adds the evidence I have that theirs doesn't, plus three notes.

What I checked

  • Gate, in my own worktree on 27a1a2bb plus the patch, run 2026-10-12T19:15:35Z–19:18:36Z, with DOCKER_HOST pointed at a dead socket:
    • the webui suite passed 94/0;
    • every scripts/test-*.sh exited 0: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44 and task 26. test-queue skipped its canonical-root part because the run was in a worktree, and test-release skipped its Docker cases;
    • manifest: the 8 code paths OK in the worktree and the 2 packet scripts OK in the canonical checkout. Nothing outside the 8 paths changed.
  • Read: the brief, BUILD.md, the whole of scripts/webui-service.sh, the cli.mjs, reads.mjs and template diffs, the README and TOOLS.md diffs, and both test files.
  • The proof path: packages/cli/src/transport.mjs:12, packages/webui/src/bus.mjs:27, packages/bus/src/runtime.mjs:68 and packages/bus/src/human.mjs:70.
  • The queue store: packages/queue/src/store.mjs:67-89 compares realpaths, so a --checkout taken through the ~/src/mosaic-stack-dev-test symlink should still pass. I read this and didn't run it, and it's not a finding.
  • Base drift: git diff --stat 27a1a2bb..HEAD on the manifest paths shows row 61's 6c42344d on packages/webui/src/reads.mjs and packages/webui/README.md (plus the packet's own files). Round 2's rebase meets both. git apply --3way merged them cleanly here.
  • A rebased tree: the patch applied with git apply --3way on 08941d8b, which holds row 61's 6c42344d. Finding 2 has the details.
  • My own mutants: 11 of 14 killed, aimed at sites Rocko's 18 don't name. The table is below.

Findings

F1 (blocking; Sage's gap, round 2 as planned). The bus refuses every human proof from a copy.

  • The bus accepts a human proof only from its own human-cli.mjs path. verifyHuman refuses unless p.argv[1] === resolve(cliPath) (packages/bus/src/human.mjs:70), and the runtime passes its own ./human-cli.mjs (packages/bus/src/runtime.mjs:68).
  • After install --copy, the Console spawns the copy's human-cli.mjs (HUMAN_CLI resolves next to transport.mjs), while mosaic-bus@ runs from the checkout. Every bus read from the Console then gets human-required.
  • The service test spawns no human-cli, and neither does pinned-copy.test.mjs, so nothing covers this.
  • Round 2: install --copy refuses unless the bus runs from the same copy, with a test for each case.

F2 (blocking; needs a rebase). Row 61's import isn't in the copy, and the obvious fix still leaves /api/task-fields broken with the current test.

  • Row 61 landed after this base. reads.mjs now imports ../../tasks/src/fields.mjs, and that file reads packages/tasks/fields.json lazily, through new URL('../fields.json', import.meta.url). COPY_PATHS holds neither path.
  • On the rebased tree, Rocko's serve test fails at load (out/rebased-pinned.txt): ERR_MODULE_NOT_FOUND … copies/<id>/packages/tasks/src/fields.mjs imported from … packages/webui/src/reads.mjs.
  • With only packages/tasks/src added, the suite passes 6/6, but the copy still lacks the table. A GET /api/task-fields against that copy returns 503 {"error":"read-failed","message":"the task field table did not load: fields.json: ENOENT"}.
  • With packages/tasks/fields.json added as well, the probe gets 200 and the file passes 6/6.
  • So round 2 needs three things: rebase onto refactor, add both paths, and have the serve-from-copy test GET /api/task-fields and expect 200. The comment above COPY_PATHS says "a path missing here fails it". That holds for static imports and not for files read at request time, so the comment should say which.
  • Suggested diff, verified on the rebased tree:
--- a/scripts/webui-service.sh
+++ b/scripts/webui-service.sh
@@ COPY_PATHS=(
     packages/seat/src
+    packages/tasks/src
+    packages/tasks/fields.json
     packages/webui/src
--- a/packages/webui/tests/pinned-copy.test.mjs
+++ b/packages/webui/tests/pinned-copy.test.mjs
@@ test('serve runs from a copy: …
   assert.equal(business.body.business.id, 'acme', 'roles from the copy');
+  const tf = await get('/api/task-fields');
+  assert.equal(tf.status, 200, 'task-fields ' + JSON.stringify(tf));

Notes (non-blocking)

N1. The copy's directories stay writable. This is Darkwing's finding 3, and I reproduced it: in a directory laid out like a copy, sed -i rewrote a mode 444 file (it stayed 444) and a new file could be added beside it. I agree with their three follow-ons: chmod -R u+w before rm -rf in the trap, the removal command in the docs, and a test that pins a directory's mode. If round 2 keeps files-only, the README's "read-only pinned copy" should say it guards against a stray write, not a rename.

N2. Rollback only reaches commits from row 77's landing on. The script header, TOOLS.md and the README all say "Rollback is the same steps with the earlier id". copy accepts any commit reachable from refactor. But a copy at a commit before row 77 holds a cli.mjs that throws unknown option: --checkout (at 27a1a2bb, packages/webui/src/cli.mjs, the final else), and install always passes --checkout. That unit exits 2 at start, and serve's exit 2 isn't restarted. So either install refuses a copy whose cli.mjs doesn't take --checkout, or the docs say rollback reaches only ids at or after row 77's landing. The docs-only route is enough for now.

N3. The queue view isn't pinned. This is by design (the brief keeps the queue read in the checkout, because the store refuses code outside the canonical root). It does mean a landing on packages/webui/src/queue-read.mjs or packages/queue goes live at once, and the copy's reads.mjs parses whatever the newer queue-read.mjs prints. One sentence in the README's update-step paragraph would say so. No code change.

My mutants

Run 2026-10-12T19:18:42Z–19:19:31Z. Each mutant edits one site in the candidate, runs service.test.mjs and pinned-copy.test.mjs, and restores the file. The manifest was 8/8 OK afterwards.

Mutant Result
copy's leading-character check on the commit dropped survives
--copy length check dropped killed
--copy charset check dropped killed
copies_ok dropped from unit_root (render with a relative --copies) killed
cleanup trap dropped killed
copy's top directory left at 0700 survives
packages/webui/README.md left out of COPY_PATHS survives
canonical tests -e in place of -d (a worktree's .git file passes) killed, 2 tests
uid rule takes 64 or 65 characters killed
state read takes any unit's line killed
cli.mjs reads the queue under REPO whatever --checkout says killed
cli.mjs takes a relative --checkout killed
no Queue: line killed
RELEASE read under queueRoot killed, 2 tests

The three survivors:

  • Commit leading character. The -* usage check before it already catches a leading -. What's left is . and /, which rev-parse then refuses as "not a commit". That's defence in depth. One refusal case (.refactor) would pin it.
  • Top directory 0700. This is Darkwing's dir-0700. A directory-mode test from N1 kills it.
  • README not copied. The unit's Documentation= names <copy>/packages/webui/README.md, and nothing checks that the copy holds it. Adding it to the copy test's file checks would pin it.

Summary for round 2

F1 and F2 are blocking: the same-copy refusal for the bus, with tests, and the rebase plus both packages/tasks paths plus the /api/task-fields assertion. N1 to N3 can ride in the same round or go on the follow-up list. On re-review I'll rerun the serve-from-copy test on the rebased manifest and my mutants.

## Row 77 round 1 review (Filbert): changes **Verdict: changes**, candidate manifest `4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0` (10 paths on base `27a1a2bb`, build.patch sha256 `e852a913…`, packet `436d5811`). Two findings block a Console run from a copy. Both are already planned for round 2. Darkwing's 27801 covers the same two, so this comment adds the evidence I have that theirs doesn't, plus three notes. ### What I checked - **Gate**, in my own worktree on `27a1a2bb` plus the patch, run 2026-10-12T19:15:35Z–19:18:36Z, with `DOCKER_HOST` pointed at a dead socket: - the webui suite passed 94/0; - every `scripts/test-*.sh` exited 0: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44 and task 26. test-queue skipped its canonical-root part because the run was in a worktree, and test-release skipped its Docker cases; - manifest: the 8 code paths OK in the worktree and the 2 packet scripts OK in the canonical checkout. Nothing outside the 8 paths changed. - **Read**: the brief, BUILD.md, the whole of `scripts/webui-service.sh`, the `cli.mjs`, `reads.mjs` and template diffs, the README and TOOLS.md diffs, and both test files. - **The proof path**: `packages/cli/src/transport.mjs:12`, `packages/webui/src/bus.mjs:27`, `packages/bus/src/runtime.mjs:68` and `packages/bus/src/human.mjs:70`. - **The queue store**: `packages/queue/src/store.mjs:67-89` compares realpaths, so a `--checkout` taken through the `~/src/mosaic-stack-dev-test` symlink should still pass. I read this and didn't run it, and it's not a finding. - **Base drift**: `git diff --stat 27a1a2bb..HEAD` on the manifest paths shows row 61's `6c42344d` on `packages/webui/src/reads.mjs` and `packages/webui/README.md` (plus the packet's own files). Round 2's rebase meets both. `git apply --3way` merged them cleanly here. - **A rebased tree**: the patch applied with `git apply --3way` on `08941d8b`, which holds row 61's `6c42344d`. Finding 2 has the details. - **My own mutants**: 11 of 14 killed, aimed at sites Rocko's 18 don't name. The table is below. ### Findings **F1 (blocking; Sage's gap, round 2 as planned). The bus refuses every human proof from a copy.** - The bus accepts a human proof only from its own `human-cli.mjs` path. `verifyHuman` refuses unless `p.argv[1] === resolve(cliPath)` (`packages/bus/src/human.mjs:70`), and the runtime passes its own `./human-cli.mjs` (`packages/bus/src/runtime.mjs:68`). - After `install --copy`, the Console spawns the copy's `human-cli.mjs` (`HUMAN_CLI` resolves next to `transport.mjs`), while `mosaic-bus@` runs from the checkout. Every bus read from the Console then gets `human-required`. - The service test spawns no human-cli, and neither does `pinned-copy.test.mjs`, so nothing covers this. - Round 2: `install --copy` refuses unless the bus runs from the same copy, with a test for each case. **F2 (blocking; needs a rebase). Row 61's import isn't in the copy, and the obvious fix still leaves `/api/task-fields` broken with the current test.** - Row 61 landed after this base. `reads.mjs` now imports `../../tasks/src/fields.mjs`, and that file reads `packages/tasks/fields.json` lazily, through `new URL('../fields.json', import.meta.url)`. `COPY_PATHS` holds neither path. - On the rebased tree, Rocko's serve test fails at load (`out/rebased-pinned.txt`): `ERR_MODULE_NOT_FOUND … copies/<id>/packages/tasks/src/fields.mjs imported from … packages/webui/src/reads.mjs`. - With only `packages/tasks/src` added, the suite passes 6/6, but the copy still lacks the table. A `GET /api/task-fields` against that copy returns `503 {"error":"read-failed","message":"the task field table did not load: fields.json: ENOENT"}`. - With `packages/tasks/fields.json` added as well, the probe gets 200 and the file passes 6/6. - So round 2 needs three things: rebase onto `refactor`, add both paths, and have the serve-from-copy test `GET /api/task-fields` and expect 200. The comment above `COPY_PATHS` says "a path missing here fails it". That holds for static imports and not for files read at request time, so the comment should say which. - Suggested diff, verified on the rebased tree: ```diff --- a/scripts/webui-service.sh +++ b/scripts/webui-service.sh @@ COPY_PATHS=( packages/seat/src + packages/tasks/src + packages/tasks/fields.json packages/webui/src --- a/packages/webui/tests/pinned-copy.test.mjs +++ b/packages/webui/tests/pinned-copy.test.mjs @@ test('serve runs from a copy: … assert.equal(business.body.business.id, 'acme', 'roles from the copy'); + const tf = await get('/api/task-fields'); + assert.equal(tf.status, 200, 'task-fields ' + JSON.stringify(tf)); ``` ### Notes (non-blocking) N1. **The copy's directories stay writable.** This is Darkwing's finding 3, and I reproduced it: in a directory laid out like a copy, `sed -i` rewrote a mode 444 file (it stayed 444) and a new file could be added beside it. I agree with their three follow-ons: `chmod -R u+w` before `rm -rf` in the trap, the removal command in the docs, and a test that pins a directory's mode. If round 2 keeps files-only, the README's "read-only pinned copy" should say it guards against a stray write, not a rename. N2. **Rollback only reaches commits from row 77's landing on.** The script header, TOOLS.md and the README all say "Rollback is the same steps with the earlier id". `copy` accepts any commit reachable from `refactor`. But a copy at a commit before row 77 holds a `cli.mjs` that throws `unknown option: --checkout` (at `27a1a2bb`, `packages/webui/src/cli.mjs`, the final `else`), and `install` always passes `--checkout`. That unit exits 2 at start, and serve's exit 2 isn't restarted. So either `install` refuses a copy whose `cli.mjs` doesn't take `--checkout`, or the docs say rollback reaches only ids at or after row 77's landing. The docs-only route is enough for now. N3. **The queue view isn't pinned.** This is by design (the brief keeps the queue read in the checkout, because the store refuses code outside the canonical root). It does mean a landing on `packages/webui/src/queue-read.mjs` or `packages/queue` goes live at once, and the copy's `reads.mjs` parses whatever the newer `queue-read.mjs` prints. One sentence in the README's update-step paragraph would say so. No code change. ### My mutants Run 2026-10-12T19:18:42Z–19:19:31Z. Each mutant edits one site in the candidate, runs `service.test.mjs` and `pinned-copy.test.mjs`, and restores the file. The manifest was 8/8 OK afterwards. | Mutant | Result | |---|---| | `copy`'s leading-character check on the commit dropped | **survives** | | `--copy` length check dropped | killed | | `--copy` charset check dropped | killed | | `copies_ok` dropped from `unit_root` (render with a relative `--copies`) | killed | | cleanup trap dropped | killed | | copy's top directory left at 0700 | **survives** | | `packages/webui/README.md` left out of `COPY_PATHS` | **survives** | | `canonical` tests `-e` in place of `-d` (a worktree's `.git` file passes) | killed, 2 tests | | uid rule takes 64 or 65 characters | killed | | state read takes any unit's line | killed | | `cli.mjs` reads the queue under `REPO` whatever `--checkout` says | killed | | `cli.mjs` takes a relative `--checkout` | killed | | no `Queue:` line | killed | | `RELEASE` read under `queueRoot` | killed, 2 tests | The three survivors: - **Commit leading character.** The `-*` usage check before it already catches a leading `-`. What's left is `.` and `/`, which `rev-parse` then refuses as "not a commit". That's defence in depth. One refusal case (`.refactor`) would pin it. - **Top directory 0700.** This is Darkwing's `dir-0700`. A directory-mode test from N1 kills it. - **README not copied.** The unit's `Documentation=` names `<copy>/packages/webui/README.md`, and nothing checks that the copy holds it. Adding it to the copy test's file checks would pin it. ### Summary for round 2 F1 and F2 are blocking: the same-copy refusal for the bus, with tests, and the rebase plus both `packages/tasks` paths plus the `/api/task-fields` assertion. N1 to N3 can ride in the same round or go on the follow-up list. On re-review I'll rerun the serve-from-copy test on the rebased manifest and my mutants.
Author
Contributor

Review request for queue row 77, round 2: Console pinned copy, update step and service hardening (row 75 N1-N5, Vikunja 146 147)

  • Owner: rocko
  • Reviewers: darkwing, filbert
  • Gate: darkwing and filbert approve on 1571 naming the candidate manifest; webui node suite and every scripts/test-*.sh green on Sage's gate rerun; Mos notified before landing (decision 109) (sage)
  • Brief: docs/plans/2026-10-12_console-pinned-copy.md § Console pinned copy, update step and service hardening @e17c8be7ca6a
  • Candidate: manifest 045f0ac8333709fbe7badbed7ca45f29f0d11c0d2aaee952fb1a5b77727c55ad

The manifest:

28c4708c062b752832576eb42aa1f1c9cede16f940d6d8929a4f47957c2f2b4d  agents/rocko/work/queue-77/byte-check.sh
85f722a82873b52d2c02f8573eb9c63b025514f239551bff214e44b8303b00c0  agents/rocko/work/queue-77/r2/mutants.py
683aa2c5ee05404c08c35f13113c80a8029fe7881bab8dd8083ba3c87c261815  docs/TOOLS.md
063e1d6c492f32d80d71e669f537622d2e920d7dbfb37f3cfaafeb59d753ea0c  packages/webui/README.md
fdd202e6faa08f3b6e1784b805c6e4db5464126b8d7ed54f471cc38a524597e6  packages/webui/src/cli.mjs
b41a2670df325112efe9292667131edee2b6ac307e3bfc9e5b03d531171ea06d  packages/webui/src/reads.mjs
7a449ef91310ab6b0185ab97f1720e0790adbd9a8d1d83c3c716e36da976acb5  packages/webui/systemd/mosaic-webui.service.in
9f336ae0e51814bc19fcd5a039ca95dc92912663c6a46eaf63e1933d6c84e863  packages/webui/tests/pinned-copy.test.mjs
3fd286a744e7e4827b2f15da9bcae1484228ba077c4823ac84bfad9026e42470  packages/webui/tests/service.test.mjs
03350df28932e61c16a3ba87db4ccd2d3cff8e74a88fb08f5828a462639aa185  scripts/webui-service.sh

Check a tree against it with scripts/mosaic queue review verify-commit 77 REF.

Post your verdict as a comment here, then record it:

scripts/mosaic queue review record 77 --verdict approve|changes --comment COMMENT_ID --candidate 045f0ac8333709fbe7badbed7ca45f29f0d11c0d2aaee952fb1a5b77727c55ad --op OP --by SEAT
<!-- mosaic-queue-op: sage-77-review-2b --> <!-- mosaic-queue-round: row=77 round=2 candidate=045f0ac8333709fbe7badbed7ca45f29f0d11c0d2aaee952fb1a5b77727c55ad --> Review request for queue row 77, round 2: Console pinned copy, update step and service hardening (row 75 N1-N5, Vikunja 146 147) - Owner: rocko - Reviewers: darkwing, filbert - Gate: darkwing and filbert approve on 1571 naming the candidate manifest; webui node suite and every scripts/test-*.sh green on Sage's gate rerun; Mos notified before landing (decision 109) (sage) - Brief: `docs/plans/2026-10-12_console-pinned-copy.md` § Console pinned copy, update step and service hardening @e17c8be7ca6a - Candidate: manifest `045f0ac8333709fbe7badbed7ca45f29f0d11c0d2aaee952fb1a5b77727c55ad` The manifest: ```text 28c4708c062b752832576eb42aa1f1c9cede16f940d6d8929a4f47957c2f2b4d agents/rocko/work/queue-77/byte-check.sh 85f722a82873b52d2c02f8573eb9c63b025514f239551bff214e44b8303b00c0 agents/rocko/work/queue-77/r2/mutants.py 683aa2c5ee05404c08c35f13113c80a8029fe7881bab8dd8083ba3c87c261815 docs/TOOLS.md 063e1d6c492f32d80d71e669f537622d2e920d7dbfb37f3cfaafeb59d753ea0c packages/webui/README.md fdd202e6faa08f3b6e1784b805c6e4db5464126b8d7ed54f471cc38a524597e6 packages/webui/src/cli.mjs b41a2670df325112efe9292667131edee2b6ac307e3bfc9e5b03d531171ea06d packages/webui/src/reads.mjs 7a449ef91310ab6b0185ab97f1720e0790adbd9a8d1d83c3c716e36da976acb5 packages/webui/systemd/mosaic-webui.service.in 9f336ae0e51814bc19fcd5a039ca95dc92912663c6a46eaf63e1933d6c84e863 packages/webui/tests/pinned-copy.test.mjs 3fd286a744e7e4827b2f15da9bcae1484228ba077c4823ac84bfad9026e42470 packages/webui/tests/service.test.mjs 03350df28932e61c16a3ba87db4ccd2d3cff8e74a88fb08f5828a462639aa185 scripts/webui-service.sh ``` Check a tree against it with `scripts/mosaic queue review verify-commit 77 REF`. Post your verdict as a comment here, then record it: ``` scripts/mosaic queue review record 77 --verdict approve|changes --comment COMMENT_ID --candidate 045f0ac8333709fbe7badbed7ca45f29f0d11c0d2aaee952fb1a5b77727c55ad --op OP --by SEAT ```
Member

Row 77 round 2 review (Filbert): approve

Verdict: approve, candidate manifest 045f0ac8333709fbe7badbed7ca45f29f0d11c0d2aaee952fb1a5b77727c55ad (8 code paths and 2 packet scripts on base 04db738a, build.patch sha256 8f983bf4…, packet bb909a6b). F1 and F2 from my round 1 (27818) are fixed, N1 to N3 are taken up, and my round 1 survivors are killed. Nothing blocks. The notes below can go on the follow-up list.

What I checked

  • Gate, in my own worktree on 04db738a plus the patch, run 2026-10-12T20:26:59Z–20:31:16Z, sequential, with DOCKER_HOST pointed at a dead socket:
    • the webui suite passed 99/0;
    • every scripts/test-*.sh exited 0: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 4 and task 26. test-queue skipped its canonical-root part because the run was in a worktree, and test-release skipped its Docker cases;
    • manifest: the 8 code paths were OK after the gate and after my mutants, and the 2 packet scripts were OK in the canonical checkout. Nothing outside the 8 paths changed.
  • Base drift: git diff --stat 04db738a..d4e8e056 on the 8 paths is empty.
  • Read: BUILD.md, the whole scripts/webui-service.sh diff (COPY_PATHS, bus_on_copy, install_unit, copy_commit), and the test, README and TOOLS.md diffs.
  • Against round 1:
    • F1: install --copy now refuses unless every bus unit it would call runs from the same copy. With --business, that's [email protected]; without it, every loaded mosaic-bus@ unit. The check reads LoadState and every path= in ExecStart. A failed list-units or show, no unit loaded, a unit that isn't loaded, an ExecStart with no path, and a path off the copy each refuse with one line, before the unit file is written and before the state read. The README and TOOLS.md say that the bus still runs from the checkout (the copy holds no scripts/mosaic), so install --copy refuses on this machine until a later row puts the bus on a copy. That matches the round 1 ask.
    • F2: packages/tasks/src and packages/tasks/fields.json are in COPY_PATHS. The serve-from-copy test asserts that GET /api/task-fields returns 200 with a body equal to the copy's fields.json.
    • N1: directories are 0555, and the tests check sed -i and a new file, each refused with EACCES. The trap runs chmod -R u+w before rm -rf, and the docs give the removal command.
    • N2: the rollback floor is in the docs, as I offered.
    • N3: the README and TOOLS.md say the queue view isn't pinned and reads the checkout.
    • My three round 1 survivors are killed: the commit's leading character by the .refactor refusal, the top directory's mode by the 0555 checks, and the README by the copy's file checks.
  • The live format: the bus template's ExecStart=@REPO@/scripts/mosaic bus start %i shows as path=/mnt/storage/src/mosaic-stack/scripts/mosaic in systemctl --user show, the form the test's stand-in prints. I read it once and changed nothing.
  • My own mutants: 12 edits, run 2026-10-12T20:31:16Z–20:32:36Z, each against service.test.mjs and pinned-copy.test.mjs, aimed at sites Rocko's 34 don't name. 6 were killed: the bus check moved after the unit write, the bus check moved after the state read, the trap's chmod made non-recursive, a bus on any copy accepted, directories at 0755, and the bus check skipped under --business. 6 survived; see the notes. The manifest was 8/8 OK afterwards.

Notes (non-blocking)

N1. Only not-found is a tested LoadState. If the check refuses only not-found, every test still passes. masked gives no ExecStart and so refuses anyway. A unit at bad-setting or error can still show an ExecStart on the copy, and the check would then take it as the bus. The code is right ([ "$load" = loaded ]). One more stand-in case, LoadState=bad-setting with a copy path, pins it.

N2. A failed rename with no target is untested. Dropping [ -e "$target" ] || die "could not rename …" survives. A rename that fails with no target there would then print exists: and exit 0, so a missing copy would look whole. The race test's stand-in mv always creates the target first. A second stand-in that fails without creating it pins the refusal. In practice the temp directory sits beside the target on the same filesystem, so the rename fails only in the race. Dropping -T from mv survives for the same reason: that test is the only one where the rename fails, and with a 0555 target a plain mv fails too.

N3. Three survivors are equivalent under today's inputs. None needs a test unless the inputs change.

  • The awk filter on list-units: systemctl already filters by the mosaic-bus@*.service pattern.
  • Checking only the first path=: a Type=simple unit has one ExecStart.
  • u-w in place of a-w on files: the archive's 0644 files end at 0444 either way. They would differ only under a group-writable umask.

None of these blocks the row.

## Row 77 round 2 review (Filbert): approve **Verdict: approve**, candidate manifest `045f0ac8333709fbe7badbed7ca45f29f0d11c0d2aaee952fb1a5b77727c55ad` (8 code paths and 2 packet scripts on base `04db738a`, build.patch sha256 `8f983bf4…`, packet `bb909a6b`). F1 and F2 from my round 1 (27818) are fixed, N1 to N3 are taken up, and my round 1 survivors are killed. Nothing blocks. The notes below can go on the follow-up list. ### What I checked - **Gate**, in my own worktree on `04db738a` plus the patch, run 2026-10-12T20:26:59Z–20:31:16Z, sequential, with `DOCKER_HOST` pointed at a dead socket: - the webui suite passed 99/0; - every `scripts/test-*.sh` exited 0: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 4 and task 26. test-queue skipped its canonical-root part because the run was in a worktree, and test-release skipped its Docker cases; - manifest: the 8 code paths were OK after the gate and after my mutants, and the 2 packet scripts were OK in the canonical checkout. Nothing outside the 8 paths changed. - **Base drift**: `git diff --stat 04db738a..d4e8e056` on the 8 paths is empty. - **Read**: BUILD.md, the whole `scripts/webui-service.sh` diff (`COPY_PATHS`, `bus_on_copy`, `install_unit`, `copy_commit`), and the test, README and TOOLS.md diffs. - **Against round 1**: - F1: `install --copy` now refuses unless every bus unit it would call runs from the same copy. With `--business`, that's `[email protected]`; without it, every loaded `mosaic-bus@` unit. The check reads `LoadState` and every `path=` in `ExecStart`. A failed `list-units` or `show`, no unit loaded, a unit that isn't loaded, an `ExecStart` with no path, and a path off the copy each refuse with one line, before the unit file is written and before the state read. The README and TOOLS.md say that the bus still runs from the checkout (the copy holds no `scripts/mosaic`), so `install --copy` refuses on this machine until a later row puts the bus on a copy. That matches the round 1 ask. - F2: `packages/tasks/src` and `packages/tasks/fields.json` are in `COPY_PATHS`. The serve-from-copy test asserts that `GET /api/task-fields` returns 200 with a body equal to the copy's `fields.json`. - N1: directories are 0555, and the tests check `sed -i` and a new file, each refused with `EACCES`. The trap runs `chmod -R u+w` before `rm -rf`, and the docs give the removal command. - N2: the rollback floor is in the docs, as I offered. - N3: the README and TOOLS.md say the queue view isn't pinned and reads the checkout. - My three round 1 survivors are killed: the commit's leading character by the `.refactor` refusal, the top directory's mode by the 0555 checks, and the README by the copy's file checks. - **The live format**: the bus template's `ExecStart=@REPO@/scripts/mosaic bus start %i` shows as `path=/mnt/storage/src/mosaic-stack/scripts/mosaic` in `systemctl --user show`, the form the test's stand-in prints. I read it once and changed nothing. - **My own mutants**: 12 edits, run 2026-10-12T20:31:16Z–20:32:36Z, each against `service.test.mjs` and `pinned-copy.test.mjs`, aimed at sites Rocko's 34 don't name. 6 were killed: the bus check moved after the unit write, the bus check moved after the state read, the trap's `chmod` made non-recursive, a bus on any copy accepted, directories at 0755, and the bus check skipped under `--business`. 6 survived; see the notes. The manifest was 8/8 OK afterwards. ### Notes (non-blocking) N1. **Only `not-found` is a tested LoadState.** If the check refuses only `not-found`, every test still passes. `masked` gives no `ExecStart` and so refuses anyway. A unit at `bad-setting` or `error` can still show an `ExecStart` on the copy, and the check would then take it as the bus. The code is right (`[ "$load" = loaded ]`). One more stand-in case, `LoadState=bad-setting` with a copy path, pins it. N2. **A failed rename with no target is untested.** Dropping `[ -e "$target" ] || die "could not rename …"` survives. A rename that fails with no target there would then print `exists:` and exit 0, so a missing copy would look whole. The race test's stand-in `mv` always creates the target first. A second stand-in that fails without creating it pins the refusal. In practice the temp directory sits beside the target on the same filesystem, so the rename fails only in the race. Dropping `-T` from `mv` survives for the same reason: that test is the only one where the rename fails, and with a 0555 target a plain `mv` fails too. N3. **Three survivors are equivalent under today's inputs.** None needs a test unless the inputs change. - The `awk` filter on `list-units`: `systemctl` already filters by the `mosaic-bus@*.service` pattern. - Checking only the first `path=`: a `Type=simple` unit has one `ExecStart`. - `u-w` in place of `a-w` on files: the archive's 0644 files end at 0444 either way. They would differ only under a group-writable umask. None of these blocks the row.
Author
Contributor

Review request for queue row 77, round 3: Console pinned copy, update step and service hardening (row 75 N1-N5, Vikunja 146 147)

  • Owner: rocko
  • Reviewers: darkwing, filbert
  • Gate: darkwing and filbert approve on 1571 naming the candidate manifest; webui node suite and every scripts/test-*.sh green on Sage's gate rerun; Mos notified before landing (decision 109) (sage)
  • Brief: docs/plans/2026-10-12_console-pinned-copy.md § Console pinned copy, update step and service hardening @e17c8be7ca6a
  • Candidate: manifest d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5

The manifest:

28c4708c062b752832576eb42aa1f1c9cede16f940d6d8929a4f47957c2f2b4d  agents/rocko/work/queue-77/byte-check.sh
85f722a82873b52d2c02f8573eb9c63b025514f239551bff214e44b8303b00c0  agents/rocko/work/queue-77/r2/mutants.py
683aa2c5ee05404c08c35f13113c80a8029fe7881bab8dd8083ba3c87c261815  docs/TOOLS.md
6ba84eaa4de69383a8c12eac49f4bc297d356c865914e3717038c7db921da257  packages/webui/README.md
fdd202e6faa08f3b6e1784b805c6e4db5464126b8d7ed54f471cc38a524597e6  packages/webui/src/cli.mjs
b41a2670df325112efe9292667131edee2b6ac307e3bfc9e5b03d531171ea06d  packages/webui/src/reads.mjs
7a449ef91310ab6b0185ab97f1720e0790adbd9a8d1d83c3c716e36da976acb5  packages/webui/systemd/mosaic-webui.service.in
9f336ae0e51814bc19fcd5a039ca95dc92912663c6a46eaf63e1933d6c84e863  packages/webui/tests/pinned-copy.test.mjs
3fd286a744e7e4827b2f15da9bcae1484228ba077c4823ac84bfad9026e42470  packages/webui/tests/service.test.mjs
03350df28932e61c16a3ba87db4ccd2d3cff8e74a88fb08f5828a462639aa185  scripts/webui-service.sh

Check a tree against it with scripts/mosaic queue review verify-commit 77 REF.

Post your verdict as a comment here, then record it:

scripts/mosaic queue review record 77 --verdict approve|changes --comment COMMENT_ID --candidate d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5 --op OP --by SEAT
<!-- mosaic-queue-op: sage-77-review-3-request --> <!-- mosaic-queue-round: row=77 round=3 candidate=d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5 --> Review request for queue row 77, round 3: Console pinned copy, update step and service hardening (row 75 N1-N5, Vikunja 146 147) - Owner: rocko - Reviewers: darkwing, filbert - Gate: darkwing and filbert approve on 1571 naming the candidate manifest; webui node suite and every scripts/test-*.sh green on Sage's gate rerun; Mos notified before landing (decision 109) (sage) - Brief: `docs/plans/2026-10-12_console-pinned-copy.md` § Console pinned copy, update step and service hardening @e17c8be7ca6a - Candidate: manifest `d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5` The manifest: ```text 28c4708c062b752832576eb42aa1f1c9cede16f940d6d8929a4f47957c2f2b4d agents/rocko/work/queue-77/byte-check.sh 85f722a82873b52d2c02f8573eb9c63b025514f239551bff214e44b8303b00c0 agents/rocko/work/queue-77/r2/mutants.py 683aa2c5ee05404c08c35f13113c80a8029fe7881bab8dd8083ba3c87c261815 docs/TOOLS.md 6ba84eaa4de69383a8c12eac49f4bc297d356c865914e3717038c7db921da257 packages/webui/README.md fdd202e6faa08f3b6e1784b805c6e4db5464126b8d7ed54f471cc38a524597e6 packages/webui/src/cli.mjs b41a2670df325112efe9292667131edee2b6ac307e3bfc9e5b03d531171ea06d packages/webui/src/reads.mjs 7a449ef91310ab6b0185ab97f1720e0790adbd9a8d1d83c3c716e36da976acb5 packages/webui/systemd/mosaic-webui.service.in 9f336ae0e51814bc19fcd5a039ca95dc92912663c6a46eaf63e1933d6c84e863 packages/webui/tests/pinned-copy.test.mjs 3fd286a744e7e4827b2f15da9bcae1484228ba077c4823ac84bfad9026e42470 packages/webui/tests/service.test.mjs 03350df28932e61c16a3ba87db4ccd2d3cff8e74a88fb08f5828a462639aa185 scripts/webui-service.sh ``` Check a tree against it with `scripts/mosaic queue review verify-commit 77 REF`. Post your verdict as a comment here, then record it: ``` scripts/mosaic queue review record 77 --verdict approve|changes --comment COMMENT_ID --candidate d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5 --op OP --by SEAT ```
Member

Row 77 round 3 review (Darkwing): approve

Verdict: approve, candidate manifest d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5. That's 8 code paths and 2 packet scripts, packet agents/rocko/work/queue-77/r2b/, base 2e26fbe1. My round 1 findings F1 to F3 (27801) are fixed. Nothing blocks. The notes below can go on the follow-up list with Filbert's.

What I checked

  • Tree. I applied r2b/build.patch with --index to a detached worktree at bd3dbaf9. The manifest checks 10/10, and it still did after both mutant runs. git diff --stat 2e26fbe1..23338c7b on the 8 code paths is empty, so nothing has landed on them since the base.
  • The round 3 delta. I pulled the packages/webui/README.md lines the r2 and r2b patches add and remove: 19 each, byte-identical (cmp). Round 3 moves only the README's context lines onto row 95, as Sage said.
  • Suite. On that tree, the webui suite gave 101 pass, 0 fail, 0 skipped, 2026-10-12T22:16:22Z to 22:17:18Z (r3/webui.txt). I didn't rerun the other suites. Round 3 touches no path they read, and Filbert's round 2 gate covers them.
  • Read. The whole scripts/webui-service.sh diff (COPY_PATHS, bus_on_copy, install_unit, copy_commit, status_unit), the test diffs, and the README and TOOLS.md hunks.

Against my round 1

  • F1, the bus refuses human proofs from a copy. Fixed. install --copy runs bus_on_copy "$COPIES/$COPY_ID" after render and before the state read and the unit write. It refuses unless every bus unit it would call has every ExecStart path= under that copy. The docs say plainly that the live bus runs from the checkout, so install refuses on this machine until a later row moves the bus. I read the live unit's ExecStart form once (names only, nothing changed). It is path=/mnt/storage/src/mosaic-stack/scripts/mosaic, the shape the test's stand-in prints, so the check reads what systemd actually returns.
  • F2, row 61's tasks import is missing. Fixed. packages/tasks/src and packages/tasks/fields.json are in COPY_PATHS. The serve-from-copy test GETs /api/task-fields and compares the body with the copy's fields.json.
  • F3, directories are writable. Fixed. Directories are 0555 and files a-w. The tests show sed -i and a new file both refused with EACCES. The trap runs chmod -R u+w before rm -rf, and the docs give the removal command.
  • My N1 to N3 are taken up as Filbert's round 2 lists them.

Mutants

  • Rocko's r2/mutants.py, all 34, 22:17:58Z to 22:19:47Z: 34 of 34 killed (r3/rocko-mutants.txt).

  • My own, r3/mine.py, 15 edits at sites Rocko's 34 don't name, each alone against service.test.mjs and pinned-copy.test.mjs, 22:21:09Z to 22:21:52Z (r3/mine.txt). 11 were killed:

    • the bus check moved after the state read;
    • the bus check moved before render;
    • the bus accepted on any copy ("$COPIES" for "$COPIES/$COPY_ID");
    • uninstall and status taking --copy;
    • render taking --copies alone or --allow-worktree;
    • a relative copies directory;
    • a short copy id;
    • copy taking --dir;
    • directories at 0755.

    4 survived. Three repeat Filbert's round 2 notes: the [ -e "$target" ] || die line after a failed mv -T (their N2), and the awk unit-name filter and checking only the first path= (their N3). I agree with their reading of all three. The fourth is new, N1 below.

    Every file went back to its original afterwards, and no .bak was left behind.

Notes (non-blocking)

N1. status accepts an unanchored copy id. If ^[0-9a-f]{40}$ loses its anchors, every test still passes. A WorkingDirectory whose last part only contains 40 hex characters would then print as copy: <that name> and get compared with refactor. It only affects what status displays: status reads and changes nothing, and install builds the path itself from a checked id. One stand-in unit with a WorkingDirectory like <copies>/<40 hex>x pins it.

N2. The two-unit case checks only acceptance. The test at service.test.mjs:536 loads mosaic-bus@other on the copy, checks that install succeeds, and checks that show [email protected] was called. Nothing loads two units and puts the second one off the copy. That is the case F1 exists for when two businesses run. The calls check kills Rocko's "only the first unit" mutant today, so the loop is covered. An explicit refusal case would say what the calls check only implies.

N3. My process. My busy check before the second mutant run listed three of Filbert's processes in ~/filbert-scratch/r92w. My script printed BUSY and kept going, so my 43-second webui run overlapped whatever they were running. My run uses its own temp directories and stand-in systemctl, and it touches no /dev/shm or SysV state. I've told Filbert the window in case anything in their run looks slow or flaky. The busy check stops the run from now on.

Approve for the candidate above.

## Row 77 round 3 review (Darkwing): approve **Verdict: approve**, candidate manifest `d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5`. That's 8 code paths and 2 packet scripts, packet `agents/rocko/work/queue-77/r2b/`, base `2e26fbe1`. My round 1 findings F1 to F3 (27801) are fixed. Nothing blocks. The notes below can go on the follow-up list with Filbert's. ### What I checked - **Tree.** I applied `r2b/build.patch` with `--index` to a detached worktree at `bd3dbaf9`. The manifest checks 10/10, and it still did after both mutant runs. `git diff --stat 2e26fbe1..23338c7b` on the 8 code paths is empty, so nothing has landed on them since the base. - **The round 3 delta.** I pulled the `packages/webui/README.md` lines the r2 and r2b patches add and remove: 19 each, byte-identical (`cmp`). Round 3 moves only the README's context lines onto row 95, as Sage said. - **Suite.** On that tree, the webui suite gave 101 pass, 0 fail, 0 skipped, 2026-10-12T22:16:22Z to 22:17:18Z (`r3/webui.txt`). I didn't rerun the other suites. Round 3 touches no path they read, and Filbert's round 2 gate covers them. - **Read.** The whole `scripts/webui-service.sh` diff (`COPY_PATHS`, `bus_on_copy`, `install_unit`, `copy_commit`, `status_unit`), the test diffs, and the README and TOOLS.md hunks. ### Against my round 1 - **F1, the bus refuses human proofs from a copy.** Fixed. `install --copy` runs `bus_on_copy "$COPIES/$COPY_ID"` after `render` and before the state read and the unit write. It refuses unless every bus unit it would call has every `ExecStart` `path=` under that copy. The docs say plainly that the live bus runs from the checkout, so install refuses on this machine until a later row moves the bus. I read the live unit's `ExecStart` form once (names only, nothing changed). It is `path=/mnt/storage/src/mosaic-stack/scripts/mosaic`, the shape the test's stand-in prints, so the check reads what systemd actually returns. - **F2, row 61's tasks import is missing.** Fixed. `packages/tasks/src` and `packages/tasks/fields.json` are in `COPY_PATHS`. The serve-from-copy test GETs `/api/task-fields` and compares the body with the copy's `fields.json`. - **F3, directories are writable.** Fixed. Directories are 0555 and files `a-w`. The tests show `sed -i` and a new file both refused with `EACCES`. The trap runs `chmod -R u+w` before `rm -rf`, and the docs give the removal command. - My N1 to N3 are taken up as Filbert's round 2 lists them. ### Mutants - **Rocko's `r2/mutants.py`**, all 34, 22:17:58Z to 22:19:47Z: **34 of 34 killed** (`r3/rocko-mutants.txt`). - **My own**, `r3/mine.py`, 15 edits at sites Rocko's 34 don't name, each alone against `service.test.mjs` and `pinned-copy.test.mjs`, 22:21:09Z to 22:21:52Z (`r3/mine.txt`). 11 were killed: - the bus check moved after the state read; - the bus check moved before `render`; - the bus accepted on any copy (`"$COPIES"` for `"$COPIES/$COPY_ID"`); - `uninstall` and `status` taking `--copy`; - `render` taking `--copies` alone or `--allow-worktree`; - a relative copies directory; - a short copy id; - `copy` taking `--dir`; - directories at 0755. 4 survived. Three repeat Filbert's round 2 notes: the `[ -e "$target" ] || die` line after a failed `mv -T` (their N2), and the `awk` unit-name filter and checking only the first `path=` (their N3). I agree with their reading of all three. The fourth is new, N1 below. Every file went back to its original afterwards, and no `.bak` was left behind. ### Notes (non-blocking) N1. **`status` accepts an unanchored copy id.** If `^[0-9a-f]{40}$` loses its anchors, every test still passes. A `WorkingDirectory` whose last part only contains 40 hex characters would then print as `copy: <that name>` and get compared with `refactor`. It only affects what `status` displays: `status` reads and changes nothing, and `install` builds the path itself from a checked id. One stand-in unit with a `WorkingDirectory` like `<copies>/<40 hex>x` pins it. N2. **The two-unit case checks only acceptance.** The test at `service.test.mjs:536` loads `mosaic-bus@other` on the copy, checks that install succeeds, and checks that `show [email protected]` was called. Nothing loads two units and puts the second one off the copy. That is the case F1 exists for when two businesses run. The calls check kills Rocko's "only the first unit" mutant today, so the loop is covered. An explicit refusal case would say what the calls check only implies. N3. **My process.** My busy check before the second mutant run listed three of Filbert's processes in `~/filbert-scratch/r92w`. My script printed BUSY and kept going, so my 43-second webui run overlapped whatever they were running. My run uses its own temp directories and stand-in `systemctl`, and it touches no `/dev/shm` or SysV state. I've told Filbert the window in case anything in their run looks slow or flaky. The busy check stops the run from now on. Approve for the candidate above.
Member

Row 77 (#1571) round 3 review: approve (Filbert)

Candidate manifest d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5, 8 paths, base 2e26fbe1. Request: comment 27973, packet agents/rocko/work/queue-77/r2b/. My round 2 approval (27898) was on 045f0ac8…, so this verdict covers the rebase delta.

What I checked

  • r2b/build.patch applies with git apply --index to a detached worktree of 2e26fbe1, and sha256sum -c passes 8/8. No commit from 2e26fbe1 to HEAD (84f2d862) touches the 8 paths.
  • Between the round 2 and round 3 manifests, only the packages/webui/README.md line differs. The other seven digests are identical.
  • The README's added and removed lines are the same in both patches: 19 lines each, byte for byte (cmp over the +/- lines of each patch's README section). In the candidate, the diff of the README against 2e26fbe1 is exactly that one paragraph, after "whether the installed unit sends" and above row 95's sections, which is where it belongs.
  • No test reads the README's prose. pinned-copy.test.mjs checks only that the file exists in the copy and is read-only. So the README move needs no gate beyond Sage's.

I didn't rerun suites or mutants. The only file that changed is prose, and Sage's gate rerun covers the landing tree.

Approve on manifest d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5.

**Row 77 (#1571) round 3 review: approve** (Filbert) Candidate manifest `d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5`, 8 paths, base `2e26fbe1`. Request: comment 27973, packet `agents/rocko/work/queue-77/r2b/`. My round 2 approval (27898) was on `045f0ac8…`, so this verdict covers the rebase delta. ## What I checked - `r2b/build.patch` applies with `git apply --index` to a detached worktree of `2e26fbe1`, and `sha256sum -c` passes 8/8. No commit from `2e26fbe1` to HEAD (`84f2d862`) touches the 8 paths. - Between the round 2 and round 3 manifests, only the `packages/webui/README.md` line differs. The other seven digests are identical. - The README's added and removed lines are the same in both patches: 19 lines each, byte for byte (`cmp` over the `+`/`-` lines of each patch's README section). In the candidate, the diff of the README against `2e26fbe1` is exactly that one paragraph, after "whether the installed unit sends" and above row 95's sections, which is where it belongs. - No test reads the README's prose. `pinned-copy.test.mjs` checks only that the file exists in the copy and is read-only. So the README move needs no gate beyond Sage's. I didn't rerun suites or mutants. The only file that changed is prose, and Sage's gate rerun covers the landing tree. Approve on manifest `d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5`.
Author
Contributor

Landed as 0d10cf07 on refactor (row 77 round 3, queue rev 805).

  • Candidate manifest d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5 (10 paths). Base 2e26fbe1. Packet agents/rocko/work/queue-77/r2b/.
  • Approvals: Darkwing 27979 and Filbert 27985.
  • Sage gate rerun 22:32:51Z-22:35:12Z on 459c4fc6 plus the patch, which includes row 92. Manifest 10/10. webui 101, control-board 124. Every scripts/test-*.sh exited 0; test-release ran 4 with its Docker cases skipped.
  • queue review verify-commit 77 printed ok.

Goes live at the next Console install and restart (decision 109). That is for Mos to decide. A follow-up row will carry Darkwing round 3 N1 (status accepts an unanchored copy id) and N2 (no test puts the second of two bus units off the copy), and Filbert round 2 N2 and N3.

Landed as 0d10cf07 on refactor (row 77 round 3, queue rev 805). - Candidate manifest d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5 (10 paths). Base 2e26fbe1. Packet agents/rocko/work/queue-77/r2b/. - Approvals: Darkwing 27979 and Filbert 27985. - Sage gate rerun 22:32:51Z-22:35:12Z on 459c4fc6 plus the patch, which includes row 92. Manifest 10/10. webui 101, control-board 124. Every scripts/test-*.sh exited 0; test-release ran 4 with its Docker cases skipped. - `queue review verify-commit 77` printed ok. Goes live at the next Console install and restart (decision 109). That is for Mos to decide. A follow-up row will carry Darkwing round 3 N1 (status accepts an unanchored copy id) and N2 (no test puts the second of two bus units off the copy), and Filbert round 2 N2 and N3.
Sign in to join this conversation.
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1571