Compare commits

...
Author SHA1 Message Date
jason.woltje 6c75887b0c sessions: Sage 10-21 afternoon (rows 249, 197, 222, 66, queue revs 2642-2904), with pending Dewey and Researcher lines 2026-10-21 10:19:14 -05:00
jason.woltje 0636721e60 queue: revs 2642-2904 (rows 234, 235, 249 landed; 212, 213, 218, 221, 231, 247 landed; 228, 233, 238, 240, 195, 245, 248 landed; 197 r8 approve, 215 r1 approve, 66 round 4; Case 2 and installer row notes)
Sweeps other seats' revs: Gizmoduck (234, 235, 249), Duckworth (234, 235, 195, 245, 249 verdicts),
Darkwing (233, 240, 195, 245), Filbert (233, 240, 215 verdicts), Gosalyn (212, 221, 231, 247),
Morgana (213), Researcher (228), Launchpad (238), Herb (197, 215), Webby (197, 212, 213, 218, 221,
228, 231, 238, 247, 248 verdicts). The rest are Sage's notes, landings, moves and review requests.
2026-10-21 10:18:29 -05:00
jason.woltje 7b9737953a deferred: row 248 Webby N1, the grant executor test sleep 30 can outlive its test; Duckworth deferred it in the row 249 review (#1751 c30070) 2026-10-21 09:55:13 -05:00
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
jason.woltje 8ca064158a sessions: Filbert row 215 candidate review r1, approve 2026-10-21 08:43:38 -05:00
jason.woltje 8a04f4aa4a row 248: row 235 follow-up, the grant-executor hold-add and hold-remove stubs stop when their test dir is gone, so no stub outlives its test (#1750)
The row 197 r8 D5 proof stopped on a leftover hold-add stub. G23 and G13 write release, finish and
remove the test dir before the stub polls again, so the stub looped forever on sshd's side. 4 runs
of the file left 7 stubs. Both hold loops now also stop once $D is gone.

Candidate 4d99c178b, approved by Webby in round 1 (rev 2883, webby-0080). Gate on Sage's slot
13:05-13:07Z: the whole file 3 runs at 37/0 with no stub left, bus 596/0, and a mutant that drops
the hold-add check leaves 2 stubs. The late remote add the leak exposed is row 249 (#1751).
2026-10-21 08:16:49 -05:00
jason.woltje bc579f7a84 docs: host run sheet section 4, the apt-get download W: line as root is expected, and the profile's sha256 is checked again at /etc/apparmor.d (mos-1168) 2026-10-21 08:14:11 -05:00
jason.woltje 6de1ebc629 plans: row 235 follow-up brief, the grant undo records a host line it can't prove gone (mos-1166, gizmoduck-0139, duckworth-0091) 2026-10-21 08:11:21 -05:00
jason.woltje 540239b6b7 plans: row 235 follow-up brief, the held grant-executor stubs stop when their test dir is gone 2026-10-21 08:03:55 -05:00
jason.woltje 6d5545d442 row 245: row 195 follow-up, F9 runs node as root only at the pinned root-only path, the header and TOOLS.md list what root runs, F6 refuses .. after claude-<pin>/ (Duckworth N1-N3, #1693)
Darkwing's ef8b4439e (darkwing/row245), Duckworth round 1 APPROVE (rev 2849, comment 30043),
manifest 69bc05f4f706, 5 paths. Slot run sage-1961 (darkwing-0107): singles 23/0, mutants 16/16
killed, N1 and N3 among them. DESIGN.md:336-338 stays unedited, superseded by gosalyn-0087.
Sage gate on 856c1826b plus these 5 paths (12:24-12:35Z): case1-accept.test.mjs alone 23/0,
cli 681/0, every scripts/test-*.sh 429/0.
2026-10-21 07:35:39 -05:00
jason.woltje 856c1826bc row 247: C2-20b persona import follow-up, keys from config.yaml print as JSON strings, the dry run applies C2-08's fence and refuses, privateDir refuses an EEXIST race by name (#1749)
Gosalyn's 6d43618a3 (gosalyn/c2-20b), Webby round 1 APPROVE webby-0076 (rev 2852), manifest 6d3f279f692b,
4 paths. Webby's row 221 notes N1, N2 and N4. Slot runs sage-1969 (red base, the builder's comma
regression) and sage-1971 (14/0, M1-M4 killed). M1/M2 sit at :248 in this commit, not :237 as
BUILD.md says (gosalyn-0111). JSON.stringify leaves DEL, C1, U+2028/9 and bidi raw, a known limit
by sage-1964.
Sage gate on 5c6f73ed7 plus these 4 paths (12:10-12:21Z): the test file alone 14/0, cli 680/0,
every scripts/test-*.sh 429/0. f8276c5eb adds only docs/SESSIONS and docs/case2 sheets no test reads.
2026-10-21 07:22:07 -05:00
18 changed files with 9248 additions and 167 deletions
+8
View File
@@ -151,3 +151,11 @@ every run behind my busy check and Sage's, chained with `&&`.
Summed: 818 node tests passed with 0 failed, and 429 selftests passed with 0 failed. The compose network
was removed afterwards, and the sweep found no process left in the worktree, the output directory or the
TMPDIR. This file's results are a doc-only commit on top of the build, so nothing that ran has changed.
## Correction to deviation 6 (follow-up, 2026-10-21)
Deviation 6 says the node F9 runs is root's own. At 37171f65b the script didn't check that. F9 ran
`--node` as root before F10's path check, so the header's "root itself only reads" was false
(Duckworth's N1, `agents/duckworth/work/row195/REVIEW-m1.md` at 6afcee2a5). A host installed as designed
was safe, because `case2-unit.sh` refused a non-root-owned `--node`. Row 245 carries the fix and its
tests: `agents/darkwing/work/queue-245/BUILD.md`.
+91
View File
@@ -0,0 +1,91 @@
# Row 245 build: Duckworth's N1-N3 on `scripts/case1-accept.sh` (row 195 follow-up)
Duckworth approved row 195's manifest round 1 (duckworth-0088, `agents/duckworth/work/row195/REVIEW-m1.md`
at 6afcee2a5) with three notes that don't block. Row 195's code landed as b65014647, and the notes are in
`docs/plans/DEFERRED.md` (d6cff0cdd). Row 245 carries them: brief `docs/plans/2026-10-20_hub-own-uid.md` §
"Row 195 follow-up" at 62ddc96cc, owner Darkwing, reviewer Duckworth, gate owner Sage (sage-1923).
The branch is `darkwing/row245`, on 62ddc96cc. Its first commit is a cherry-pick of `darkwing/row195-n1`
1e3c4767c, which was written on 37171f65b and never run. The script, test and TOOLS.md blobs are the same
as in 1e3c4767c, and none of those paths changed between 37171f65b and 62ddc96cc.
## What changes
N1. F9 ran `$ROOT$NODE` as root (`case1-accept.sh:298` at 37171f65b) before F10 checked that `--node` is
the pinned path. Duckworth offered two fixes: F9's parse through `AS_USER`, or F10's path check first. I
took the path check, plus one more:
- F9 now runs node only when `--node` equals `/opt/mosaic-stack/node-<--node-pin>/bin/node`.
- Then `not_root_only` walks from that file up to `/`. Each entry must be the running user's (root's on the
host) with no group or other write, the rule `case2-unit.sh` `root_only` applied when it rendered the
units. A symlink fails it, because its mode is 777. The first entry that breaks the rule goes in the
evidence.
I didn't run the parse through `AS_USER`. That would hand a check about a root file to the stack's user,
and a process running as `mosaic` shares that node's uid. Whether it could change what node prints then
turns on the host's ptrace setting, and the root-only check doesn't depend on that. Sage agreed in
sage-1877. Rechecking ownership costs one loop and doesn't depend on
how the host was installed.
N2. The header (:9-15) and the `docs/TOOLS.md` entry now list what root runs: `systemctl show` and
`is-enabled` (F1, P11), `curl` on the loopback healthz (F2), `sudo -l` (P1, P3), `sudo -u jarvis bwrap
--version` and `hostcheck.sh --check` (P9), and node for F9 under the rule above.
N3. F6 matched `--claude` by prefix, so `/opt/mosaic-stack/claude-<pin>/../claude-2.0.9/...` passed the
path check. F6 now fails on any `..` segment, before it runs `--version`. F10 compares for equality, so
it needed no change.
Row 195's BUILD.md gets a correction entry for deviation 6. Row 195's DESIGN.md is unchanged. Its "either
works" wording at `DESIGN.md:336-338` is superseded by gosalyn-0087 (root writes `dev.args` at W10), and
it stays in place: an approved design isn't edited (sage-1877).
## Tests
`packages/cli/tests/case1-accept.test.mjs`:
- The node stub touches `node-as-root` in the state directory when something runs it outside the fake
`sudo` (which sets HOME to the stack's home). `host()` sets the node file's directories and the scratch
root to 0755, so the umask can't fail F9.
- "every check passing" also asserts that root ran the pinned node.
- New: "F9 won't run --node as root off the pin path or with a writable directory above it". Another node
path fails F9 and F10, with no root run. A group-writable `bin/` and a symlinked node each fail F9
alone, with the bad path in the evidence and no root run.
- The F6 test gains a `..` case: a path under `claude-<pin>/` by prefix that names 2.0.9's binary, which
prints the pin. F6 alone fails, with "has a .. segment".
That makes 23 tests. All 23 pass on Sage's slot (results below).
## Mutants
The twelve from row 195 still apply, unchanged. These four are new, each one exact replacement:
| # | Mutant | Expected killer |
|---|---|---|
| 13 | F9 runs any `--node` (the pin check becomes `true`) | F9 won't run (another node) |
| 14 | F9 skips the ownership walk (`bad=""`) | F9 won't run (writable `bin/`, symlink) |
| 15 | `not_root_only` checks only the file, not the directories above it | F9 won't run (writable `bin/`) |
| 16 | F6 accepts a `..` segment | F6 stub, path and `..` |
The brief's gate asks for a mutant for each of N1 and N3. Mutants 13-15 are N1's, and 16 is N3's.
## Slot plan
Same shape as row 195's steps, on this row's tip: the single test file, then the sixteen mutants, then
the gate (the cli suite and every `scripts/test-*.sh`), each behind the busy checks and chained with
`&&`. The scripts are in `/mnt/storage/scratch/darkwing-245-out/`.
## Slot results (grant sage-1961, 2026-10-21)
All three steps ran on 0590a29fc with node v26.8.1, launched from `/`. Both busy checks were CLEAR
before each step.
- Singles, 11:27:46-11:27:59Z: `case1-accept.test.mjs` passed 23, failed 0.
- Mutants, 11:28:10-11:31:20Z: all 16 killed, each with exit 1. The runner restored the script, and
its sha256 875bb5b7 matches the committed blob. Rows 1-12 died as in row 195. 13, 14 and 15 died on
"F9 won't run --node as root off the pin path or with a writable directory above it". 16 died on
"F6 fails on a stub that prints nothing, on a path outside claude-<pin> and on a .. segment".
- Gate, 11:31:20-11:42:51Z: the cli suite passed 664, failed 0, skipped 0. Every `scripts/test-*.sh`
exited 0: auth 15, conductor 17, config 31, discord 78, extension-package 18, foundation 44,
land-work 86, queue 28 (its node part 156/0), release 14, task 98. That's 429 selftests, none failed.
- Afterwards no process had its cwd in the worktree, the out dir or TMPDIR, and the
`darkwing-r245_default` network was gone.
Logs: `/mnt/storage/scratch/darkwing-245-out/` (`step1.log`, `mut.out`, `mut/m1-16.log`, `gate.out`).
+98
View File
@@ -0,0 +1,98 @@
# 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 <host>" 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.
+87
View File
@@ -0,0 +1,87 @@
# Row 247 build notes, C2-20b persona import follow-up (#1749)
Brief: docs/plans/2026-10-20_case2-rows.md "C2-20b persona import follow-up: printed keys, the dry run
fence check and the mkdir race", landed in 335bb3e30. These are Webby's row 221 notes N1, N2 and N4
(webby-0073, REVIEW-r1 a0a00630e on webby/reviews). N3 is in DEFERRED (sage-1941).
## Base
Branch gosalyn/c2-20b from 335bb3e30, which is row 221's landing 300766450 plus the brief.
## What changed
- N1, `scripts/case2-persona-import.mjs`: the per-channel prompt line prints the platform key, with its
fixed `platforms.` or `gateway.platforms.` prefix, and the channel key through `JSON.stringify`. A key
holding a newline prints as `\n` inside one quoted line. The profile name on that line is not config
text: `pairing` already holds it to `PROFILE_ID`.
- N2: the dry run calls `personaTextFault` on each persona after its count line. A fault prints as
`profile P: persona <reason>, so C2-08's fence would refuse it`, where the reason is the fixed string
`personaTextFault` returns, never persona text or a line number. With any fault the run refuses, exit
1, and writes nothing, as stage would. The header comment no longer says a fault is counted, not
refused, and now says a SOUL.md over four times the fence in bytes refuses before any count, which is
what `soul()` already did (Webby's second point under N2).
- N4: `privateDir` catches an EEXIST from its mkdir and refuses with `<path> was made by something else
between the check and mkdir`. Any other mkdir error is thrown as before, since the brief names only
EEXIST.
- Tests, `packages/cli/tests/case2-persona-import.test.mjs`:
- The per-channel prompt test expects the quoted keys.
- New: a chet config.yaml with a newline in the platform key and in the channel key prints one quoted
line, and no output line starts with the forged text.
- The dry run heading test now expects the fence fault line, exit 1 and the refusal, still with
nothing written. Its persona has two heading lines, which the fence refuses.
- New: a persona of 8001 two-byte characters, 16002 bytes and under the 32000 byte cap, makes the dry
run print the over 8000 characters fault, write nothing and exit 1. At exactly 8000 it exits 0.
- New: the mkdir race refuses by name at stage, for the staging directory, and at install, for
`personas/<business>`. Each refused directory is left empty.
- New fixture `packages/cli/tests/fixtures/case2-persona-import/mkdir-race.mjs`, an `--import` preload
like `trace.mjs`. It makes the directory named by `$PERSONA_MKDIR_RACE` just before the script's own
`mkdirSync` of that path, so the script meets a real EEXIST from the kernel, not a thrown stub.
## Known limit of N1
`JSON.stringify` escapes C0 controls, `"` and `\`, but leaves DEL, C1, U+2028, U+2029 and the bidi
controls raw. None of those ends a line on a terminal, so the forged line N1 names can't happen. A bidi
control could still reorder how the rest of that one line displays. The brief asks for `JSON.stringify`,
and Sage kept it in sage-1964 because the keys come from Jason's own Hermes config. Note for Webby: row
231's `quote()` in packages/library covers the wider set, and this row doesn't import it. If you want it,
ask for it in review.
## Mutants
One per fix, plus a second for N1 so each of its two keys has its own. Each replaces one exact string
in the script, runs the test file, then restores the script from a copy and checks its sha256.
| Mutant | Fix | Change |
|---|---|---|
| M1 | N1 | the channel key prints raw |
| M2 | N1 | the platform key prints raw |
| M3 | N2 | the dry run's refusal after a fault is skipped |
| M4 | N4 | privateDir's EEXIST branch never matches, so EEXIST is thrown raw |
## Outside readers
`git grep` over packages/*/tests/* and scripts/test-*.sh finds `case2-persona-import` only in its own
test file and in two files of its fixture tree, README.md and trace.mjs, which name the test file in a
comment. No test pins a hash of the script.
## Tests
Two slot runs, `busy.sh && run.sh` in /mnt/storage/scratch/gosalyn-r247, on the test file alone.
Run 1, grant sage-1969, 11:47:45Z to 11:48:00Z, was red: 14 tests, 12 pass, 2 fail. The fault was in the
script. Adding `JSON.stringify` to the per-channel line dropped the space after the comma, so it printed
`"-1001",29 bytes`. The two per-channel tests caught it. run.sh also went on to the mutants with a red
base, so their counts meant nothing. Sage approved both fixes in sage-1971: the space goes back, and run.sh
now exits 3 before the mutants unless the base is rc=0 with fail 0.
Run 2, grant sage-1971, 11:49:04Z to 11:49:22Z, was green: 14 tests, 14 pass, 0 fail, rc=0.
| Mutant | Line | rc | Failed | Killed by |
|---|---|---|---|---|
| M1 | 237 | 1 | 2 | the per-channel prompt test and the newline key test |
| M2 | 237 | 1 | 2 | the same two |
| M3 | 238 | 1 | 2 | the dry run fence test and the dry run heading test |
| M4 | 192 | 1 | 1 | the mkdir race test |
After the run the script's sha256 was 7031d2c0dc9bd22bc748b364a35b461fc01cb98b795032a2e0ec436f3188d4e4, the
same as the copy taken before it. The /proc scan found no process in the worktree or the scratch dir.
+8
View File
@@ -1411,3 +1411,11 @@ are never rewritten or removed; corrections are new entries.
2026-10-21T10:12:45Z | Darkwing (Opus 5.5, 7525c8e4) | row 245 (#1693) built, slot asked (darkwing-0105) | claim rev 2795; darkwing/row245 0590a29fc pushed, on 62ddc96cc: 5273de0b7 cherry-pick of 1e3c4767c (case1-accept.sh, test, TOOLS.md blobs identical) and 0590a29fc notes to queue-245/BUILD.md (DESIGN.md:336-338 superseded by gosalyn-0087, unedited, sage-1877); slot scripts in /mnt/storage/scratch/darkwing-245-out pinned to 0590a29fc, nothing run; told Sage the untracked queue-195 DESIGN.md was r2 (fcbb4aa9b = 818b79696), nothing lost; flagged 245 and 246 both closing #1693
2026-10-21T11:28:49Z | Sage (Opus 5.5, 1ef1e4f8) | 235 landing gate (bus 596/0, business 200/0, cli 666/0, scripts green) and landing b1fece6f0, #1741 closed 30026; 197 r6 D5 proof stopped at solo-a script-release (Compose run has no --no-build, reproduced in lane env), #1695 30031; 221 landed before 197 r7 (sage-1954): gate 11:07-11:19Z (test 11/0, harness 140/0, cli 677/0, scripts green), 300766450, #1722 closed 30033, revs 2812-2813; C2-20b brief and DEFERRED N3 in 335bb3e30, row 247 #1749 briefed; 197 r7 9c3166915 focused run on my slot (red 9/2, green 11/0, mutant killed), request rev 2819 (30036), proof candidates A 2be6d07fa and B cceb258ff ready; slots Herb sage-1956, Darkwing 245 sage-1961 (sage-1894..1962) | in progress
2026-10-21T11:50:41Z | Darkwing (Opus 5.5, 7525c8e4) | row 245 (#1693) slot green, round 1 in review (rev 2842, request comment 30040) | grant sage-1961 on 0590a29fc: singles 23/0, mutants 16/16 killed (13-15 N1, 16 N3), gate cli 664/0 and every scripts/test-*.sh green (429/0), /proc scan empty; results commit ef8b4439e pushed on darkwing/row245; manifest ~/darkwing-scratch/r245/manifest-ef8b4439e*.sha256, 5 paths; slot handed back (darkwing-0107); Duckworth woken (darkwing-0108)
2026-10-21T13:43:38Z | Filbert (Opus 5.5, 048f3c63) | row 215 C2-04 candidate review r1 (herb-0083/0085, sage-2019) | APPROVE on 8d5695d7 tip 14a03d10f, static, no tests; #1716 c30071, queue rev 2897; N1-N4 non-blocking
2026-10-20T23:34:58Z | Dewey (Opus 5.5, 84fb346c) | #436b design r4 review (megavolt-0055, DESIGN.md 3e66f5acf) | CHANGES: revoke on an open rotation deletes one of two live tokens; N8 refuse a new grant while held; items 1 (sage-1578 b), 2, N7, duckworth N1 settled; verdict design-r4-verdict.md 19fdb59a; dewey-0761 Megavolt, dewey-0762 Sage
2026-10-20T23:35:58Z | Dewey (Opus 5.5, 84fb346c) | #436b design r5 review (megavolt-0057, DESIGN.md 1be535aca) | APPROVE; build carries N9 (settle row skips a revoked rotation, 404 delete is done) and N10 (serviceLeft list); verdict design-r5-verdict.md de52be1b; dewey-0763 Megavolt, dewey-0764 Sage
2026-10-20T23:41:36Z | Dewey (Opus 5.5, 84fb346c) | row 222 askpass vs 199 helper (megavolt-0061, design 7.2 8614ba87c) | accepted (a) http.followRedirects=false and (b) host/user-checked helper, plus exact prompt match, credential.helper reset or mirror config check, tests and mutants listed; dewey-0765 Megavolt, -0766 Gosalyn, -0767 Sage
2026-10-21T05:03:38Z | researcher | row 228 (#1729) round 2 in review (revs 2667, 2668) | round 1 4fa2ed1e3 (Webby 29677) drifted on packages/cli/README.md; rebased onto 9b5362018 as researcher/row228-r2 (3a2b547c3 + 5f2aad593 carrying the N1 trim), pushed; manifest ab40b92f (3 files); slot sage-1720: singles 94/0 (13+10+42+29), mutant M1 killed, restore sha OK; busy-check copy used for the 16-hour launchpad-test-dex container (Sage ok, sage-1729); request failed (no token), Sage posts as jarvis; packet agents/researcher/work/queue-228/r2
2026-10-21T06:47:54Z | researcher | row 228 (#1729) round 3 in review (rev 2702; start rev 2700) | rebase of r2 onto the 234 landing c771a36f4 as researcher/row228-r3 cf18c42b9 (one commit, pushed) with Webby's round 2 N1 (:711 and :718); manifest 82cf98c6 (3 files); slot sage-1796: session.test 13 + host.test 42 = 55/0, manifest OK before and after, busy228.sh copy named in the clear; r2 94/0 and M1 killed stand (seat blobs unchanged); request attempt left in requesting state (lock held by sage-238-review-2b when the outcome rev was written, exit 3; transport said failed pre-send, no token), Sage resolves and posts as jarvis; packet agents/researcher/work/queue-228/r3
2026-10-21T07:29:23Z | researcher | row 228 (#1729) landed as 86249ac55 (pushed 07:25Z by Sage) | patch-id b6ffe31feb0d matches my r3 candidate cf18c42b9 (checked here against origin/refactor); Sage combined gate with 238: seat 165/0, cli 640/0, scripts 585/0; queue note rev 2725, done rev 2727, #1729 closed comment 29966 (sage-1824); q228r2 worktree removed, branches researcher/row228, -r2 and -r3 stay on origin and locally; packets agents/researcher/work/queue-228 r1 r2 r3 uncommitted
2026-10-21T15:19:14Z | Sage (Opus 5.5, 1ef1e4f8) | MVP push 10-21 afternoon: rows 249, 197 proof 3/4, 222 singles, 66 r4d, queue-commit | 249 landed 9de5a57ab (+7b9737953 DEFERRED N1, #1751 closed, Vikunja 445 c442); 197 proof 3 rc=1 on a driver defect, Herb's fix a5b636750 to Webby, proof 4 set up in sage-r197p4; 222 singles 34/34 and 5 mutants killed (sage-2028); 66 round 4 in review (82c830779, c30083/30084); queue revs 2642-2904 committed 0636721e6
+7 -3
View File
@@ -905,9 +905,13 @@ args file (`/etc/mosaic-stack/dev.args` on .53), which must pass
`stack-upgrade.sh`'s rules, say `--profile factory` and name `--business`.
Every command that runs the stack's code runs as
`sudo -u USER -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin` from `/`.
Root only reads systemd, `ss`, `/proc`, the account database and file
metadata. The one write is F3's message and its reply, and it never opens
`claude-session.env`.
Root itself reads `ss`, `/proc`, the account database and file metadata. It
runs `systemctl show` and `is-enabled`, `curl` on the Console's loopback
`/healthz`, `sudo -l`, `sudo -u jarvis bwrap --version`, `hostcheck.sh
--check`, and node to parse `managed-settings.json` for F9. F9 runs node only
when `--node` is the `--node-pin` path, and the file and every directory above
it are root's with no group or other write. The one write is F3's message and
its reply, and it never opens `claude-session.env`.
It prints one line per check, `<id> <path> <mark> <evidence>`, then
`count <mark> N` for each mark and a result line. The functional checks
+6
View File
@@ -106,11 +106,17 @@ apt-get download apparmor-profiles=4.0.1really4.0.1-0ubuntu0.24.04.9
dpkg-deb --fsys-tarfile apparmor-profiles_*.deb | tar -x ./usr/share/apparmor/extra-profiles/bwrap-userns-restrict
echo "11d39094f044f0cda0febb3ad517b830301da6b2ce929664af09ee9e4dd264f9 usr/share/apparmor/extra-profiles/bwrap-userns-restrict" | sha256sum -c -
install -o root -g root -m 0644 usr/share/apparmor/extra-profiles/bwrap-userns-restrict /etc/apparmor.d/bwrap-userns-restrict
echo "11d39094f044f0cda0febb3ad517b830301da6b2ce929664af09ee9e4dd264f9 /etc/apparmor.d/bwrap-userns-restrict" | sha256sum -c -
apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrict
dpkg-statoverride --update --add root mosaic 0750 /usr/bin/bwrap
```
Run as root in a `mktemp -d` dir, `apt-get download` prints a line starting `W: Download is performed
unsandboxed as root`, because the `_apt` user can't write that dir. The line is expected and the
download still succeeds. Each `sha256sum -c -` line must print `OK`. The second one checks the copy in
`/etc/apparmor.d`, the file that loads. Stop on any other result.
`aa-status` then lists `bwrap` and `unpriv_bwrap` in enforce mode. Install the one profile file, never the
whole `apparmor-profiles` package, whose 20 other profiles would load at the next boot. Rollback, in
reverse order: `dpkg-statoverride --remove /usr/bin/bwrap`, then `chown` and `chmod` to the values in
+50
View File
@@ -121,3 +121,53 @@ tells Mos before row b lands.
Gate: as row a, with section 10's row b tests and mutants, G27, G28 and the upsert mutant included, and
the creative refusal tests and mutants (writer and spend).
## Machine grant row b follow-up: the held stubs end with their test
The row 197 r8 D5 proof stopped on a leftover process on 2026-10-21. In
`packages/bus/tests/grant-executor.test.mjs`, the `add` stub waits for `release` when `hold-add`
is set. G23 ("a revoke during install") and G13 write `release`, finish, and `t.after` removes the
test dir. The stub polls every 50 ms, so it usually misses `release` and then loops forever on a
dir that is gone. Four runs of the file on Sage's slot left 7 stubs, every run 37/0.
Build: each hold loop in that file, `hold-add` and `hold-remove`, also stops when `$D` no longer
exists. The test file is the only change. Nothing under `src` changes.
Gate: Webby approves the commit. On Sage's gate the whole file passes three times in a row with no
stub left, checked by pgrep on the run's TMPDIR, and the bus suite is green. A mutant that drops the
dir check from `hold-add` leaves a stub behind.
## Machine grant row b follow-up: the undo can't prove the remote add has ended
Mos asked this in mos-1166. Gizmoduck answered in gizmoduck-0137 and gizmoduck-0139, and Duckworth
checked the answer in duckworth-0091. Line numbers are at 6d5545d44.
An abort during install SIGKILLs the local ssh process group (host-run.mjs:136). A timeout does the
same (host-run.mjs:131-134). sshd gives a non-pty command no SIGHUP when the client dies, so the
remote `add` can keep running. The undo runs `remove <user> <tag>` once (broker.mjs:1017). If the add
writes its line after that, the remove has already exited 0 and `hostLineLeft` stays unset (:1019).
Then hosts.status shows the host clean (:1396), reconcile skips a grant that already ended
(:1042), and nothing prompts a sweep. hosts.remove's host-line-left check (:1362) passes, and once
the record is gone a sweep refuses host-unknown (:1350), so no verb reaches the line. The line
authorizes a key nobody holds, under the installer's restrict prefix. No test covers this case. G24
seeds its failed grant with `hostLineLeft` already set.
Build:
- In `#runHostGrant`, mark install done right after `await install(...)` returns. In the catch, when
install started and never returned, record `hostLineLeft: tag` whatever the remove's exit was. The
rule keys on install not returning. It covers an abort, a timeout and a dropped connection, and
doesn't depend on `signal.aborted`. The message names `mosaic hosts sweep <host>`.
- An add that exits nonzero on the host also records the hint. That hint is false but harmless,
since a sweep that doesn't list the tag drops it (G24, "a left tag no longer on the host").
- Fix the comment at grant-executor.test.mjs:432, which says a killed session's add exits.
- Row a's enroll add: check it for the same race, then fix it or record in BUILD.md why it can't
happen.
- Owner Gizmoduck. Reviewer Duckworth.
Tests: one abort case and one timeout case. Each uses a held stub add that writes after the remove
and exits 0. Each asserts `hostLineLeft` on grant.failed and in hosts.status, that hosts.remove
refuses host-line-left before the sweep, that the sweep removes the line, and that hosts.remove then
succeeds. A mutant that drops the new catch branch fails a test.
Gate: Duckworth approves the manifest. On Sage's gate the bus suite and the whole touched test files
are green, and the mutant is killed.
+11
View File
@@ -322,6 +322,17 @@ at every gate. Started 2026-09-12 during the control board MVP.
base. This matters only if someone reruns that set. Fix the anchor
in that tooling before any rerun, and check that a stale anchor
refuses instead of skipping the mutant (sage-1941). (2026-10-21)
- **Grant executor test X5 starts a `sleep 30` that can outlive its test.**
(Webby N1 on row 248 r1, webby-0080; deferred by Duckworth in the row
249 review, REVIEW-r1 28174fe08, #1751 comment 30070.) The test at
`packages/bus/tests/grant-executor.test.mjs:989` (:934 before row
249) runs `sleep 30` on the sshd side, where an abort doesn't reach it. Fix it with a stub
that exits once its test dir is gone, as row 248 did for the hold
stubs, or with a shorter sleep. This binds the next row that touches
that test file. Until then, a lane cleanup that runs after this file
waits 30 s before it calls a lone `sleep 30` a leak. The row 197
lanes scan only at lane end, about 15 minutes after pkg-bus, so they
can't see it (mos-1169). (2026-10-21)
## Queue
+25 -18
View File
@@ -33,7 +33,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
<!-- mosaic-queue:begin -->
Generated from `docs/plans/queue.json` revision 2641 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
Generated from `docs/plans/queue.json` revision 2904 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|---|---|---|---|---|---|---|---|---|
@@ -102,7 +102,7 @@ Generated from `docs/plans/queue.json` revision 2641 by `queue render`. Do not e
| 63 | Multi-project verbs: task verbs across the tree | darkwing; reviewers dewey, filbert | #1556 | done | 62 done | dewey and filbert approve on the issue naming the candidate manifest; tasks, harness and every scripts/test-*.sh green on Sage's gate rerun (sage) | `docs/plans/2026-10-11_multi-project-sync.md` § Multi-project verbs: task verbs across the tree (row B) | lands before row 90 (decision 125): both share packages/tasks/README.md and tests/verbs.test.mjs, so row 90 rebases after this lands |
| 64 | Managed sessions get their role's skills on both harnesses | sage; reviewers darkwing, filbert | #1557 | done | 65 done | darkwing and filbert approve on #1557 naming the candidate manifest; business, harness, cli and every test-*.sh green on Sage's gate rerun; Sage's landing session on each harness shows the role's skills (sage) | `docs/plans/2026-10-11_managed-session-config.md` § Managed sessions get their role's skills on both harnesses | Landed (mos-0863/0864): 9f0039711 = c6d7d538f (range-diff =, patch-id 7e1948e2f2e9), F1 doc 6b6567b8b, packet ebfc23c43; gate-land GATE_PASSED 2384/0 on 6b6567b8b. D2 on #1557 comment 29228: claude-code coder rc13bcd555652 replied with its 4 skills, pi coder r3455505dd5da launch only (no pi credential on host, to Mos); both manifests list the 4 coder skills, sha256 = landed files. D3 window step 46de60893, D4 #1672 row 174. |
| 65 | Model routing table: sessions resolve harness, model and thinking from one table | sage; reviewers darkwing, dewey | #1558 | done | 49 done, 131 done | Mos clears the routing table's public/ diff (decision 123, mos-0840) before in-review; darkwing and dewey approve on #1558; plugins, bus, business, harness, seat, cli, webui, test-*.sh green on Sage's rerun; landing session shows module, route, counted model, thinking; report names the mos-0840 view (sage) | `docs/plans/2026-10-11_managed-session-config.md` § Model routing table: sessions resolve harness, model and thinking from one table | Decision 94 hold ended (cutover live 17:21Z); restarted under decision 111 and Mos sage-0091. |
| 66 | Managed sessions run the unslop check on both harnesses | sage; reviewers dewey, filbert | #1559 | in-review, round 3, request posted | 64 done | dewey and filbert approve on #1559 naming the candidate manifest; unslop, harness, cli and every test-*.sh green on Sage's gate rerun; Sage's landing session on each harness shows the notice, the correction and the counts (sage) | `docs/plans/2026-10-11_managed-session-config.md` § Managed sessions run the unslop check on both harnesses | Decision 94 hold ended (cutover live 17:21Z); restarted under decision 111 and Mos sage-0091. |
| 66 | Managed sessions run the unslop check on both harnesses | sage; reviewers dewey, filbert | #1559 | in-review, round 4, request posted | 64 done | dewey and filbert approve on #1559 naming the candidate manifest; unslop, harness, cli and every test-*.sh green on Sage's gate rerun; Sage's landing session on each harness shows the notice, the correction and the counts (sage) | `docs/plans/2026-10-11_managed-session-config.md` § Managed sessions run the unslop check on both harnesses | Drift: row 245 (6d5545d44) changed docs/TOOLS.md, which is in this manifest. r4c must take 245's TOOLS.md hunks before its gate and new manifest; the approved TOOLS.md blob is now stale (sage-1992 sweep). |
| 67 | Decisions read follow-up: seen-then-resolve, list-before-503 and full-row tests | filbert; reviewers darkwing, dewey | #1560 | done | — | darkwing and dewey approve on #1560 naming the candidate manifest; bus, webui and every scripts/test-*.sh green on Sage's gate rerun (sage) | `docs/plans/2026-10-11_decisions-read-follow-up.md` § Decisions read follow-up: seen-then-resolve, list-before-503 and full-row tests | — |
| 68 | Bus host runs an explicit Claude Code binary | sage; reviewers dewey, filbert | #1561 | done | — | Filbert and Dewey approve; cli, seat, harness suites and every scripts/test-*.sh green on Sage's gate rerun; unit reinstalled and opus probe through MOSAIC_CLAUDE_BIN (sage) | `docs/plans/2026-10-11_bus-claude-binary.md` § Bus host runs an explicit Claude Code binary | — |
| 69 | Terminal app follow-up: narrow-terminal guard test, hidden-box way out and width tests | darkwing; reviewers dewey, filbert | #1562 | done | 55 done | dewey and filbert approve on the issue naming the candidate manifest; cli and queue node suites plus every scripts/test-*.sh green on Sage's gate rerun (sage) | `docs/plans/2026-10-11_terminal-app-follow-up.md` § Terminal app follow-up: narrow-terminal guard test, hidden-box way out and width tests | Sage accepts the two shortened texts in round 1 (hidden box way out, status line), both forced by the 40x10 frame. Follow-up at landing: when help is cut to fit, draw() points at mosaic inbox --json though help has no whole text; open it as its own row (build.md Found, not fixed) |
@@ -231,9 +231,9 @@ Generated from `docs/plans/queue.json` revision 2641 by `queue render`. Do not e
| 192 | Q175 phase 1: the Console verifies SSO itself (--sso), OIDC RP with oauth4webapi 3.8.8, no live change (MVP T1.2, #412) | launchpad; reviewers webby | #1690 | done | — | Webby approves the manifest; webui and cli suites, scripts/test-config.sh and every scripts/test-*.sh green on Sage's gate; design section 9 mutants killed (sage) | `docs/plans/2026-10-20_sso-phase1.md` § Brief: Q175 phase 1, the Console verifies SSO itself (--sso) | Landed 4429a104f on refactor (squash of launchpad/row192 53bb59462..37582e820, round 2 manifest 6f8cee14...7787). patch-id 4a4869026 matches the reviewed diff on all 18 paths. Gate on 088087ac9: package suites 2758/0, scripts 429/0, mutants 22 of 23 killed (#23 equivalent, sage-1407). npm ci in canonical, oauth4webapi 3.8.8. Live re-pin is a window with Mos (mos-1099 item 3). |
| 193 | many humans per business, human roles, private and shared threads, one access.json store (MVP T1.3, Vikunja #414/#415) | morgana; reviewers duckworth | #1691 | in-progress | — | Duckworth approves DESIGN.md then the manifest; bus, business, cli and webui suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users.md` § Brief: many humans per business, human roles, private and shared threads | — |
| 194 | agent lifecycle from the Console and the CLI (create, launch, terminate, regenerate, delete) with a per-agent hire/fire veto (MVP T1.5, Vikunja #414/#415) | honker; reviewers webby | #1692 | in-progress | — | Webby approves DESIGN.md then the manifest; bus, cli, business and webui suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_agent-lifecycle.md` § Brief: agent lifecycle from the Console and the CLI, with a per-agent hire/fire veto | sage-1610 (honker-0049): a human add takes tracker {bot, botId}, required for a vikunja definition, refused otherwise, checked by checkBot and bot uniqueness; CLI and API only. Delete accepts a tracker bot when all credentials are store refs, and its result names the bot for removal by hand in Vikunja. No Vikunja account is created (Q188 untouched). Mutants: drop the requirement, refuse the bot on delete, skip checkBot on add. |
| 195 | row 195: the Case 1 install on 10.1.1.53 as mosaic under /srv/mosaic, reusing C2-13's scripts by input under row 190's canon (Q181, Q182/Q183, Q185; ops-01 root steps under #423 step 1) | darkwing; reviewers duckworth | #1693 | briefed | 190 done, 233 done | Duckworth approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_hub-own-uid.md` § Brief: the Case 1 install on 10.1.1.53 (row 195) | sage-1439: rewritten to Case 1 on .53 (f24f2cd40). Chains on C2-13's candidate too once C2-13 is queued. Darkwing starts DESIGN.md after row 190's mutants. |
| 195 | row 195: the Case 1 install on 10.1.1.53 as mosaic under /srv/mosaic, reusing C2-13's scripts by input under row 190's canon (Q181, Q182/Q183, Q185; ops-01 root steps under #423 step 1) | darkwing; reviewers duckworth | #1693 | done | 190 done, 226 done, 233 done | Duckworth approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_hub-own-uid.md` § Brief: the Case 1 install on 10.1.1.53 (row 195) | LANDED b65014647 (pushed 361ba2387 tip). Patch-id 656e43ff25de = 4ced5e63d..37171f65b; verify-commit ok, 5 paths. Combined gate with 213 on 38b83ea5c+paths 09:45:52-09:57:52Z: business 200/0 bus 542/0 harness 140/0 cli 663/0, all 10 scripts green, L1 killed. N1-N3 to DEFERRED next; follow-up darkwing/row195-n1 1e3c4767c. |
| 196 | the human boundary across uids and humans: Console, tester listener, broker.sock and the path-bound human proof (P19 333, before federation R3, mos-0989) | gander; reviewers duckworth | #1694 | in-progress | — | Duckworth approves DESIGN.md then the manifest; bus, webui and cli suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-boundary.md` § Brief: the human boundary across uids and humans (P19 333) | — |
| 197 | a second gate lane: two full gates at once on dragon-lin, design first (mos-0977 item 6) | herb; reviewers webby | #1695 | in-review, round 1, request posted | — | Webby approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate, singly and in the two-lane proof (sage) | `docs/plans/2026-10-20_second-gate-lane.md` § Brief: a second gate lane, two full gates at once on dragon-lin | Order with row 198 (Sage, mos-1040 N1): 197 lands first. After B1 the shared tests are packages/webui/tests/serve.test.mjs:100 and preview.test.mjs:208. Row 198 rebases onto 197's landing before its manifest review. |
| 197 | a second gate lane: two full gates at once on dragon-lin, design first (mos-0977 item 6) | herb; reviewers webby | #1695 | in-review, round 6, request posted | — | Webby approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate, singly and in the two-lane proof (sage) | `docs/plans/2026-10-20_second-gate-lane.md` § Brief: a second gate lane, two full gates at once on dragon-lin | r7 APPROVED (webby-0074 rev 2822). r7 proof sage-1973 11:50-11:51Z STOPPED at solo-a pkg-cli (#1695 c30044): gate-lane-image.test.mjs:93 and :103 call docker via PATH, which in a lane is the guard shim; composeFixture env drops MOSAIC_GATE_DOCKER_RECORD, shim exits 2. business 200/0, bus 596/0, cli 197/2. r8 Herb: test-only fix + run under a lane-shaped PATH. N10 still open. |
| 198 | retire the control board writer on 7331 (unauthenticated loopback writer, Q175; mos-1008) | bushroot; reviewers webby | #1696 | in-progress | 192 done, 197 done | Webby approves INVENTORY.md then the manifest; control-board, webui and cli suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_retire-control-board.md` § Brief: retire the control board writer on 7331 | Order with row 197 (Sage, mos-1040 N1): 197 lands first. Row 198 rebases onto 197's landing before its manifest review; shared tests are packages/webui/tests/serve.test.mjs:100 and preview.test.mjs:208. |
| 199 | MVP T1 item 6: a credential store per agent, own git identity, pushes to a remote (G11, G13; mos-0974) | megavolt; reviewers duckworth | #1697 | done | — | Duckworth approves DESIGN.md; Mos rules on the authority model; Duckworth approves the manifest; bus, business, cli and tasks suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: a credential store per agent, own git identity, pushes to a remote | Landed cb5fdb064 on refactor (base 76134dfde), pushed. Patch-id 828fa21a matches git diff 00d4cc1f6..86963d281 on the 37 manifest paths; docs/TOOLS.md 3-way with row 200, hunk unchanged. Manifest 35f900fd, Duckworth approve r1 29779. Gate on 9e1085816+patch: packages 2735/0, scripts 429/0; landed blobs equal the gate tree. |
| 200 | stack-release.sh activate --sso for the Q175 phase 2 window (row 192 F3; chained on 192) | launchpad; reviewers webby | #1698 | done | — | Webby approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_stack-release-sso.md` § Brief: stack-release.sh activates a Console with --sso (Q175 phase 2 prerequisite) | Landed 9e1085816 on refactor (base 722136772), pushed. Patch-id 7ebfa6a5 matches git diff 00d4cc1f6..9df40878f on the 3 manifest paths. Manifest b3ead55c, Webby approve r3 29772. Gate on 9df40878f: packages 2675/0, scripts 429/0. Mutants 13/14, M12 ruled equivalent (sage-1446). |
@@ -248,37 +248,44 @@ Generated from `docs/plans/queue.json` revision 2641 by `queue render`. Do not e
| 209 | FED R8: the hub window on 10.1.1.54, a recorded acceptance (#414, r4.11 b93f098f0; live window run by Jason and Mos, after the Case 2 install #416, Bean and Chet live and both Hermes gateways stopped) | sage; reviewers duckworth | — | queued | — | Every entry check's output is in the R8 record with who ran it; Jason signs in to the hub and sends one message, Melanie revokes, his next request is refused; Duckworth checks the record (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R8, the hub window on 10.1.1.54 | Stays queued until its entry gates near: Case 2 install on .54 (#416, by 2026-10-27), Bean and Chet live, both Hermes gateways on .54 stopped, R7, #412 phase 2, #419 answer. Filed per mos-1073 after duckworth-0034 (Sage) |
| 210 | Agent host access with sudo through the credential store: grant per agent and host, the stack does ssh and sudo, the agent never sees the key (#1707, Vikunja #424, Q183/Q184, due 10-27; chained on row 199's candidate) | gizmoduck; reviewers duckworth | #1707 | done | — | Duckworth approves DESIGN.md, then the manifest; bus, business, cli suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_host-credentials.md` § Brief: agent host access with sudo through the credential store (#424) | Landed f5753c0e8 on refactor (base 9d165be8c), pushed. Patch-id 723b1c83 matches git diff ad95c888c..0485384e9 on the 23 manifest paths; landed files pass r5 manifest 688d38b9. Duckworth approve r2 rev 2592, r3 29851. Mutants 52/53, M22b equivalent (rev 2584). Gate on ad95c888c+patch: packages 2796/0, scripts 429/0; landed blobs equal the gate tree. |
| 211 | Force-push guard: scripts/git-hooks/pre-push refuses non-fast-forward and delete pushes from every worktree, and scripts/slot-busy.sh exits 1 on BUSY (#1709, Vikunja #426, mos-1078/1083) | rocko; reviewers dewey | #1709 | briefed | — | Dewey approves the manifest; the new or changed scripts/test-*.sh and every other scripts/test-*.sh green on Sage's gate; the mutants killed; hook installed in the canonical checkout with a scripted refused force-push (sage) | `docs/plans/2026-10-20_force-push-guard.md` § Brief: a pre-push hook that refuses force-pushes, and a busy check that fails on BUSY (#426) | — |
| 212 | Case 2 C2-01 packages/library: mapping, scope, reader, resolver, R-rules, renderItems, survey | gosalyn; reviewers webby | #1713 | in-review, round 1, request posted | — | Webby approves the manifest. The library tests and the cli suite are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-01 packages/library: mapping, scope, reader, resolver, R-rules, renderItems, survey | — |
| 213 | Case 2 C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file | morgana; reviewers webby | #1714 | in-review, round 2, request posted | — | Webby approves the manifest. The business, bus and harness suites, `scripts/test-config.sh` and every `scripts/test-*.sh` are green on Sage's gate, and the `models` mutant is killed. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file | — |
| 212 | Case 2 C2-01 packages/library: mapping, scope, reader, resolver, R-rules, renderItems, survey | gosalyn; reviewers webby | #1713 | done | — | Webby approves the manifest. The library tests and the cli suite are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-01 packages/library: mapping, scope, reader, resolver, R-rules, renderItems, survey | Landed ee7b2f137 (pushed 09:13Z) on f6c70f944, alone per mos-1162: the 21 manifest paths from a2c72ac95. Patch-id ea84d3e35470 equals the candidate's; blobs and modes match; verify-commit ok (21 paths). No 212 path changed after c771a36f4. Gate: Sage's landing gate 08:51-09:03Z on 4ced5e63d plus 212's and 197's paths, GATE_PASSED, node 981/0, selftests 429/0. Gosalyn r3 slot 451/0, test-queue 28/0. Row 197 waits for r6 (lane proof stopped, sage-1881). |
| 213 | Case 2 C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file | morgana; reviewers webby | #1714 | done | — | Webby approves the manifest. The business, bus and harness suites, `scripts/test-config.sh` and every `scripts/test-*.sh` are green on Sage's gate, and the `models` mutant is killed. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file | LANDED 361ba2387 (39 paths incl. 4 packet files). Patch-id f313e69b6f7e = c771a36f4..2b3e67e36; verify-commit ok, 35 manifest paths. Same combined gate as 195: business 200/0 bus 542/0 harness 140/0 cli 663/0, test-config 31/0 and all 10 scripts green, models mutant L1 killed. Webby N2 is row 235 acceptance (mos-1163). |
| 214 | Case 2 C2-11a editor wikilink node, after measuring whether [[ round-trips | rocko; reviewers filbert | #1715 | briefed | — | Filbert approves the manifest. The webui suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-11a editor wikilink node, after measuring whether [[ round-trips | — |
| 215 | Case 2 C2-04 the library entry in the business file, consumer skips, M9, mirror, library.file and library.status | herb; reviewers filbert | #1716 | briefed | — | Filbert approves the manifest. The business, seat, cli, project, bus and webui suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-04 the library entry in the business file, consumer skips, M9, mirror, library.file and library.status | — |
| 215 | Case 2 C2-04 the library entry in the business file, consumer skips, M9, mirror, library.file and library.status | herb; reviewers filbert | #1716 | in-review, round 1, request posted | — | Filbert approves the manifest. The business, seat, cli, project, bus and webui suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-04 the library entry in the business file, consumer skips, M9, mirror, library.file and library.status | Sage 15:01Z drift sweep after 249 landed 9de5a57ab (+7b9737953): manifest 8d5695d7 shares packages/bus/src/broker.mjs with 249, merge-tree was clean. Order stays 197 then 215 (sage-2029, sage-2030): after 197 lands Herb rebases 215 on that tip for a merge-scoped round with Filbert (broker.mjs from 249, stack-release.test.mjs from 197), then Sage's full landing gate. Rows 13, 66, 197, 202 have no overlap. |
| 216 | Case 2 C2-05a lookup index and queries | researcher; reviewers filbert | #1717 | briefed | — | Filbert approves the manifest. The library suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-05a lookup index and queries | — |
| 217 | Case 2 C2-06 the cited-reply guard, the page store and the three notices | launchpad; reviewers webby | #1718 | briefed | — | Webby approves the manifest. The library, bus and harness suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-06 the cited-reply guard, the page store and the three notices | — |
| 218 | Case 2 C2-12 the setup sheet for Melanie and the survey run sheet | sage; reviewers webby | #1719 | briefed | — | Doc only. Webby approves it against C2-01's real flags. Before landing, grep `packages/*/tests` and `scripts/test-*.sh` to confirm no test reads either file. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-12 the setup sheet for Melanie and the survey run sheet | — |
| 218 | Case 2 C2-12 the setup sheet for Melanie and the survey run sheet | sage; reviewers webby | #1719 | done | — | Doc only. Webby approves it against C2-01's real flags. Before landing, grep `packages/*/tests` and `scripts/test-*.sh` to confirm no test reads either file. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-12 the setup sheet for Melanie and the survey run sheet | LANDED f8276c5eb (pushed, on Darkwing sessions b65f3a0e8). Webby APPROVE r1-r3 (revs 2848, 2856, 2860; r3 candidate 1a38baf5af2a = 89ba79d94). Patch-id 18ba1e40ad29 seat 5c6f73ed7..89ba79d94 equals landed b65f3a0e8..f8276c5eb; three blobs equal 89ba79d94. Gate: doc only, 0 test readers (control 3). Melanie handover waits on C2-06 and Jason (#419). |
| 219 | Case 2 C2-09a one thread per creative instance | rocko; reviewers dewey | #1720 | briefed | — | Dewey approves the manifest. The bus, cli and webui suites are green on Sage's gate, along with every `scripts/test-*.sh`. Watch for hash pins on `bus.js` and `broker.mjs` (`packages/bus/tests/channel-proof.test.mjs` pins a webui file). (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-09a one thread per creative instance | — |
| 220 | Case 2 C2-10a no raw session view (M7) and no raw Discord engine (M8) | researcher; reviewers filbert | #1721 | briefed | — | Filbert approves the manifest. The conversation and discord suites and `scripts/test-discord.sh` are green on Sage's gate, along with every `scripts/test-*.sh`. No test reaches a Discord server. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10a no raw session view (M7) and no raw Discord engine (M8) | — |
| 221 | Case 2 C2-20 Bean and Chet persona import, with a count-only dry run | gosalyn; reviewers webby | #1722 | briefed | — | Webby approves the manifest. The test file runs green on its own and the cli suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-20 Bean and Chet persona import, with a count-only dry run | — |
| 222 | Case 2 C2-03a fetch-only sync | megavolt; reviewers dewey | #1723 | in-progress | — | Dewey approves the manifest. The library and bus suites are green on Sage's gate, along with every `scripts/test-*.sh`, and the mutant is killed. A search of the test run's output for the fixture token finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-03a fetch-only sync | — |
| 221 | Case 2 C2-20 Bean and Chet persona import, with a count-only dry run | gosalyn; reviewers webby | #1722 | done | — | Webby approves the manifest. The test file runs green on its own and the cli suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-20 Bean and Chet persona import, with a count-only dry run | LANDED 300766450, pushed. Cand 8f403d0d4, manifest 20f05e3a6ad5, 22 paths, verify-commit ok, patch-id dfbb754c6fb4 equals 62ddc96cc..8f403d0d4. Gate 11:07:50-11:19Z on b1fece6f0: test file alone 11/0, harness 140/0, cli 677/0, all scripts green. Brief Gate met: webby-0073 approve, test file alone, cli, scripts. No live run in the Gate; the import itself waits on Jason. N1 N2 N4 go to C2-20b, N3 to DEFERRED. |
| 222 | Case 2 C2-03a fetch-only sync | megavolt; reviewers dewey | #1723 | in-progress | — | Dewey approves the manifest. The library and bus suites are green on Sage's gate, along with every `scripts/test-*.sh`, and the mutant is killed. A search of the test run's output for the fixture token finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-03a fetch-only sync | sage-2024 on megavolt-0089: core.createObject=rename in the fetch argv is accepted as a brief departure, since C2-04 inspectMirror refuses nlink 2. Dewey checks it. The askpass env keys, export SAFE in project/src/git.mjs, bus/tests/library-sync.test.mjs and the SURVEY-RUN user line are accepted. The 5 s client timeout residual goes in BUILD.md and Dewey rules fix or defer. Slot after 197 proof 3 and the 249 gate. |
| 223 | Case 2 C2-07 runner retry on a guard refusal, and the runner's notices | researcher; reviewers filbert | #1724 | briefed | — | Filbert approves the manifest. The harness suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-07 runner retry on a guard refusal, and the runner's notices | — |
| 224 | Case 2 C2-05b broker lookup verbs and typed tools | rocko; reviewers dewey | #1725 | briefed | — | Dewey approves the manifest. The bus and harness suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-05b broker lookup verbs and typed tools | — |
| 225 | Case 2 C2-10b Console chat: cited items and the status line | researcher; reviewers dewey | #1726 | briefed | — | Dewey approves the manifest. The webui suite is green on Sage's gate, along with every `scripts/test-*.sh`. Grep for hash pins on `bus.js` and `serve.mjs` before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10b Console chat: cited items and the status line | — |
| 226 | Case 2 C2-13 install script and precheck for the host | gosalyn; reviewers webby | #1727 | briefed | — | Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every `scripts/test-*.sh`. The run on the host is a separate window with Jason's go. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-13b install script and library precheck | — |
| 226 | Case 2 C2-13 install script and precheck for the host | gosalyn; reviewers webby | #1727 | briefed | 215 done, 221 done, 222 done, 230 done | Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every `scripts/test-*.sh`. The run on the host is a separate window with Jason's go. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-13b install script and library precheck | — |
| 227 | Case 2 C2-14 end-to-end acceptance on a scratch root | launchpad; reviewers dewey | #1728 | briefed | — | Dewey runs the script and approves. It is green on Sage's gate with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-14 end-to-end acceptance on a scratch root | — |
| 228 | Row A: ZAI_API_KEY passes through ENV_ALLOW to sessions, README line, pass-through test (XS, Haiku high) | researcher; reviewers webby | #1729 | in-review, round 1, request posted | — | Webby approves the manifest. The mutant is killed. The seat suite and the cli suite are green on Sage's gate with every scripts/test-*.sh. (sage) | `docs/plans/2026-10-20_case2-rows.md` § Brief: row A, ZAI_API_KEY in the session env allowlist | — |
| 228 | Row A: ZAI_API_KEY passes through ENV_ALLOW to sessions, README line, pass-through test (XS, Haiku high) | researcher; reviewers webby | #1729 | done | — | Webby approves the manifest. The mutant is killed. The seat suite and the cli suite are green on Sage's gate with every scripts/test-*.sh. (sage) | `docs/plans/2026-10-20_case2-rows.md` § Brief: row A, ZAI_API_KEY in the session env allowlist | Landed 86249ac55 (pushed 07:25Z) on 59f817c32: the 3 manifest paths from cf18c42b9. Patch-id b6ffe31feb0d equals the candidate's; modes and blobs match; verify-commit ok (3 paths). Sage's combined 228+238 gate on c515273db plus both rows' 6 paths, 07:11:30Z to 07:23:34Z: seat 165/0, cli 640/0, all ten scripts/test-*.sh 585/0. |
| 229 | Row B: per-instance pi agent dir for Chet's Codex login, design only, starts after 10-27 (#428) | gosalyn; reviewers webby | #1730 | queued | — | A live check, with Jason's or Mos's go, that two clients on one ChatGPT login don't revoke each other's refresh tokens. No build row starts before it passes. Held until after 10-27. (sage) | `docs/plans/2026-10-20_case2-rows.md` § Brief: row B, a per-instance pi agent dir for Chet's Codex login (#428) | — |
| 230 | Case 2 C2-13a unit render and host precheck (case2-unit.sh, case2-precheck.sh) | gosalyn; reviewers webby | #1732 | done | — | Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every scripts/test-*.sh. A run on a host is a separate window with Jason's go. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-13a unit render and host precheck | Landed c8bc5cf4c on refactor (base 390ea5261), pushed. Patch-id f4b49eb68997 matches git diff 722136772..96768766c on the 5 manifest paths (manifest fbb0d7e42c67, Webby approve r2 rev 2563). packages/cli/README.md 3-way with row 199, hunks apart. Gosalyn gate on 96768766c: cli 541/0, scripts 429/0, mutants 10/10. Merged tree check: case2-unit 30/0, host 42/0. |
| 231 | Case 2 C2-01b survey escapes, list item lines and attachment embeds (row 212 N1-N3) | gosalyn; reviewers webby | #1733 | briefed | 212 done | Webby approves the manifest. The library tests are green on Sage's gate, along with every scripts/test-*.sh. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-01b survey escapes, list item lines and attachment embeds | — |
| 231 | Case 2 C2-01b survey escapes, list item lines and attachment embeds (row 212 N1-N3) | gosalyn; reviewers webby | #1733 | done | 212 done | Webby approves the manifest. The library tests are green on Sage's gate, along with every scripts/test-*.sh. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-01b survey escapes, list item lines and attachment embeds | LANDED 29ce1e360 (sage gate 10:05:31-10:07:52Z GREEN on 62ddc96cc: library 45/0, every scripts/test-*.sh). verify-commit ok, 9 paths; patch-id 59b98b1b3e00 equals fefd54e13..f37a65857. Pushed. Webby N4 stays as restored (sage-1917), doc-only. |
| 232 | Installer I1: stack-release external units and stack-copy bundle copies (MVP, Q187, Vikunja #432) | darkwing; reviewers filbert | #1734 | done | — | Filbert approves the manifest. The cli suite and every scripts/test-*.sh are green on Sage's gate, with the mutants killed. (sage) | `docs/plans/2026-10-20_installer-rows.md` § I1 stack-release external units and bundle copies | Landed e65a3ab4d on refactor (base 0444acd08), pushed. Patch-id 5adfbab9 matches git diff 8fede5fc6..da2da2f6c on the 9 manifest paths; tree entries (modes too) equal da2da2f6c, files pass manifest 7af3c943. Filbert approve r1 29829, r2 29865. Darkwing's landing gate on da2da2f6c: cli 593/0, scripts 429/0 (+156 queue node tests). Mutants 17/17. |
| 233 | Installer I3: stack-upgrade.sh root driver and the ops-01 host run sheet (MVP, Q187, Vikunja #432) | darkwing; reviewers filbert | #1735 | in-progress | 232 done | Filbert approves the manifest. The test file, the cli suite and every scripts/test-*.sh are green on Sage's gate, with the mutants killed. (sage) | `docs/plans/2026-10-20_installer-rows.md` § I3 the upgrade command and the host run sheet | — |
| 234 | Machine grant row a: host enrollment, store records and migration 3i-3j (#436a, design r6 f9039682e, lands by 10-25 before the .53/.54 installs) | gizmoduck; reviewers duckworth | #1740 | in-progress | 210 done | Duckworth approves the manifest. The bus, business and cli suites and every scripts/test-*.sh are green on Sage's gate. The design's section 10 row a tests and mutants pass, the migration test included. (sage) (sage) | `docs/plans/2026-10-20_host-credentials.md` § Machine grant row a: enrollment, store records and migration 3i-3j | — |
| 235 | Machine grant row b: raise binding, executor, reconcile and revoke (#436, design r6 f9039682e, C1, N1, G27, G28) | gizmoduck; reviewers duckworth | #1741 | queued | 234 done | Duckworth approves the manifest. The bus, business and cli suites and every scripts/test-*.sh are green on Sage's gate. Section 10's row b tests and mutants pass, G27, G28 and the upsert mutant included. (sage) (sage) | `docs/plans/2026-10-20_host-credentials.md` § Machine grant row b: raise binding, executor, reconcile and revoke | — |
| 233 | Installer I3: stack-upgrade.sh root driver and the ops-01 host run sheet (MVP, Q187, Vikunja #432) | darkwing; reviewers filbert | #1735 | done | 232 done | Filbert approves the manifest. The test file, the cli suite and every scripts/test-*.sh are green on Sage's gate, with the mutants killed. (sage) | `docs/plans/2026-10-20_installer-rows.md` § I3 the upgrade command and the host run sheet | Landed 57f59e0c3 on 5465fd52d (pushed 05:39Z). verify-commit matches the manifest (7 paths). Patch-id b16ac924 equals the candidate's (1a1dc80ca vs e65a3ab4d). Modes match. Sage's gate on the landing tree: cli 617/0, scripts/test-*.sh 585/0. Filbert N1, N4 and N5 go to row 240, N2 and N3 to DEFERRED. |
| 234 | Machine grant row a: host enrollment, store records and migration 3i-3j (#436a, design r6 f9039682e, lands by 10-25 before the .53/.54 installs) | gizmoduck; reviewers duckworth | #1740 | done | 210 done | Duckworth approves the manifest. The bus, business and cli suites and every scripts/test-*.sh are green on Sage's gate. The design's section 10 row a tests and mutants pass, the migration test included. (sage) (sage) | `docs/plans/2026-10-20_host-credentials.md` § Machine grant row a: enrollment, store records and migration 3i-3j | LANDED c771a36f4 (pushed 06:16Z 10-21) on caa4b5944: r3 c783d5e39 41 paths via temp index, modes from ls-tree. patch-id 32bbbea2bd6f equals the candidate's; verify-commit ok 41 paths. Gate on the c783d5e39 tree 06:02:12Z-06:15:26Z: 15 packages 2890/0, ten scripts/test-*.sh 585/0. Duckworth r3 approve 29935 rev 2683. N10, N12 to row 235. |
| 235 | Machine grant row b: raise binding, executor, reconcile and revoke (#436, design r6 f9039682e, C1, N1, G27, G28) | gizmoduck; reviewers duckworth | #1741 | done | 234 done | Duckworth approves the manifest. The bus, business and cli suites and every scripts/test-*.sh are green on Sage's gate. Section 10's row b tests and mutants pass, G27, G28 and the upsert mutant included. (sage) (sage) | `docs/plans/2026-10-20_host-credentials.md` § Machine grant row b: raise binding, executor, reconcile and revoke | LANDED b1fece6f0, pushed (rode Darkwing SESSIONS aa19a2464). Cand ed4193316, manifest dad377ca8fe4, 13 paths, verify-commit ok, patch-id c355d2fc457d. Gate 10:30:55-10:42:43Z: bus 596/0, business 200/0, cli 666/0, all scripts green. Brief Gate met: duckworth-0089 approve, upsert mutant equivalent, row b and creative tests and mutants on Gizmoduck slot runs (38 of 39 killed, B17 equivalent). |
| 236 | #436b Gitea grant path build: seat accounts and tokens through an approved grant, schema 3j-3k (design r5 1be535aca, due 10-27 at risk; live waits for Jason's section 8 approval and bootstrap step) | megavolt; reviewers dewey | #1742 | queued | 210 done, 234 done | Dewey approves the manifest. The bus, business and cli suites and every scripts/test-*.sh are green on Sage's gate. Section 9's mutants are killed, N9's restart test included. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: #436b Gitea grant path, build (schema 3j-3k) | — |
| 237 | Row 199 follow-up N1: one decision publishes once (publish-busy or #spent recheck) | megavolt; reviewers duckworth | #1743 | done | 199 done | Duckworth approves the manifest. The bus suite and every scripts/test-*.sh are green on Sage's gate, and the mutant is killed. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: row 199 follow-up N1, one decision publishes once | Landed d7d943199 on refactor (local, push pending), patch-id c5f4beb4 = e42415935, manifest bb777cb0 blobs checked. Sage gate 01:31-01:44Z on 816d48b03+4 paths: M1 killed (bus 1/13, cli 1/8), every package suite and scripts/test-*.sh green (bus 478, cli 564). |
| 238 | row 200 follow-up N1: the sso-config hint carries --instance NAME (#1744) | launchpad; reviewers webby | #1744 | in-review, round 1, request posted | 200 done | Webby approves the manifest; stack-release.test.mjs whole, cli suite and every scripts/test-*.sh green on Sage's gate; the mutant killed (sage) | `docs/plans/2026-10-20_stack-release-sso.md` § Brief: row 200 follow-up N1, the sso-config hint names the instance | — |
| 238 | row 200 follow-up N1: the sso-config hint carries --instance NAME (#1744) | launchpad; reviewers webby | #1744 | done | 200 done | Webby approves the manifest; stack-release.test.mjs whole, cli suite and every scripts/test-*.sh green on Sage's gate; the mutant killed (sage) | `docs/plans/2026-10-20_stack-release-sso.md` § Brief: row 200 follow-up N1, the sso-config hint names the instance | Landed d49b1f2bd (pushed 07:25Z) on 86249ac55: the 3 manifest paths from 4c90099ea. Patch-id b9d0bb0ee3c7 equals the candidate's; modes and blobs match; verify-commit ok (3 paths). Sage's combined 228+238 gate on c515273db plus both rows' 6 paths, 07:11:30Z to 07:23:34Z: seat 165/0, cli 640/0, all ten scripts/test-*.sh 585/0. Mutant (sso-config drops --instance) killed, survived none. Round 1 N1 stays on the #1744 carry list. |
| 239 | Row 194 follow-up: the credential revoke runs under the writer's lock, refusals after a revoke say so, and a stale-revision fire test (duckworth-0074, mos-1158) | honker; reviewers duckworth | #1745 | queued | 194 done | Duckworth approves the manifest. The bus, business, cli and webui suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) (sage) | `docs/plans/2026-10-20_agent-lifecycle.md` § Brief: row 194 follow-up, the revoke under the writer's lock and a fire test | — |
| 240 | Installer I4: hostcheck, the bwrap profile check, pins and boot unit, from r6 B2 (MVP, Q187, Vikunja #432; split from I3) | darkwing; reviewers filbert | #1746 | queued | 233 done | Filbert approves the manifest. The test file, the cli suite and every scripts/test-*.sh are green on Sage's gate, with the mutants killed. (sage) (sage) | `docs/plans/2026-10-20_installer-rows.md` § I4 hostcheck: the bwrap profile check, pins and boot unit | — |
| 240 | Installer I4: hostcheck, the bwrap profile check, pins and boot unit, from r6 B2 (MVP, Q187, Vikunja #432; split from I3) | darkwing; reviewers filbert | #1746 | done | 233 done | Filbert approves the manifest. The test file, the cli suite and every scripts/test-*.sh are green on Sage's gate, with the mutants killed. (sage) (sage) | `docs/plans/2026-10-20_installer-rows.md` § I4 hostcheck: the bwrap profile check, pins and boot unit | Landed c515273db (pushed 06:57Z) on da3051ccc: the 9 manifest paths from ad1b23ab2. Patch-id 48e376efe8a4 equals the candidate's; modes and blobs match; verify-commit ok (9 paths). Sage's gate on 060a17b54 plus the 9 paths, 06:45:05Z to 06:56:21Z: cli 639/0, all ten scripts/test-*.sh 585/0. Darkwing's r2: singles 37/0, M1-M8 8/8. Filbert N1 and N2 in DEFERRED (da3051ccc); N3 by design. |
| 241 | Row 237 follow-up: one decision lands once, a two-ticket broker land test and three mutants on the landing guards (megavolt-0069, sage-1651, P19 441) | megavolt; reviewers duckworth | #1747 | done | 237 done | Duckworth approves the manifest. The bus, seat and cli suites and every scripts/test-*.sh are green on Sage's gate, and the three mutants are killed. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: row 237 follow-up, one decision lands once, with tests at the broker | Landed 5d4a61776 on refactor, pushed (base e65a3ab4d). Blobs from 3e3ed303e on 2 paths; patch-id a1649f80 equals git diff f5753c0e8..3e3ed303e on those paths; ls-tree modes equal; manifest e8266864 verifies on the landed files. Sage slot on bbf3b1ee8: M1-M3 killed; gate 15 packages 2797/0, 10 script suites 429/0. Duckworth approve 29870 rev 2628. |
| 242 | Row 237 follow-up N1: a retry after publish-timeout pushes once; hold the decision or kill and wait for the push, a slow-push retry test and a mutant (duckworth-0069, P19 440) | megavolt; reviewers duckworth | #1748 | done | 237 done | Duckworth approves the manifest. The bus and cli suites and every scripts/test-*.sh are green on Sage's gate, and the mutant is killed. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: row 237 follow-up N1, a retry after publish-timeout pushes once | Landed 5fb736b92 on refactor, pushed (base 96bac588f). Blobs from 635c10924 on 3 paths; patch-id ceb741ee equals git diff 8fede5fc6..635c10924 on those paths; ls-tree modes equal; manifest 1fbfab6c verifies on the landed files. Megavolt M1/M2 killed on Sage's slot; Sage gate 15 packages 2826/0, 10 script suites 429/0. Duckworth approve 29889 rev 2639. |
| 243 | process.test.mjs startup token refusal sets its file mode (chmod), umask sweep of packages/*/tests | morgana; reviewers webby | — | briefed | 197 done | Webby approves the manifest. The test file passes whole under umask 022 and under umask 077 on Sage's slot. (sage) | `docs/plans/2026-10-21_gate-env-rows.md` § The startup token refusal test sets its file mode | mos-1162: lands after 197 r6, whose proof runs on the unchanged test. |
| 244 | Gate runs use a fixture PI_AUTH_FILE (DEFERRED 059577886, Webby N4) | morgana; reviewers webby | — | briefed | 197 done | Webby approves the manifest. The new test and every touched file are green on Sage's slot, along with test-task.sh and test-release.sh. (sage) | `docs/plans/2026-10-21_gate-env-rows.md` § Gate runs use a fixture pi auth file | — |
| 245 | row 195 follow-up: Duckworth's N1-N3 on scripts/case1-accept.sh (root never runs an unchecked --node; the header lists what root runs; F6 refuses .. after the claude pin dir) | darkwing; reviewers duckworth | #1693; closes none | done | — | Duckworth approves the manifest. The cli suite and every scripts/test-*.sh are green on Sage's gate, and a mutant for each of N1 and N3 is killed. (sage) | `docs/plans/2026-10-20_hub-own-uid.md` § Row 195 follow-up: Duckworth's N1-N3 on the Case 1 acceptance script | LANDED 6d5545d44 on 856c1826b, pushed. Patch-id 36a621ac3966 equals 62ddc96cc..ef8b4439e. Gate: Duckworth r1 APPROVE rev 2849; N1 mutants 13-15 and N3 mutant 16 killed (sage-1961, darkwing-0107); Sage gate 12:24-12:35Z: case1-accept alone 23/0, cli 681/0, scripts/test-*.sh 429/0. Lands before row 246 as the brief asks. #1693 stays open for 246. |
| 246 | row 195 window: the Case 1 acceptance run on 10.1.1.53 (carries row 195's done condition; needs a Jason or Mos go) | darkwing; reviewers duckworth | #1693 | queued | 226 done, 245 done | The window's acceptance run passes on .53, Duckworth reads its output, and Mos confirms. (sage) | `docs/plans/2026-10-20_hub-own-uid.md` § Row 195 window: the Case 1 acceptance run on 10.1.1.53 | — |
| 247 | Case 2 C2-20b persona import follow-up: printed keys, the dry run fence check and the mkdir race | gosalyn; reviewers webby | #1749 | done | 221 done | Webby approves the manifest. The test file runs green on its own, and the three mutants are killed on Sage's slot. The cli suite and every scripts/test-*.sh are green on Sage's gate. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-20b persona import follow-up: printed keys, the dry run fence check and the mkdir race | LANDED 856c1826b on f8276c5eb, pushed. Patch-id 373ab2cc4c50 equals 6d43618a3's. Gate: Webby r1 APPROVE rev 2852; test file alone 14/0 and M1-M4 killed (sage-1971); Sage gate 12:10-12:21Z: file 14/0, cli 680/0, scripts/test-*.sh 429/0. M1/M2 at :248 not :237 (gosalyn-0111). |
| 248 | row 235 follow-up: the grant-executor hold-add and hold-remove stubs stop when their test dir is gone (found by the 197 r8 proof) | sage; reviewers webby | #1750 | done | — | Webby approves the commit. On Sage's gate the whole grant-executor.test.mjs passes three times with no stub left, the bus suite is green, and a mutant that drops the dir check leaves a stub. (sage) | `docs/plans/2026-10-20_host-credentials.md` § Machine grant row b follow-up: the held stubs end with their test | LANDED 8a04f4aa4 on refactor, pushed. Temp-index landing of candidate 4d99c178b's one path onto bc579f7a8. No drift on the path since 540239b6b. Patch-id 84974d6b08d6 matches the candidate's. Gate per the brief: Webby APPROVE r1 rev 2883, Sage's gate 13:05-13:07Z (file 3x 37/0 no stub, bus 596/0, mutant left 2). |
| 249 | row 235 follow-up: the grant undo records hostLineLeft when install never returned, since the remote add can outlive an abort or timeout (mos-1166) | gizmoduck; reviewers duckworth | #1751 | done | — | Duckworth approves the manifest. On Sage's gate the bus suite and the whole touched test files are green, and a mutant that drops the new catch branch is killed. (sage) | `docs/plans/2026-10-20_host-credentials.md` § Machine grant row b follow-up: the undo can't prove the remote add has ended | LANDED 9de5a57ab on refactor, pushed with 7b9737953 (DEFERRED N1). Range-diff: 8a04f4aa4..05595794b vs 8ca064158..9de5a57ab, same 3 paths, patch-id 99551b122e66 on both. Gate sage-2026 14:53Z: grant-executor whole 39/0, bus 598/0, no stub left, C1 (drop :1025) killed by G8 and both G23. Duckworth approve c30070. |
<!-- mosaic-queue:end -->
+8614 -114
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -940,6 +940,7 @@ export class Broker {
if (signal.aborted) fail('revoked');
};
let installing = false,
installed = false,
registered = false,
committed = false;
try {
@@ -952,6 +953,7 @@ export class Broker {
step('install');
installing = true;
await this.#hostGrant.install(business, g.host, record, tag, key.publicLine, signal);
installed = true;
stop();
await this.#hostGrant.verify(g.host, record, key, signal);
stop();
@@ -1018,6 +1020,9 @@ export class Broker {
} catch {
extra = { hostLineLeft: tag };
}
// An abort or a timeout kills only the local ssh, so an add that hasn't returned may still write
// its line after the remove has run. Only install returning proves it ended (row 249).
if (!installed) extra = { hostLineLeft: tag };
}
try {
this.#store.transaction(() => this.#hostGrantFailed(business, decision, g, code, extra));
+61 -5
View File
@@ -51,6 +51,7 @@ async function until(fn, ms = 10000) {
// The stubs read flag files in the test dir: fail-<verb> exits 1, noop-add exits 0 and writes nothing,
// hold-add waits for `release` (then exits 1 if `skip` is there), and hold-remove waits for `release`.
// Both also stop when the test dir is gone, since a held stub on the sshd side outlives its test (#1750).
// wrap.sh logs every request. The sudo stub logs its arguments to sudo.log and runs the command as the
// test's user.
async function sshd(t, dir) {
@@ -63,12 +64,12 @@ async function sshd(t, dir) {
stub('list', '[ -e "$D/fail-list" ] && exit 1\n[ -e "$D/granted" ] && grep " mosaic-[0-9a-f]*$" "$D/granted"\nexit 0');
stub(
'add',
'if [ -e "$D/hold-add" ]; then : > "$D/holding"; while [ ! -e "$D/release" ]; do sleep 0.05; done; [ -e "$D/skip" ] && exit 1; fi\n' +
'if [ -e "$D/hold-add" ]; then : > "$D/holding"; while [ ! -e "$D/release" ] && [ -d "$D" ]; do sleep 0.05; done; [ -e "$D/skip" ] && exit 1; fi\n' +
'[ -e "$D/fail-add" ] && exit 1\n[ -e "$D/noop-add" ] && exit 0\n' +
'grep -q " $4\\$" "$D/granted" 2>/dev/null && exit 0\necho "restrict $2 $3 $4" >> "$D/granted"',
);
stub('sudo', 'printf "%s\\n" "$*" >> "$D/sudo.log"\nshift 2\nexec "$@"');
stub('remove', 'if [ -e "$D/hold-remove" ]; then : > "$D/holding-remove"; while [ ! -e "$D/release" ]; do sleep 0.05; done; fi\n' +
stub('remove', 'if [ -e "$D/hold-remove" ]; then : > "$D/holding-remove"; while [ ! -e "$D/release" ] && [ -d "$D" ]; do sleep 0.05; done; fi\n' +
'[ -e "$D/fail-remove" ] && exit 1\n[ -e "$D/granted" ] || exit 0\ngrep -v " $2\\$" "$D/granted" > "$D/granted.tmp"\nmv "$D/granted.tmp" "$D/granted"');
writeFileSync(
join(dir, 'wrap.sh'),
@@ -286,14 +287,17 @@ test('G8: a failure at install or verify removes the line and leaves no record,
const { id, end } = await f.approve();
assert.equal(end.code, 'host-failed', flag);
assert.equal(end.tag, f.tag);
assert.equal('hostLineLeft' in end, false);
// An install that didn't return keeps the sweep hint, since its add may still be running (row 249).
// Here the hint is false, and a sweep drops it (G24). noop-add fails at verify, after install returned.
const returned = flag === 'noop-add';
assert.equal(end.hostLineLeft, returned ? undefined : f.tag, flag);
assert.equal(f.requests().at(-1), `remove ${USER} ${f.tag}`, flag);
assert.deepEqual(f.granted(), []);
assert.equal(f.record(), undefined);
assert.equal(existsSync(f.keyFile), false);
assert.equal(f.state(), undefined);
assert.deepEqual(f.grantEvents().filter((e) => e.grantDecision === id).map((e) => e.action), ['grant.step', 'grant.failed']);
assert.match(f.messages(id)[0].body, /failed: host-failed\.$/);
assert.match(f.messages(id)[0].body, returned ? /failed: host-failed\.$/ : /Run: mosaic hosts sweep web1$/);
}
});
@@ -429,7 +433,8 @@ test('G23: a revoke during install ends grant.failed revoked with no key file, n
await until(() => existsSync(join(f.dir, 'holding')));
assert.deepEqual(f.credentials.abortClaim('demo/ops', 'host.web1'), { pending: true });
await until(() => f.ended(d.id));
// The held add then exits without writing, as a killed session's would.
// `skip` makes the held add exit without writing. A killed session's add can still write; the row 249
// tests below cover that.
f.flag('skip');
f.flag('release');
assert.equal(f.ended(d.id).code, 'revoked');
@@ -441,6 +446,57 @@ test('G23: a revoke during install ends grant.failed revoked with no key file, n
assert.equal(f.credentials.busy('demo/ops', 'host.web1'), false);
});
// Row 249. An abort and a timeout kill only the local ssh, and sshd sends a non-pty command no SIGHUP,
// so the held add goes on. Released after the undo's remove, it writes its line and exits 0.
const lateAdd = async (f, id, code) => {
await until(() => f.ended(id));
const end = f.ended(id);
assert.equal(end.code, code);
assert.equal(end.hostLineLeft, f.tag);
assert.match(f.messages(id)[0].body, new RegExp(`tagged ${f.tag} may be left on the host\\. Run: mosaic hosts sweep web1$`));
assert.equal(f.requests().at(-1), `remove ${USER} ${f.tag}`);
f.flag('release');
await until(() => f.granted().some((l) => l.endsWith(' ' + f.tag)));
assert.equal(f.record(), undefined);
assert.deepEqual((await f.call('human', 'hosts.status'))[0].hostLineLeft, [f.tag]);
await refusesWith(f.call('human', 'hosts.remove', { host: 'web1' }), 'host-line-left');
assert.deepEqual(await f.call('human', 'hosts.sweep', { host: 'web1' }), { host: 'web1', removed: [f.tag], hostLineLeft: [] });
assert.deepEqual(f.granted(), []);
assert.deepEqual(await f.call('human', 'hosts.remove', { host: 'web1' }), { host: 'web1', removed: true });
};
test('G23 (row 249): an add that writes after an abort\'s remove leaves hostLineLeft until the sweep', async (t) => {
const f = await setup(t);
f.flag('hold-add');
const d = await f.raise();
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => existsSync(join(f.dir, 'holding')));
assert.deepEqual(f.credentials.abortClaim('demo/ops', 'host.web1'), { pending: true });
await lateAdd(f, d.id, 'revoked');
});
// The timeout's kill without its 30 s wait: install gets its own signal, which SIGKILLs the local ssh
// group as host-run.mjs's timer does, and the call then fails as a timed-out request does.
test('G23 (row 249): an add that writes after a timed-out install\'s remove leaves hostLineLeft until the sweep', async (t) => {
const f = await setup(t, {
wrap: (hg) => ({
...hg,
install: async (business, host, record, tag, line) => {
const timer = new AbortController();
const out = hg.install(business, host, record, tag, line, timer.signal);
await until(() => existsSync(join(f.dir, 'holding')));
timer.abort();
await out.catch(() => {});
throw new Error('timed out');
},
}),
});
f.flag('hold-add');
const d = await f.raise();
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await lateAdd(f, d.id, 'host-failed');
});
test('G23: a revoke recorded after the raise and before the start ends step 1 revoked, and a held slot ends host-busy', async (t) => {
const f = await setup(t);
const revoked = f.fixtureGrant();
+45 -5
View File
@@ -7,7 +7,7 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { tmp } from "./helpers.mjs";
@@ -77,10 +77,13 @@ args=(); for a in "$@"; do if [[ "$a" == /* ]] && [ -e "$ROOT_DIR$a" ]; then arg
HOME="$ROOT_DIR/srv/mosaic" PATH="$np$PATH" exec "$ROOT_DIR$cmd" "\${args[@]}"`,
};
// Executables inside the tree, run by the fake sudo or by the script itself.
// Executables inside the tree, run by the fake sudo or by the script itself. Node marks a run outside
// the fake sudo, which sets HOME to the stack's home, so a test can tell whether root ran it.
const NODE_STUB = `[ "$HOME" = "$ROOT_DIR/srv/mosaic" ] || touch "$HC/node-as-root"
[ "\${1:-}" = --version ] && { cat "$HC/node-version"; exit 0; }
exec "$REAL_NODE" "$@"`;
const TREE = {
[NODE]: `[ "\${1:-}" = --version ] && { cat "$HC/node-version"; exit 0; }
exec "$REAL_NODE" "$@"`,
[NODE]: NODE_STUB,
[CLAUDE]: `[ "\${1:-}" = --version ] && cat "$HC/claude-version"; exit 0`,
[CLI]: `${log("mosaic")}
case "$1" in
@@ -151,6 +154,9 @@ function host(t) {
for (const [name, text] of Object.entries(state)) put(join(hc, name), text);
for (const [name, body] of Object.entries(FAKES)) put(join(bin, name), `#!/usr/bin/env bash\n${name === "stat" ? "" : log(name)}\n${body}\n`, 0o755);
for (const [path, body] of Object.entries(TREE)) put(join(root, path), `#!/usr/bin/env bash\n${body}\n`, 0o755);
// F9 runs node only when it and each directory above it have no group or other write, whatever the umask.
for (let d = dirname(NODE); d !== "/"; d = dirname(d)) chmodSync(join(root, d), 0o755);
chmodSync(root, 0o755);
return { dir, root, hc, bin, args };
}
@@ -204,6 +210,7 @@ test("row 195 accept: every check passing exits 0, with each P mark and counts t
assert.match(r.out, /^F5 bwrap-label pass bwrap 300 reads 'bwrap \(enforce\)' and its claude descendant 302 reads/m);
assert.match(r.out, /^F7 pm-live pass one live launch: pm run r-0002$/m);
assert.ok(!(r.out + r.err).includes(CRED_VALUE));
assert.ok(existsSync(join(h.hc, "node-as-root")), "F9 runs the pinned, root-only node as root");
});
test("row 195 accept: exits 1 with each F check failing alone, and its line names it", (t) => {
@@ -274,7 +281,7 @@ test("row 195 accept: F5 fails with no bwrap of mosaic's, and passes on comm alo
assert.deepEqual(failed(mustRun(h)), []);
});
test("row 195 accept: F6 fails on a stub that prints nothing and on a path outside claude-<pin>", (t) => {
test("row 195 accept: F6 fails on a stub that prints nothing, on a path outside claude-<pin> and on a .. segment", (t) => {
let h = host(t);
put(join(h.hc, "claude-version"), "");
let r = mustRun(h);
@@ -286,6 +293,14 @@ test("row 195 accept: F6 fails on a stub that prints nothing and on a path outsi
r = mustRun(h);
assert.deepEqual(failed(r), ["F6"]);
assert.match(r.out, /^F6 claude-pin FAIL --claude \S+claude-2\.0\.9\S+ is not under \/opt\/mosaic-stack\/claude-2\.1\.0\/$/m);
// Under claude-<pin>/ by prefix, but the binary it names is 2.0.9's, which answers with the pin.
h = host(t);
const dotdot = CLAUDE.replace(`claude-${CLAUDE_PIN}/`, `claude-${CLAUDE_PIN}/../claude-2.0.9/`);
put(h.args, ARGS.map((a) => (a === CLAUDE ? dotdot : a)).join("\n") + "\n", 0o644);
put(join(h.root, other), `#!/usr/bin/env bash\necho "${CLAUDE_PIN} (Claude Code)"\n`, 0o755);
r = mustRun(h);
assert.deepEqual(failed(r), ["F6"], r.out);
assert.match(r.out, /^F6 claude-pin FAIL --claude \S+\/claude-2\.1\.0\/\.\.\/claude-2\.0\.9\/\S+ has a \.\. segment$/m);
});
test("row 195 accept: F7 fails on two live PM launches, one for another business, and no launch.by", (t) => {
@@ -328,6 +343,31 @@ test("row 195 accept: F9 fails on another owner, a group-writable mode and a num
}
});
test("row 195 accept: F9 won't run --node as root off the pin path or with a writable directory above it", (t) => {
// Another node that works for the stack's user: F9 refuses to run it, and F10 fails on the path.
let h = host(t);
const other = NODE.replace(NODE_PIN, "v24.0.9");
put(h.args, ARGS.map((a) => (a === NODE ? other : a)).join("\n") + "\n", 0o644);
put(join(h.root, other), `#!/usr/bin/env bash\n${NODE_STUB}\n`, 0o755);
let r = mustRun(h);
assert.deepEqual(failed(r), ["F9", "F10"], r.out);
assert.match(r.out, /^F9 autoupdater-off FAIL root won't run --node \/opt\/mosaic-stack\/node-v24\.0\.9\/bin\/node: it is not the --node-pin path$/m);
assert.ok(!existsSync(join(h.hc, "node-as-root")));
// The pinned path, under a group-writable directory and then a symlinked node: F9 alone fails, and
// root doesn't run it.
for (const [fault, bad] of [
[(x) => chmodSync(join(x.root, dirname(NODE)), 0o775), "/opt/mosaic-stack/node-v24\\.1\\.0/bin \\(uid \\d+, mode 775\\)"],
[(x) => { const f = join(x.root, NODE); renameSync(f, `${f}.real`); symlinkSync(`${f}.real`, f); }, "/opt/mosaic-stack/node-v24\\.1\\.0/bin/node \\(uid \\d+, mode 777\\)"],
]) {
h = host(t);
fault(h);
r = mustRun(h);
assert.deepEqual(failed(r), ["F9"], r.out);
assert.match(r.out, new RegExp(`^F9 autoupdater-off FAIL root won't run --node \\S+: ${bad} is not root's alone$`, "m"));
assert.ok(!existsSync(join(h.hc, "node-as-root")));
}
});
test("row 195 accept: F11 fails on one *.json under discord/ and passes on an empty directory", (t) => {
let h = host(t);
put(join(h.root, HOME, ".mosaic-dev", "discord", "chief.json"), "{}");
@@ -18,6 +18,7 @@ const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const SCRIPT = join(REPO, "scripts", "case2-persona-import.mjs");
const FIXTURE = join(REPO, "packages", "cli", "tests", "fixtures", "case2-persona-import");
const TRACE = join(FIXTURE, "trace.mjs");
const MKDIR_RACE = join(FIXTURE, "mkdir-race.mjs");
const PAIRS = ["--pair", "default=Bean", "--pair", "chet=Chet"];
const sha256 = (data) => createHash("sha256").update(data).digest("hex");
const mode = (path) => statSync(path).mode & 0o777;
@@ -36,15 +37,15 @@ function scratch(t) {
return { root, hermes, configDir, staging: join(root, "staging"), trace: join(root, "trace.log") };
}
function run(argv, { trace } = {}) {
const pre = trace ? ["--import", TRACE] : [];
const env = { ...process.env, ...(trace ? { PERSONA_TRACE: trace } : {}) };
function run(argv, { trace, race } = {}) {
const pre = [...(trace ? ["--import", TRACE] : []), ...(race ? ["--import", MKDIR_RACE] : [])];
const env = { ...process.env, ...(trace ? { PERSONA_TRACE: trace } : {}), ...(race ? { PERSONA_MKDIR_RACE: race } : {}) };
const r = spawnSync(process.execPath, [...pre, SCRIPT, ...argv], { encoding: "utf8", env, timeout: 30_000 });
return { status: r.status, out: r.stdout, err: r.stderr, all: r.stdout + r.stderr };
}
const stage = (s, extra = [], opts) => run(["stage", "--hermes-root", s.hermes, ...PAIRS, "--staging", s.staging, ...extra], opts);
const install = (s, business = "case2") => run(["install", "--staging", s.staging, "--config-dir", s.configDir, "--business", business]);
const install = (s, business = "case2", opts) => run(["install", "--staging", s.staging, "--config-dir", s.configDir, "--business", business], opts);
// Every fixture marker and every line of fixture text the script reads.
function assertNoText(s, output) {
@@ -102,18 +103,38 @@ test("stage lists the per-channel prompts in all three places by name and byte c
assert.equal(r.status, 0, r.all);
const lines = r.out.split("\n").filter((l) => l.includes(": per-channel prompt "));
assert.deepEqual(lines, [
`profile default: per-channel prompt discord 111111111111111111, ${Buffer.byteLength("marker-prompt-top-5d02 Help with the novel.")} bytes`,
`profile default: per-channel prompt discord 222222222222222222, ${Buffer.byteLength("Ünïcode prompt")} bytes`,
`profile default: per-channel prompt "discord" "111111111111111111", ${Buffer.byteLength("marker-prompt-top-5d02 Help with the novel.")} bytes`,
`profile default: per-channel prompt "discord" "222222222222222222", ${Buffer.byteLength("Ünïcode prompt")} bytes`,
// Unquoted in the fixture, and past 2 ** 53: the id prints exactly.
"profile default: per-channel prompt discord 1333333333333333333, 5 bytes",
`profile default: per-channel prompt platforms.slack C0FIXTURE, ${Buffer.byteLength("marker-prompt-platforms-8e3a")} bytes`,
"profile default: per-channel prompt gateway.platforms.telegram -1001, 29 bytes",
"profile default: per-channel prompt gateway.platforms.telegram -1002, not text",
'profile default: per-channel prompt "discord" "1333333333333333333", 5 bytes',
`profile default: per-channel prompt "platforms.slack" "C0FIXTURE", ${Buffer.byteLength("marker-prompt-platforms-8e3a")} bytes`,
'profile default: per-channel prompt "gateway.platforms.telegram" "-1001", 29 bytes',
'profile default: per-channel prompt "gateway.platforms.telegram" "-1002", not text',
]);
assert.ok(r.out.includes("profile chet: no per-channel prompts"), r.out);
assertNoText(s, r.all);
});
// C2-20b N1 (webby-0073): a platform or channel key holding a newline prints as one quoted line, so
// it can't forge a line of the script's own output.
test("a config key with a newline prints as one quoted line", (t) => {
const s = scratch(t);
writeFileSync(join(s.hermes, "profiles", "chet", "config.yaml"), [
'"discord\\nstaged: /forged/bean.md":',
" channel_prompts:",
' "1\\npairing: profile chet is Bean": marker-prompt-forge-4c1e',
"",
].join("\n"));
const r = stage(s);
assert.equal(r.status, 0, r.all);
const lines = r.out.split("\n");
assert.deepEqual(lines.filter((l) => l.startsWith("profile chet: per-channel prompt ")), [
`profile chet: per-channel prompt "discord\\nstaged: /forged/bean.md" "1\\npairing: profile chet is Bean", ${Buffer.byteLength("marker-prompt-forge-4c1e")} bytes`,
]);
assert.ok(!lines.some((l) => l.startsWith("staged: /forged") || l.startsWith("pairing: profile chet is Bean")), r.out);
assertNoText(s, r.all);
});
test("a read trace shows stage and the dry run open SOUL.md and config.yaml and never memory, sessions or secrets", (t) => {
const s = scratch(t);
for (const argv of [["--staging", s.staging], ["--dry-run"]]) {
@@ -288,23 +309,66 @@ test("install refuses an open or foreign staging file, a digest off the manifest
assert.match(r.err, /the manifest doesn't list two personas/);
});
test("--dry-run writes nothing and prints only counts, two heading lines in a fixture counted", (t) => {
test("--dry-run writes nothing and prints only counts and the fence fault, two heading lines in a fixture counted", (t) => {
const s = scratch(t);
personaTree(s, { chet: "You are Chet. marker-chet-dry-7d21\n# Voice\nShort.\r\n## Habits\nNot # a heading, nor #nospace.\n" });
const before = readdirSync(s.root).sort();
const r = run(["stage", "--hermes-root", s.hermes, ...PAIRS, "--dry-run"]);
assert.equal(r.status, 0, r.all);
assert.equal(r.status, 1, r.all);
assert.deepEqual(readdirSync(s.root).sort(), before);
assert.deepEqual(r.out.split("\n").filter(Boolean), [
"files 2",
'profile default: lines 3, lines starting "# " or "## " 0, lines naming a Hermes tool or channel 0',
'profile chet: lines 5, lines starting "# " or "## " 2, lines naming a Hermes tool or channel 0',
'profile chet: persona holds </persona-file> or a line starting "# " or "## ", so C2-08\'s fence would refuse it',
]);
assert.equal(r.err, "");
assert.equal(r.err, "case2-persona-import: refused: C2-08's fence would refuse the persona of profile chet, so stage would write nothing\n");
assertNoText(s, r.all);
assert.ok(!r.all.includes("Voice") && !r.all.includes("Habits"));
});
// C2-20b N2 (webby-0073): a persona over the fence and under the byte cap used to pass the dry run
// and then refuse at stage. The dry run names the fault and refuses too, and passes at the limit.
test("--dry-run applies C2-08's fence: over it the fault prints, nothing is written and it exits 1", (t) => {
const atLimit = "é".repeat(PERSONA_CHARS);
const s = scratch(t);
personaTree(s, { bean: atLimit + "é" });
const before = readdirSync(s.root).sort();
const r = run(["stage", "--hermes-root", s.hermes, ...PAIRS, "--dry-run"]);
assert.equal(r.status, 1, r.all);
assert.deepEqual(readdirSync(s.root).sort(), before);
assert.deepEqual(r.out.split("\n").filter(Boolean), [
"files 2",
'profile default: lines 1, lines starting "# " or "## " 0, lines naming a Hermes tool or channel 0',
"profile default: persona is over 8000 characters, so C2-08's fence would refuse it",
'profile chet: lines 2, lines starting "# " or "## " 0, lines naming a Hermes tool or channel 0',
]);
assert.match(r.err, /refused: C2-08's fence would refuse the persona of profile default, so stage would write nothing/);
assert.ok(!r.all.includes("éé"));
personaTree(s, { bean: atLimit });
const ok = run(["stage", "--hermes-root", s.hermes, ...PAIRS, "--dry-run"]);
assert.equal(ok.status, 0, ok.all);
assert.equal(ok.err, "");
});
// C2-20b N4 (webby-0073): a directory made between privateDir's lstat and its mkdir refuses with a
// named reason, not a raw EEXIST. mkdir-race.mjs makes the directory just before the script's mkdir.
test("a directory made between the check and mkdir refuses by name, at stage and at install", (t) => {
const s = scratch(t);
let r = stage(s, [], { race: s.staging });
assert.equal(r.status, 1, r.all);
assert.equal(r.err, `case2-persona-import: refused: ${s.staging} was made by something else between the check and mkdir\n`);
assert.deepEqual(readdirSync(s.staging), []);
const s2 = scratch(t);
assert.equal(stage(s2).status, 0);
const dir = join(s2.configDir, "personas", "case2");
r = install(s2, "case2", { race: dir });
assert.equal(r.status, 1, r.all);
assert.equal(r.err, `case2-persona-import: refused: ${dir} was made by something else between the check and mkdir\n`);
assert.deepEqual(readdirSync(dir), []);
});
test("--dry-run counts three lines naming a Hermes tool or channel and prints none of the words", (t) => {
const s = scratch(t);
personaTree(s, {
@@ -0,0 +1,13 @@
// mkdir race for case2-persona-import.test.mjs: loaded with --import, it makes the directory named by
// $PERSONA_MKDIR_RACE just before the script's own mkdirSync of that path, as if another process won
// the race after the script's lstat. The script's mkdir then meets a real EEXIST.
import fs from "node:fs";
import { syncBuiltinESMExports } from "node:module";
const { mkdirSync } = fs;
const target = process.env.PERSONA_MKDIR_RACE;
fs.mkdirSync = function raced(p, ...rest) {
if (String(p) === target) mkdirSync(p, { mode: 0o700 });
return mkdirSync.call(this, p, ...rest);
};
syncBuiltinESMExports();
+26 -4
View File
@@ -7,9 +7,13 @@
# Root runs it on the Case 1 host after the install window, from a root-owned checkout. It reads the
# user, home, copy, node, claude and both ports from the args file, which must pass stack-upgrade.sh's
# rules and be a factory file for --business. Every command that runs the stack's code runs as
# `sudo -u USER -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin` from /. Root itself only reads
# systemd, ss, /proc, the account database and file metadata. It changes nothing on the host but the
# message F3 sends and its reply, and it never opens claude-session.env.
# `sudo -u USER -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin` from /. Root itself reads ss,
# /proc, the account database and file metadata, and runs these: systemctl show and is-enabled (F1,
# P11), curl on the Console's loopback healthz (F2), sudo -l (P1, P3), sudo -u jarvis bwrap --version
# and hostcheck.sh --check (P9), and node to parse managed-settings.json (F9). F9 runs node only when
# --node is the pinned path and it and every directory above it are root's with no group or other
# write. The script changes nothing on the host but the message F3 sends and its reply, and it never
# opens claude-session.env.
#
# One line per check, `<id> <path> <mark> <evidence>`, then `count <mark> N` for each mark and a
# result line. F checks are `pass` or `FAIL`. P checks are `closed`, `accepted <ruling>` or
@@ -259,6 +263,7 @@ check_f4() {
check_f6() {
local want="/opt/mosaic-stack/claude-$CLAUDE_PIN/" v
[[ "$CLAUDE" == "$want"* ]] || { ffail F6 claude-pin "--claude $CLAUDE is not under $want"; return; }
[[ "$CLAUDE/" != */../* ]] || { ffail F6 claude-pin "--claude $CLAUDE has a .. segment"; return; }
v="$("${AS_USER[@]}" "$CLAUDE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$CLAUDE_PIN (Claude Code)" ]; then fpass F6 claude-pin "$CLAUDE prints '$v'"
else ffail F6 claude-pin "$CLAUDE --version prints '$(clean "$v")', not '$CLAUDE_PIN (Claude Code)'"; fi
@@ -291,10 +296,27 @@ check_f8() {
else ffail F8 credential-file "$CRED is '$s', not '$S_USER 600 regular file'"; fi
}
# Empty when $1 and every directory above it, up to /, are the running user's (root's on the host) with
# no group or other write: case2-unit.sh's root_only rule for --node, checked again before root runs it.
# Otherwise the first path that breaks the rule. A symlink breaks it, because its mode is 777.
not_root_only() {
local p="$1" st
while :; do
st="$(stat -c '%u %a' -- "$ROOT$p" 2>/dev/null)" || { echo "$p (cannot stat)"; return; }
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) || { echo "$p (uid ${st% *}, mode ${st#* })"; return; }
[ "$p" = / ] && return
p="$(dirname -- "$p")"
done
}
check_f9() {
local s v
local s v bad
s="$(stat -c '%U:%G %a %F' -- "$ROOT$SETTINGS" 2>/dev/null)" || s="missing"
[ ! -L "$ROOT$SETTINGS" ] && [ "$s" = "root:root 644 regular file" ] || { ffail F9 autoupdater-off "$SETTINGS is '$s', not 'root:root 644 regular file'"; return; }
# Root runs node here, so it runs only the pinned path, and only when nothing but root can replace it.
[ "$NODE" = "/opt/mosaic-stack/node-$NODE_PIN/bin/node" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: it is not the --node-pin path"; return; }
bad="$(not_root_only "$NODE")"
[ -z "$bad" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: $bad is not root's alone"; return; }
v="$("$ROOT$NODE" -e 'const s=JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(JSON.stringify(s?.env?.DISABLE_AUTOUPDATER??null))' -- "$ROOT$SETTINGS" 2>/dev/null)" || v="unreadable"
if [ "$v" = '"1"' ]; then fpass F9 autoupdater-off "$SETTINGS is root:root 0644 with env.DISABLE_AUTOUPDATER \"1\""
else ffail F9 autoupdater-off "$SETTINGS env.DISABLE_AUTOUPDATER is $v, not \"1\""; fi
+16 -5
View File
@@ -18,9 +18,11 @@
// staged file against the manifest and C2-08's fence, and writes
// <configDir>/personas/<business>/<instance>.md at 0600. It never overwrites.
//
// Output is names, byte counts and sha256, never persona or prompt text. --dry-run writes nothing
// and prints counts only: files, lines, lines starting "# " or "## ", and lines naming a Hermes tool
// or channel from HERMES_WORDS. A fence fault there is counted, not refused.
// Output is names, byte counts and sha256, never persona or prompt text. A key from config.yaml
// prints as a JSON string, so it can't start a line of its own. --dry-run writes nothing and prints
// counts only: files, lines, lines starting "# " or "## ", and lines naming a Hermes tool or channel
// from HERMES_WORDS. It applies C2-08's fence as stage does, prints each persona's fault by its
// reason and then refuses. A SOUL.md over four times the fence in bytes refuses before any count.
//
// Exit codes: 0 ok, 1 refused, 2 usage.
@@ -184,7 +186,12 @@ function privateDir(path, { create }) {
const s = lstatSync(path, { throwIfNoEntry: false });
if (!s) {
if (!create) throw new Refusal(`${path} doesn't exist`);
mkdirSync(path, { mode: 0o700 });
try {
mkdirSync(path, { mode: 0o700 });
} catch (e) {
if (e.code === "EEXIST") throw new Refusal(`${path} was made by something else between the check and mkdir`);
throw e;
}
chmodSync(path, 0o700);
return;
}
@@ -221,10 +228,14 @@ function stage(o, say) {
if (o.dryRun) {
say(`files ${personas.length}`);
const faulted = [];
for (const p of personas) {
const c = counts(p.text);
say(`profile ${p.profile}: lines ${c.lines}, lines starting "# " or "## " ${c.headings}, lines naming a Hermes tool or channel ${c.words}`);
const fault = personaTextFault(p.text);
if (fault) { say(`profile ${p.profile}: persona ${fault}, so C2-08's fence would refuse it`); faulted.push(p.profile); }
}
if (faulted.length) throw new Refusal(`C2-08's fence would refuse the persona of profile ${faulted.join(" and ")}, so stage would write nothing`);
return;
}
@@ -234,7 +245,7 @@ function stage(o, say) {
for (const p of pairs) {
const prompts = config(p);
if (!prompts.length) say(`profile ${p.profile}: no per-channel prompts`);
for (const c of prompts) say(`profile ${p.profile}: per-channel prompt ${c.where} ${c.channel}, ${c.bytes === null ? "not text" : `${c.bytes} bytes`}`);
for (const c of prompts) say(`profile ${p.profile}: per-channel prompt ${JSON.stringify(c.where)} ${JSON.stringify(c.channel)}, ${c.bytes === null ? "not text" : `${c.bytes} bytes`}`);
}
for (const p of personas) {
const fault = personaTextFault(p.text);