Compare commits

...
Author SHA1 Message Date
jason.woltje 8b8f8d7810 docs(row253): record integration onto refactor 378a76b85 2026-10-21 21:24:58 -05:00
jason.woltje 2f466464cc docs(row253): record static approval and pre-confirmation residual 2026-10-21 21:23:38 -05:00
jason.woltje 39b98a26e9 fix(row253): bound cleanup retries after initial inventory 2026-10-21 21:23:38 -05:00
jason.woltje ecb15d1deb fix(row253): supervise socket queries across sudo sessions 2026-10-21 21:23:38 -05:00
jason.woltje bef1bd886b docs(row253): store repair patch without context whitespace 2026-10-21 21:23:29 -05:00
jason.woltje 70e6bb1615 docs(row253): freeze C4 repair for static review 2026-10-21 21:23:29 -05:00
jason.woltje f9b9112b97 docs: propose row253 C4 session supervisor and draft hop fake 2026-10-21 21:23:29 -05:00
jason.woltje 43f17b9afe test: prepare row253 socket fixtures and fix gate collisions 2026-10-21 21:23:29 -05:00
jason.woltje 971681f29a gander: design tester socket migration for host scripts 2026-10-21 21:23:29 -05:00
jason.woltje 378a76b854 queue: revs 3103-3108 (morgana: rows 205 and 274 claims; darkwing: row 273 claim; sage: row 276 F5 follow-up added and briefed (#1781, duckworth-0111 C4), row 202 done after ddfb29af7 (#1700 closed)) 2026-10-21 21:17:02 -05:00
jason.woltje ddfb29af7c row 202: FED R5, mosaic access list, grant and revoke (#1700)
Candidate quackerjack/fed-r5-r5 a74cd9f87 on c3d88840c, manifest of 6
paths (CANDIDATE.sha256). Duckworth APPROVE round 5, rev 3026 (#1700
c30184). C1 gate on Sage's slot by Quackerjack on the merge of a74cd9f87
onto a849d89b7 (3b86180b9), finished 01:56:24Z: cli suite 771/771, skip 0
(run 2; run 1 red on a too-long gate TMPDIR, harness only), every
scripts/test-*.sh rc 0 with Docker up, mutants M1-M13 killed by their
named tests. Landed onto 9c9732983 with the candidate's blobs and modes
for the 6 paths; c3d88840c..9c9732983 touches none of them.
2026-10-21 21:00:28 -05:00
jason.woltje 9c97329833 docs: brief for the row 253 F5 follow-up, supervised legacy AS_USER calls (duckworth-0111 C4) 2026-10-21 20:58:00 -05:00
jason.woltje a849d89b73 docs: phase 2 plan, row 273 cases go in stack-release.test.mjs (mos-1247; test-release.sh never calls stack-release.sh) 2026-10-21 20:23:38 -05:00
jason.woltje 0042f713dc queue: revs 3092-3102 (megavolt: row 236 claim; sage: row 273 activate --send/--no-sso added and briefed (#1778, mos-1246), row 274 FED R3 added and briefed (#1779), row 275 193b1b added and briefed (#1780), row 205 re-pinned to 193b1a with its piece, row 206 re-pinned and after 275) 2026-10-21 20:06:55 -05:00
jason.woltje f0d0b31e9b docs: briefs for the activate --send/--no-sso row, FED R3 and the 193b1 split; phase 2 plan F39 fixes
- docs/plans/2026-10-22_activate-send-nosso.md: the row Mos approved in mos-1246. Darkwing builds,
  Filbert reviews. --no-sso drops --send, --send refuses without --sso (F39), test-release.sh cases.
- docs/plans/2026-10-22_federation-r3.md: FED R3, the broker user principal, on 193b1a's candidate.
  Owner Morgana, reviewer Dewey. Its parents R1 and row 196 have landed, and the filing was owed.
- human-users-build.md: 193b1 split into 193b1a (message filter, chained on 196) and 193b1b (viewer
  verbs and board refusals, after FED R3 and R4). Mutant 3 moves to 193b2 (sage-2288, morgana-0063).
- sso-phase2-window.md draft 2: the tool gap paragraph names the approved row, and rollback step 1
  drops "--send follows the pointer", since --no-sso drops --send (mos-1246 items 2 and 3).
2026-10-21 19:42:01 -05:00
jason.woltje 92be0db025 docs: Q175 phase 2 window plan, draft 2 after mos-1245 (#385, Vikunja 412)
R1 same-copy rollback, 7247d80c7 named, placeholder stop, step 10 aimed at a
tester-owned thread, agent config writes. Names the tool gap: activate has no
--send and no --no-sso, and --expect-from is external-units only.
2026-10-21 19:33:57 -05:00
jason.woltje 02da6a2ae4 queue: revs 3088-3091 (sage: rows 205 and 236 brief re-pins to HEAD and briefed) 2026-10-21 19:30:48 -05:00
jason.woltje 2dd9c80c1c docs: Q175 phase 2 window plan, draft 1 for Mos (#385, Vikunja 412) 2026-10-21 19:21:29 -05:00
jason.woltje 137ddd0079 docs: BUILD-LOG entry for the row 254 landing and a Sage SESSIONS line (#1759) 2026-10-21 19:18:12 -05:00
jason.woltje e3c0446f55 queue: revs 3073-3087 (gizmoduck: row 264 claim, row 268 r2 back to in-progress and in-review on ecd9b35eb, request refused without a token; sage: 268 r2 review request as jarvis, rows 269-272 for 264b-264e added and briefed with issues #1774-#1777; webby: row 268 r2 APPROVE carried on ecd9b35eb) 2026-10-21 19:17:00 -05:00
jason.woltje dcd8d4ddaa queue: revs 3071-3072 (sage: row 254 landing note and done after 331597709 and eb9672c91, #1759 closed) 2026-10-21 18:58:46 -05:00
jason.woltje eb9672c910 row 254: README request-timeout sentence and a DEFERRED entry after the landing (#1759)
Doc-only commit after the row 254 landing. The library README now says
that a sync outlasting the server's 120-second hold gets request-timeout
and still finishes (Dewey N1, #1759 comment 30173; server.mjs sets the
120 s socket timeout and the broker promise runs on). DEFERRED records
Dewey N2: library-mapping-invalid, library-remote-invalid,
library-state-invalid and library-git still map to 502 until the Console
library routes row decides them. No test reads either file's content.
2026-10-21 18:55:26 -05:00
jason.woltje 3315977095 row 254: library.sync 130 s human client timeout, six library-* STATUS entries, channel-proof re-pin, README (#1759)
Candidate megavolt/row254 b31300d79 on c3d88840c, manifest be63cfbe
(7 paths). Dewey APPROVE r1, rev 3006 (#1759 c30173). Gate on Sage's
slot by Megavolt, 23:37:27-23:40:52Z on b31300d79: library 77/77, bus
628/628, webui 305 pass with 1 skip (editor rebuild needs esbuild
0.28.2), every scripts/test-*.sh rc 0; the human-cli timeout and
library-fetch-failed STATUS mutants killed by their behaviour tests.
Landed onto 41b0cf755 with the candidate's blobs and modes for the 7
paths; c3d88840c..41b0cf755 touches none of them. Merged-tree rerun of
library, bus, webui and cli on Sage's slot before the push.
2026-10-21 18:44:22 -05:00
jason.woltje 41b0cf7551 queue: revs 3060-3070 (gizmoduck: row 268 claim and in-review, review request refused without a token; sage: 268 review request as jarvis, notes on 216 and 220 pointing at RULINGS.md da9555ca2, row 264 to gizmoduck with reviewer duckworth and briefed; webby: row 268 r1 APPROVE on 9d9ee12c3) 2026-10-21 18:35:57 -05:00
jason.woltje 5f5b51dd65 sage: rulings for row 264 (T1 item 7 sessions) on Gizmoduck's phase A questions Q1-Q7
Five slices 264a-264e, all MVP; schema label by landing order across rows 236, 264a and 206; PRD 1.13
already landed; row 194 is the stop owner for 264d; one admission hook for row 191; Pi count exact with
Claude unknown until Vikunja 466; legacy single human until row 193 (gizmoduck-0154, #1769, Vikunja 463).
2026-10-21 18:34:12 -05:00
jason.woltje da9555ca2f sage: rulings for rows 216 (C2-05a) and 220 (C2-10a) on the phase A build notes
Row 216: paging approved, get takes universe, links page instead of refusing, manuscript without a
chapter returns the first chapter, long search lines return excerpt null, exclude stays path-prefix
until C2-15, and four codes approved with query-chapter-required dropped. The next round rebases on
row 268's landing and adds the PRD 1.14 playground-level tests.

Row 220: one business helper gives library and creative seat sets from the business files, so the
Discord binding schema doesn't change. M7 is wired in the control board, M8 in discord check and run.
Both fail closed on an unreadable business file. The scope grows by the helper, its test and the board's
serve.mjs. Questions 2 to 4 are answered as proposed.
2026-10-21 18:12:46 -05:00
jason.woltje 0dbb242905 sessions: Ludwig PRD 1.11-1.14 drafts handed to Sage (ludwig-0032), Sage PRD 1.13/1.14 landing (db1e7732a) and rows 264-268 2026-10-21 18:04:09 -05:00
jason.woltje ea6989d4ed queue: revs 3038-3059 (sage: rows 264-268 added for T1 item 7 sessions, C2-09b, C2-10c, C2-15 and C2-01c with #1769-#1773; Case 2 re-pin of 214, 216, 217, 219, 220, 223-227, 229 and 265-267 to rows blob a6ac627c8; 216 and 217 after 268; 268 briefed for Gizmoduck) 2026-10-21 18:03:56 -05:00
jason.woltje db1e7732aa PRD 1.14: playground levels in REQ-LIB-2 (book, universe, draft) and the draft universe in REQ-LIB-3, from Gosalyn's approved ruling 51fdfb7e2 (webby-0089, mos-1233, mos-1241, Vikunja 464) 2026-10-21 17:45:38 -05:00
jason.woltje 5d2437f899 docs: Case 2 design (Gosalyn) as of the playground ruling round d2 at 51fdfb7e2, approved by Webby in 7c00c8345 (webby-0089, mos-1233), landed before PRD 1.14 cites it (ludwig-0031, ludwig-0032, Vikunja 464). The design's history stays on origin/gosalyn/case2-design. 2026-10-21 17:45:38 -05:00
jason.woltje 28e3d82265 gosalyn: Case 2 C2-01c brief round d2 for Webby d1: gate grep scoped to packages and docs/case2 with the six owned hits named, draft frozen with books [], hybrids may have universe-level prefixes, tests for a prefix equal to a book and for prefixes-first order (webby-0090 B1 N1 N2, mos-1240) 2026-10-21 17:45:38 -05:00
jason.woltje 524e169b8c gosalyn: Case 2 C2-01c brief, playground levels: kind values and draft entries, other-universe refusal replacing outside-book, classify checks prefixes first with book null, the readers and every file that pins the old code; C2-05a brief gains one line for the null book and drafts, C2-06 needs none (mos-1240, sage-2218, ruling 51fdfb7e2, Vikunja 464) 2026-10-21 17:45:38 -05:00
jason.woltje 5f73b388c4 PRD 1.13 r2: della-0011 F1 (owner on its own thread and a granted collaborator create or clear a session) and N1 (REQ-SESS-1 title) (Vikunja 463) 2026-10-21 17:38:03 -05:00
jason.woltje 52f36e921e PRD 1.13: session ids from the approved T1 item 7 design (REQ-SESS-1 to 3) and the REQ-ROLE-2 and REQ-THREAD-2 replacements (mos-1231, mos-1235, Vikunja 463) 2026-10-21 17:38:03 -05:00
jason.woltje 6ba8a582e8 plans: row 226 (C2-13b) brief follows design D8: the install makes the stack copy and runs no release package or activate, and the precheck's --library-status reads the bus's recorded sync state after design 10 step 4a (624212842, webby-0085 B1, sage-2189, sage-2195) 2026-10-21 17:36:25 -05:00
jason.woltje aa772d731d plans: Case 2 v1 briefs for C2-09b rooms, C2-10c Console room view and C2-15 hybrid universes, Webby-approved round d2 (Gosalyn 72ceda935 and 99b1e0859 on gosalyn/case2-v1-briefs, webby-0089, mos-1233, Q211, Vikunja 464); codes room-not-open, addressee-ambiguous and R8 accepted by Sage (sage-2218) 2026-10-21 17:36:25 -05:00
jason.woltje 3dcd3651ed docs: T1 item 7 sessions design, approved r3 (Huey; Scrooge scrooge-0008; mos-1232, mos-1235, mos-1236, mos-1237; Vikunja 463)
Lands docs/plans/2026-10-21_t1-sessions.md as of 2c843d190 on origin/huey/t1-sessions-design (347278044, e19e30df2, 2c843d190 on cca206cdc) with its three BUILD-LOG and three SESSIONS append lines, onto the current refactor tip. The design blob is unchanged. Build dependency N2 (Claude completed-compaction format and the managed dataRoot reader) is Vikunja 466.
2026-10-21 17:35:03 -05:00
jason.woltje eb017d77f1 sessions: Dewey 251 r2 and 254 r1, Darkwing re-pin prep, Filbert 257 r2, Sage W9b-W12 and PRD 1.11/1.12 landings 2026-10-21 17:34:17 -05:00
jason.woltje f842462bdd queue: revs 2995-3037 (sage window and review requests; gander 253 start; dewey 251 r2 and 254 r1 records; megavolt 254 start and in-review; gosalyn 226 claim; webby 244 verdict; quackerjack 202 r5; duckworth 0104 record; morgana 257 r1 and r2; filbert 257 r1 and r2 records) 2026-10-21 17:34:02 -05:00
jason.woltje ad9cefcf2d sage: re-pin window 4429a104f to 7247d80c7 record (rows 196, 222, 215, 250, 243, 66): STATE.md, transcript, scripts, S1/S2, matrices, P3 transcripts; W0-W12 passed, pm read open (mos-1226, mos-1228, mos-1230, mos-1234); Vikunja post scripts and records 2026-10-21 17:32:32 -05:00
jason.woltje 5c91c96df5 sessions: Filbert row 257 r1 static review, APPROVE c30188 (rev 3031) 2026-10-21 17:31:06 -05:00
jason.woltje 1509c7cc57 PRD 1.12 r3: mos-1231 rulings (T1.7 is v1 under 463, design 9.2 is an estimate, LIB-6 residual 465, KILL-1 vs Q198 protected mode) and della-0009 owner-thread share condition (Vikunja 462) 2026-10-21 17:19:27 -05:00
jason.woltje 74dea06d92 PRD 1.12 r2: della-0008 F1-F4 (viewer reads, share needs the thread user's grant, Risks line names the sandbox, LIB-6 fixed notices) and the LIB-3 pin wording (Vikunja 462) 2026-10-21 17:17:54 -05:00
jason.woltje 3576afffb3 PRD 1.12: ids for the Q176 items from the approved designs (REQ-USER-3, REQ-USER-4, REQ-LIFE-1, REQ-KILL-1, REQ-LIB-1 to 7), the Risks line and the REQ-DEC-3 title (mos-1229, Vikunja 462) 2026-10-21 17:14:24 -05:00
jason.woltje 73222011f2 PRD 1.11: the review path is Mos's reading of Q211, and the v1 VMs' OIDC provider is open (vikunja:19/461) (mos-1229) 2026-10-21 17:03:11 -05:00
jason.woltje ea0b2a16d6 PRD 1.11: v1 is the 10-27 goal (Q211, mos-1227, Vikunja 459)
Follows Jason's Q211 ruling and Mos's reading. Adds to v1 and removes nothing.
- Status and a Version 1.11 source bullet quoting Q211.
- Users: two v1 users on their own instances (Q176). Where it runs: two PVE VMs, dragon-lin dev only (Q180, Q181).
- Out of scope: minting credentials (Q188, Q190), SSO (Q175) and access beyond localhost (Q176, Q179, Q180) move into v1. The bundled Pocket ID stays out (REQ-INST-2, Q203). Codex and SetSpark stay out. Case 2's own organization is v1.
- REQ-CRED-1: Gitea per seat through a grant (Q190).
- REQ-FED-1: person-to-person v1, agent-to-agent post-v1 (Q179, Q180).
- New REQ-INST-3: one-command install and upgrade, Console unit enabled in both v1 profiles (Q187, Q189).
- Acceptance: the 10-27 goal (Q176). The 1.10 note at :1217 now points to Q211. A Version 1.11 technical note keeps the old list text.
requirements.json goes to 1.11 with 71 ids. Both requirements tests are updated to match.
2026-10-21 17:01:08 -05:00
jason.woltje cca206cdce docs: briefs for Q198 S1 and email rows E01, E03-E07 (post-v1; mos-1222, mos-1223)
E12 and E14 wait to be queued until their WAIT-row and S5 prerequisites exist.
2026-10-21 16:54:13 -05:00
jason.woltje f437973d79 docs: post-v1 email plan (Della, Ludwig approved ludwig-0005; Vikunja 460, mos-1223)
Lands docs/plans/2026-10-21_email-plan.md (merged plan, 399 lines) with
Della's independent plan and her review of Ludwig's plan, from
origin/della/email-plan b0844a5ce on c3d88840c. Della's 10 BUILD-LOG and
4 SESSIONS lines are carried as appended. All rows are post-v1; no live
mailbox, credential, DNS, spend, external mail or re-pin is authorized.
2026-10-21 16:52:52 -05:00
jason.woltje 33029f3a8a deferred: the host pi auth file entry (059577886) moves to Done with row 244 (e69260a3c); real-model suite cases run outside the gate is open (Webby N2 on row 244 r1, #1757) 2026-10-21 16:44:55 -05:00
jason.woltje e69260a3ce row 244: gate runs mount a 0600 fixture pi auth file, test-release activates on the mock adapter, and real-model test-task cases sit behind MOSAIC_LIVE_PROVIDER=1 (#1757)
Candidate morgana/row244 94ab4c2fe on a36523713, manifest 6e7e3e3c (8 paths).
Webby APPROVE r1, rev 3008 (webby-0084, REVIEW-r1.md at webby/reviews
ebb3d83bf; request #1757 c30154). Gate on Sage's slot at 81e037d20
(sage-2147): the five gate-lane files whole 51/0, test-task.sh 91/0,
test-release.sh 14/0, mutants M-G, M-L, M-A, M-T and M-F killed.
94ab4c2fe adds only BUILD.md results. Webby's drift check: a36523713..d37210288
changes none of the 8 paths and no path either suite reads; merge-tree clean.
Rulings A and C (sage-1914, sage-2061). The dropped real-model coverage goes
to DEFERRED in the follow-up doc commit.
2026-10-21 16:44:37 -05:00
jason.woltje 3a853f35dc docs: Q198 system-manager design r6.1 (Huey, Scrooge approved r6 and r6.1, mos-1222)
Integrates origin/huey/system-manager-design 1715efb78 (base 6c75887b0) as one
doc-only commit: docs/plans/2026-10-21_q198-system-manager.md (blob 4fbebd3e,
pinned code evidence base 6c75887b0) plus Huey's 14 BUILD-LOG lines and 7
SESSIONS lines, appended unchanged. Scrooge approved r6 (scrooge-0004) and
confirmed r6.1 N1-N3 closed (scrooge-0005). No package, script, runtime or
queue change. Slices S1-S6 go to the queue under Vikunja 452.
2026-10-21 16:43:25 -05:00
jason.woltje 111ec559c8 brief template: seats hand off directly to the next seat and Sage over T3 (Q210, mos-1221) 2026-10-21 16:36:53 -05:00
jason.woltje 25ffaae832 deferred: T27 sweep test leaves two sleep 60 children; the fix is a process-group kill in both t.after hooks (row 251 landing d37210288, darkwing-0117, Dewey c30168) 2026-10-21 16:36:29 -05:00
jason.woltje d372102882 row 251: T27 adds its key with -t 2 and sleeps 3.5 s, because ssh-agent counts lifetimes in whole seconds (#1756)
Candidate darkwing/row251 27f01ef63 on 3b0aef995, manifest f2bdd684
(BUILD.md and packages/bus/tests/host-run.test.mjs). Dewey APPROVE r2,
rev 2996 (#1756 c30168). Gate on Sage's slot: bus suite whole in a clean
worktree on f7a77721a plus the two manifest paths (b79ad5f2c adds docs only),
627/627 at 21:30:33Z.
Darkwing's slot runs (darkwing-0114) are on the old base and are not the
gate.
2026-10-21 16:30:50 -05:00
jason.woltje b79ad5f2cb brief: the human deletes a creative instance, chained after row 194 (REQ-COS-3, mos-1215); window plan W11 posts on 435 2026-10-21 16:20:12 -05:00
jason.woltje 3fb00018e0 window plan: draft 1 correction, 7333 acceptance was Mos's (mos-1015), Q1/Q3/Q4 answered (mos-1214) 2026-10-21 16:18:35 -05:00
jason.woltje f7a77721a9 window plan: 10-24 re-pin draft 1, NEW 7247d80c7 with --tester-socket (mos-1145, mos-1211) 2026-10-21 16:15:48 -05:00
jason.woltje 829e5f8340 sessions: Sage row 196 landing, rows 253 and 254 launched, 194 b4 ruling, queue revs to 2994, window draft 1 2026-10-21 16:14:06 -05:00
jason.woltje abb352d27e queue: revs 2977-2993; Morgana 244 start, in-review and outcome (2977-2979), Gander 196 r6 manifest (2980/2981), Sage 244 and 196 jarvis review requests, Duckworth 196 m2 approve record (2986), Sage 196 landing note and done, row 254 issue #1759, Darkwing 251 r2 in-progress and in-review (2990/2991), rows 253 and 254 briefed 2026-10-21 16:13:56 -05:00
jason.woltje c3d88840c5 row 196: BUILD-LOG lines and two DEFERRED entries after the landing (#1694)
Doc-only append after 7247d80c7. BUILD-LOG takes Gander's ten row 196 lines from 527b9a1e6 unchanged. DEFERRED records Duckworth's C1 (round 1 N2, the dead testerNoSignIn, binding the next serve.mjs row) and round 2 N5 (the Console library routes row decides on library.sync for the tester listener). No test reads either file's content.
2026-10-21 15:59:48 -05:00
jason.woltje 7247d80c7a row 196: human boundary on peer credentials, interpreter receipt and the Console tester socket (#1694)
Candidate 527b9a1e6 (gander/row196-r6-prd19, base f48725cfc), the 39 manifest paths of CANDIDATE.sha256 537d30aef70b6cd1 at ec1e7fad8. Duckworth approved round 1 outside B1/B2 (rev 2973, comment 30145) and round 2 merge-scoped (rev 2986, comment 30156). Gate gander-0066 on grant sage-2145: 2334 pass, 0 fail, 1 skip; mutants 38/38 killed. BUILD-LOG lines and Duckworth's C1 DEFERRED entry follow as a doc-only append.
2026-10-21 15:55:39 -05:00
jason.woltje 6661732925 sessions: Sage row 222 landing, PRD 1.9 and 1.10, Pocket ID row 255, 196 r6 grants, overlap class rule (mos-1204) 2026-10-21 15:46:03 -05:00
jason.woltje 21def28502 queue: revs 2968-2976; Gander 196 r5 manifest and r6 changes move, Sage 222 review request and done, Duckworth 196 m1 changes record, rows 254 and 255 added 2026-10-21 15:45:45 -05:00
jason.woltje add0f1aae5 PRD 1.10: REQ-INST-2, a bundled Pocket ID for users with no OIDC provider, post-v1 (Q203, mos-1204)
Adds REQ-INST-2 after REQ-INST-1 as drafted in sage-2144 and ruled in mos-1204: Jason's Q203 sentence verbatim, marked post-v1 the way REQ-FED-1 is, with Mos's reading (mos-1202) labelled as his. A Version 1.10 technical note records, as fact, that REQ-WEB-2 uses the Console's own OIDC sign-in while the out-of-scope list still names 'Pocket ID and SSO', which Jason hasn't ruled on (vikunja:19/459). The list and the Pocket ID technical note are unchanged. requirements.json goes to 1.10 with 70 ids, and both requirements tests are updated to match. Design row 255 (#1760, Vikunja 458).
2026-10-21 15:26:45 -05:00
jason.woltje 7d08237e17 brief: bundled Pocket ID for users with no OIDC provider, design (Q203, Vikunja #458, mos-1202) 2026-10-21 15:21:38 -05:00
jason.woltje f48725cfc1 PRD 1.9: REQ-WEB-2, Console decide buttons for a human the Console signed in, and the REQ-DEC-3 amendment (mos-1202)
Jason approved the buttons in decision 00ed58e2 (vikunja:19/266) and chose
O6 in Q203 (OIDC-only), relayed by Mos in mos-1202 with "Land PRD 1.9 with
REQ-WEB-2 as written". The wording is sage-2067's draft. requirements.json
goes to 1.9 with 69 ids; tasks suite 97/0 and bus requirements.test.mjs 3/0
on Sage's slot.
2026-10-21 15:19:43 -05:00
jason.woltje 0de97f429d brief: library sync follow-up, client timeout, STATUS codes and README (row 222 deferrals, #1723) 2026-10-21 15:16:54 -05:00
jason.woltje 0b33f5e9db row 222: C2-03a fetch-only library sync, askpass helper, bus verb and timer (#1723)
Candidate c9aa9c2ee (megavolt, manifest c2121b55, 15 paths), approved by
Dewey (rev 2966, comment 30138). Gate on Sage's slot by Megavolt,
20:08:37Z to 20:11:33Z: library 77/77, bus 609/609, webui library-cli 1/1
and pinned-copy 10/10, cli stack-copy 17/17, every scripts/test-*.sh rc=0,
fixture tokens 0 hits; mutant kills carried from 4a0e0ab3a with equal blobs.
2026-10-21 15:13:31 -05:00
jason.woltje 3586955286 sessions: Sage row 250 landing, 196 r5 run 4 and resume, F27 row 253; Dewey 66 r4, 251 r1 and 222 r1 lines; Darkwing 251 line 2026-10-21 15:04:41 -05:00
jason.woltje a54a17f3d8 queue: revs 2905-2967 (rows 66, 215, 243, 250 landings; 196, 222, 244, 250, 251 rounds; row 253 added)
Includes other seats' revs: dewey 2905 2966; filbert 2906 2924 2950 2957; herb 2917-2919; morgana 2928 2933 2934; gizmoduck 2935 2945 2946 2951-2953; darkwing 2938 2944 2947; webby 2939; megavolt 2960 2961.
2026-10-21 15:04:30 -05:00
jason.woltje 0cdead66cd brief: F27, case2-unit.sh and case1-accept.sh move to --tester-socket, chained after row 196 (#1758)
Mos ruling mos-1198 item 4. Owner Gander, reviewer Duckworth, gate owner Sage.
2026-10-21 14:47:32 -05:00
jason.woltje 1b6158ce26 row 250: K12 holds the stop's release while it reads the shim, the release answers the events read's reason, and R13 closes its controller (#1755)
Candidate gizmoduck/row250-r2 c1a858975 on 3b0aef995, manifest eca0b8e2.
Filbert APPROVE rev 2957 (#1755 c30130). Gate on Sage's slot (grant sage-2109,
gizmoduck-0150): P1/P2/M1 probes as predicted, conversation whole 3x 183/183
and a concurrent pair 183/183 each, control-board whole 124/124.
2026-10-21 14:42:48 -05:00
jason.woltje 081ff6b128 sessions: Sage row 215 landing, 196 r4 grant after the base change, slot order for 250, 222, 244 and 251; Herb row 215 line 2026-10-21 14:35:05 -05:00
jason.woltje be86adbff2 sessions: Filbert row 250 round 2 static review, approve 2026-10-21 14:35:05 -05:00
jason.woltje d163afc47f sessions: Filbert row 250 round 1 static review, changes 2026-10-21 14:35:04 -05:00
jason.woltje e54e054632 row 215: C2-04 the library entry in the business file, consumer skips, M9 organization-projects-protected, the bare mirror, library.file and library.status (#1716)
The business file accepts a kind "library" project entry, and each consumer
that assumed a project root skips it. An organization write whose projects
differ from the file's is refused (M9). The bare mirror is created at the
projectRepo path with no remote. library.file and library.status are human
only and refuse an agent session with human-required.
Candidate herb/row215-on197 dde3445ab on a36523713, Filbert APPROVE rev 2924.
2026-10-21 14:24:37 -05:00
jason.woltje 6096b051b1 row 243: tests that depend on the gate umask set their file modes; stack-instances bootstraps under an explicit umask with a 077 subtest (#1754)
process.test, proof-cli.test and sso.test set the too-open mode with chmod
instead of taking it from the umask. stack-instances.test bootstraps under an
explicit umask and asserts the 0700 config dir under umask 077.
Candidate morgana/row243 6581f750e (code 137bda3af), Webby APPROVE rev 2939.
2026-10-21 14:12:03 -05:00
jason.woltje 9a8c8860d3 sessions: Sage row 66 landing, J2 brief and REQ-WEB-2 wording, gate coordination for 196 r2, 215, 243, 250, 251 2026-10-21 14:06:16 -05:00
jason.woltje 6195a00f8d sessions: Filbert row 215 round 2 merge-scoped review, approve 2026-10-21 13:55:22 -05:00
jason.woltje 3b0aef9952 deferred: row 66 live checks, Claude Code at the first re-pin with row 66 and pi after #383, Vikunja #454 (mos-1186) 2026-10-21 13:55:07 -05:00
jason.woltje 2c38325eca row 66: managed sessions run the unslop check on both harnesses, the operator note on the RESULT, a correction notice on the next turn, and unslop.jsonl counts (#1559)
Lands Sage's sage/row66-r5 at 7ffb640ac, round 4 (r4d 82c830779) rebased onto a36523713 after row 197,
approved by Dewey (comment 30085) and Filbert (30086) on manifest 6cfd812da4ca (23 paths, rev 2905). The
rebase had no conflict. The range-diff is = except one pi-session.test.mjs hunk header. Patch-id without
agents/ is d67275ac4251 on 7b9737953, a36523713 and this commit's parent. Gate (mos-1186): unslop,
harness, cli and every scripts/test-*.sh green on r4d, harness 149/0 again on r5. Landing sessions on a
scratch root from the candidate tree: Claude Code r4ab24630d323 showed the note, the injected notice and
counts checked 2, flagged 1, notices 1. Pi r1dd94171bede is launch only until #383 (Vikunja #454).
2026-10-21 13:51:28 -05:00
jason.woltje 640aa03f27 brief: Console inbox Approve and Deny for grant requests, Vikunja #435 (mos-1193) 2026-10-21 13:44:03 -05:00
Herbandjason.woltje a365237133 test: resolve the real Docker binary inside lane fixtures
(cherry picked from commit c45ddeb6bd)
2026-10-21 13:28:40 -05:00
jason.woltje 5afff54908 brief: T27 row 251 records the monotonic-boundary check, 8 of 80 adds lost the key (mos-1185 item 2) 2026-10-21 12:20:21 -05:00
jason.woltje f33932921c brief: T27 lists no identities right after ssh-add -t 1, Vikunja #453 (mos-1185 item 2); row 197 lane-b observation (item 3) 2026-10-21 12:12:46 -05:00
jason.woltje 1be656ac97 brief: K12 reads cgroup.events after cgroup.kill under load, Vikunja #450 (mos-1182 item 3) 2026-10-21 11:09:37 -05:00
jason.woltje 19d3d5f026 sessions: Filbert row 66 round 4 review, approve 2026-10-21 10:21:00 -05:00
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
jason.woltje f8276c5eb3 row 218: C2-12 setup sheet for Melanie and the library survey run sheet (#1719)
Doc only, Sage's sage/row218 89ba79d94 (three commits squashed). Webby APPROVE round 1 (webby-0075,
rev 2848), round 2 (webby-0077, rev 2856) and round 3 (webby-0078, rev 2860). docs/case2/SETUP-SHEET.md
tells Melanie what the agents read, the playground, link and property forms, and what changes for
Bean and Chet, with M5's 1,200 character reply total (mos-1165). It goes to her only after C2-06
lands and on Jason's go (#419). docs/case2/SURVEY-RUN.md is design 10 step 7's run sheet: the
bare mirror as the source, a node check, output lines and refusals from survey.mjs. Claim sources
and rulings are in agents/sage/work/queue-218/BUILD.md. No test reads either doc path.
2026-10-21 07:05:39 -05:00
jason.woltje b65f3a0e84 sessions: Darkwing row 245 slot green, round 1 in review 2026-10-21 06:50:41 -05:00
583 changed files with 72776 additions and 656 deletions
+51
View File
@@ -5177,3 +5177,54 @@ Before: managed project/thread verbs had no Console conversation area. After: fi
2026-10-21T01:28:05Z | Darkwing | row 232 #1734 installer I1 build, round 1 in review | Before: stack-release.sh managed only user units (systemctl --user), and stack-copy.sh had no bundle copy. After: --units external (adopt, activate, rollback --to, status, confirm, revert, gate --since, AWAITING-UNITS, stale-start, external-pm, --plan --expect-from) and stack-copy check --units external, copy --bundle FILE SHA, wrapper ID --create; user mode byte-identical to the base by test. First build 0af0c73c6 on 00d4cc1f6; rebased onto row 200 (cb5fdb064) as a0ffabc3f on darkwing/row232-r1; --sso refused under external (D9), row 200 config_checks run under external (D10). Gate on Sage's slot (sage-1618): cli 561/0, every scripts/test-*.sh green; 17/17 mutants killed. Results commit 84f1a91e6 (pushed, seat branch only). Manifest 7a3b4ea1 (9 paths), queue revs 2595-2596, request comment 29825, reviewer Filbert. Packet agents/darkwing/work/queue-232/BUILD.md.
2026-10-21T04:50:44Z | Darkwing | row 233 #1735 installer I3 build, round 1 in review | Before: root had no upgrade driver for root-owned units and no host run sheet; stack-release.sh had no rehearse --backup last and no external plan refusals. After: scripts/stack-upgrade.sh upgrade and rollback (r6 A1-A5, A2f, R1-R4) with 20 tests, docs/guides/host-run-sheet.md, TOOLS.md lines, and on Sage's rulings stack-release.sh --backup last, BACKUP_STORE and the --plan refusals open-activation and migrate-required (3 tests). Base e65a3ab4d (row 232's landing). Gate on Sage's slot (sage-1704) on d99166ade: cli 616/0, every scripts/test-*.sh green, 1201/0 summed by awk; m1-m8 8/8 killed. Correction: the process sweep after the gate found an orphaned sleep 60 from the lock test's flock holder (N1); 08db6ac3f fixes it test only, Sage ruled the gate stands (sage-1707), rerun c0 20/0 and m1 killed with 0 left (sage-1708). Tip 1a1dc80ca (pushed, seat branch only). Manifest 464ce9eb (7 paths), queue revs 2644 and 2647, request comment 29901, reviewer Filbert. Packet agents/darkwing/work/queue-233/BUILD.md.
2026-10-21T06:37:41Z | Darkwing | row 240 #1746 installer I4 build, round 1 in review | Before: root had no host check for the bwrap AppArmor profile, its pins or a boot unit, and stack-upgrade.sh exited 1 args-failed when the args move after A5 failed, while exit 1 means the old copy runs. After: scripts/case1-hostcheck.sh (seven items, --check, --enforce, --upstream), scripts/case1-pins.sh, mosaic-hostcheck.service and 16 tests; the args move failure exits 5 args-failed, documented in TOOLS.md and run sheet section 6 and tested; run sheet section 3 puts the pinned npm on PATH and names the bin/claude.exe --claude path; sections 8 and 10 have precheck lines. Verification: slot run r1 on 47ec9fe2f failed at the singles (the new N1 test's after hook threw ENOENT after the fixture cleanup removed the root); b45852daa drops the hook; r2 on b45852daa green: singles 16/0 and 21/0, c0 plus M1-M8 8/8 killed, cli 639/0, scripts 585/0. Candidate darkwing/row240-r3 ad1b23ab2 on c771a36f4, manifest 573ece95 (9 paths), revs 2694 and 2695, comment 29943.
2026-10-21T09:00Z | Herb | row 215 C2-04 implementation started | Before: business files reject library entries and root consumers assume code projects; the organization writer can change all project mappings. Plan: approved manifest 2522025af on C2-01 a2c72ac95, Filbert reply to herb-0056, scope sage-1848/1852/1860. Focused slot sage-1869 observed expected red mapping and M9 cases, then 14/0 green for new T1/T2 cases, no skips; clear herb-0062 with empty test/fixture process scan. After that clear, T3/T4 tests and host-only mirror/bus implementation continue under sage-1874, unrun while Sage holds the slot. Full singles, suites and implementation review remain pending. No source commit, push or live change.
2026-10-21T13:29Z | Herb | row 215 C2-04 source verified for candidate review | Before: business code assumed every project had a root and organization writes could replace project mappings. After: library mappings load without agent checkouts or publish grants, project mappings are protected before and after serialization, host-only bare mirrors share a bounded publication lock with sync, and library.file/library.status are human-only async bus reads. Base f8276c5eb preserves all nine landed overlaps. Granted sage-2008 whole singles: 16 files, 451/0, no skips or cancellations, ended 13:27:27.279728Z; all source hashes unchanged, process scan empty, no own-path drift through 8a04f4aa4. Full landing gate and implementation review remain pending; no HTTP, sync writer or live change.
2026-10-20 19:09:44 UTC | Gander row 196 #1694 design phase | Before: tester TCP reaches every local uid and human proofs do not bind the socket peer. After: design draft at agents/gander/work/queue-196/DESIGN.md inventories listeners, proposes private tester HTTP and SO_PEERCRED binding, aligns the per-request principal contract with Morgana and Gizmoduck, and lists migration, tests, mutants and same-uid limits. Python versus other credential options and Console provenance require Duckworth ruling. Documentation only. No product code, suite, live probe, secret access or deployment.
2026-10-20 19:16:20 UTC | Gander row 196 #1694 design r2 | Before: Duckworth r1 requested concurrent helper-fd evidence (B1) and selected O1 with interpreter, proof-only and image conditions. After: design adds eight concurrent genuine proof processes plus a racing copier, M20 swapped socket and post-helper socket survival, root-owned system ELF pinning and activation evidence, safe gid validation, cap traffic without helpers, and unchanged P19 R2 text. Mos accepted socket-only tester access (mos-1015 via sage-1363); any authenticated browser relay is a separate row. Documentation coverage checks passed. No product code, suites or live action.
2026-10-20 19:21:19 UTC | Gander row 196 #1694 design approval and N5 | Before: Duckworth approved r2 (duckworth-0009, cb6d840df) with a nonblocking interpreter-upgrade note. After: r3 chooses startup safety revalidation, appends actual interpreter identity to bus/peercred-starts.jsonl and retains old activation evidence. Operator re-pin uses the next reviewed copy, respecting already-active refusal. Added task-by-task PLAN.md for inline implementation and independent manifest review. Documentation checks passed. No product code, suites or live action.
2026-10-20 19:34:28 UTC | Gander row 196 build paused at design mismatch | Before: approved design promised Unix tester cleanup only for the owned inode. After: scratch test shows Node/libuv close unlinks a replacement inode before JS close handlers. Evidence agents/gander/work/queue-196/evidence/tester-cleanup-blocker.txt. Sent Sage/Duckworth gander-0016/0017, recommending explicit same-uid residual with private 0700 parent retained. Interpreter, concurrent peer, principal, human, peer-transport, transport, tester and process single files passed. Partial code uncommitted, tester transport not wired. No suites, mutant batches or live actions.
2026-10-21 18:45 UTC | Gander row 196 preservation before rebase | Sage-2051, Mos order mos-1193, explicitly authorizes commit and fast-forward push of the current build before deferred full verification. Product and evidence preserved together; prior focused checks and stopped gates are recorded in the row196 packet, with no claim of full acceptance. Next: rebase onto a36523713 on new gander/row196-r2, report range-diff/patch-id/conflicts, then await Sage's clear for M1-M29 and all requested suites. No live action, force push, branch deletion or tests during hold.
2026-10-21T16:34:44Z | Huey | Q198/Q199 system-manager design, mos-1183, Vikunja 452 | Before: pinned foundation 6c75887b0 has separate bus, development queue, run evidence, turn/cohort controls and launch/usage paths; the assignment requests a mechanical manager design. After: docs/plans/2026-10-21_q198-system-manager.md proposes admission/cooldown, priority and safe interruption, durable requests, bounded retry/DLQ/replay/rerouting, Console monitoring, optional decision adapters with USD reservations, six ordered slices and three recommended owner decisions. Verification: 26 pinned file:line references resolve, ten-line summary, manual source/contract review, git diff --check and installed queue guard pass. No builds/suites, runtime writes, credential reads, service changes or paid calls. Huey queue next refused the linked worktree, then returned nothing from canonical checkout; direct brief controls this assignment. Gitea issue and queue-row capture are handed to Sage under the no-credential-read/no-queue-write boundary.
2026-10-21T18:45:47Z | Huey | Q198 system-manager design r2, mos-1194, Vikunja 452 | Before: Scrooge requested changes on add7cb506 for decision-key worker exposure, cap scope and absent sampler ownership. After: distinct MOSAIC_DECISION_OPENROUTER_KEY with S6 sessionEnv canary requirement; exclusive per-instance key with per-key caps and provider limit on every enablement; S2 owns supervisor sampler and waits until after MVP/re-pin. Also records P1 interpretation, missing-confidence fallback and stand-in timeout selection, explains verified Noul spelling, excludes notifier/gateway retry loops with citations, and cites lifecycle revocation requirement. Verification: 30 pinned citation spans resolve, ten-line summary, manual diff/source review and whitespace/queue-guard checks. No builds, suites, live changes, credential reads or paid calls.
2026-10-21T19:56:11Z | Huey | Q198 design r3, mos-1201/1201a, Vikunja 452 | Before: Scrooge approved r2 7e785a08f with three notes. After: N1 names proposed system.health bus read verb and includes broker handler/authorization in S2 scope and review; N2 labels exclusive ownership as a setup attestation that cannot be mechanically proved and identifies provider key limit as real ceiling; N3 adds recommended Q4 for P1 narrowing and removes the Mos-flag instruction. Other design content retained. Verification: manual r2-to-r3 delta review, ten-line summary, whitespace and installed queue-guard checks. No builds, suites, live changes or paid calls.
2026-10-21T20:19:53Z | Huey | Q198 design r4, mos-1203, DECISIONS Q204, Vikunja 452 | Before: r3 a36952564 presented Q1–Q4 for owner approval. After: section 9 records Q20a–Q20c verbatim as approved; D11 labels Huey’s reading of configurable daily/monthly caps with $0.10/$1 defaults in the one instance config via reviewed schema; provider key ceiling and explicit enable/key requirements retained; Q4 pending, Jason restated 2026-10-21, P1 unchanged. Verification: quotes match DECISIONS ledger, unrelated sections and P1 equal r3, ten-line summary, whitespace and installed queue guard checked. No builds, suites, live changes or paid calls.
2026-10-21T20:48:49Z | Huey | Q198 design r5, mos-1206, DECISIONS Q205/Q206, Vikunja 452 | Before: r4 1e8bccff1 left Q4 pending and long outages could exhaust request retry budgets. After: protected mode separate from unchanged D1 human kill authority; mechanical detection, safe-point pause/proof, JEV-first endpoint-before-headless probes at 60-second increments to 600 seconds without attempt limit, upstream reset waits, shared D11 probe accounting, one test session then batches, proposed three-failed-exit escalation, frozen outage clocks/counters without DLQ, advisory online-service routing, Q4 superseded and Q22 pending. Thresholds/deadlines/batches/escalation explicitly labelled Huey readings. Acceptance proposals updated, no implementation. Verification: complete Q205/Q206 read, quotes match ledger, D1/inventory/Q1–Q3 unchanged, 30 pinned citation references resolve, 11-line summary, manual consistency/diff review, whitespace and installed queue guard checked. No builds, suites, real probes, live changes or paid calls.
2026-10-21T21:04:37Z | Huey | Q198 design r6, mos-1208/mos-1210, DECISIONS Q207, scrooge-0003, Vikunja 452 | Before: r5 fd44f7c60 gated probes on JEV and lacked request-local resume failure separation. After: parallel independent probes; central JEV/smaller-model/rules endpoint chain, per-model timeout/calibration, 4 KiB metadata limit and isolated fallback credentials; subscription quota vs metered USD accounting and separately enabled paid-probe budget (proposed $0 defaults). S-F1–S-F7: bounded outage classification, request-local D6/two-resume DLQ rule and batch definition; escalation latch with authorized revision-bound system.recovery.release separate from kill clear; cooperative F5 pause/F6 proof only; proposed configurable 24-hour cumulative pause expiry; full Q205 kill sentence with labelled protected-mode interpretation; r3–r6 history. S5/S6 acceptance proposals updated. Verification: Q205/Q207 quotes match ledger, D1/inventory/Q1–Q3 unchanged, 30 pinned references resolve, summary within 15 lines, manual consistency/delta review, whitespace and installed queue guard checked. No builds, suites, real probes, live changes, credential reads or paid calls.
2026-10-21T21:39:25Z | Huey | Q198 r6.1 touch-up, mos-1222, scrooge-0004 N1–N3, Vikunja 452 | Before: approved r6 2ee68cd69 had unprovable session-call denial wording, implicit queue/late-call behavior and a partial Q207 paragraph. After: rule/S6 tests require session decision calls/output have no routing authority; non-waiting dispatchers use internal rules immediately; timed-out JEV does not occupy fallback decision slot but retains spend reservation; full Q207 paragraph copied exactly from DECISIONS; matching Q22 reading corrected. ENV_ALLOW and all runtime code unchanged. Verification: exact full-quote equality, obsolete wording absent, D1 unchanged, summary within 15 lines, manual N1–N3 delta review, whitespace and installed queue guard checked. No builds, suites, probes, live changes or paid calls. Review/handoff via T3 as Mos authorized.
2026-10-21T21:11:44Z | Della mos-1213 / Vikunja 460 planning phase | Before: PRD 1.10 has no email feature; MP 95b0298 and EP 7a64921 are read-only prior inputs. Scope: independent post-v1 email plan on della/email-plan, no implementation/live access/MVP or queue changes. Gitea issue enrollment belongs to Mos/Sage because this seat has no API credential.
2026-10-21T21:12:18Z | Della mos-1213 / Vikunja 460 planning verification | After: docs/plans/2026-10-21_email-plan-della.md is 341 lines, with 13 post-v1 rows and six Jason questions. Verified 42 pinned citation anchors, ten requested coverage areas, identical prior src trees, append-only records, clean whitespace and repository queue guard. No configured CI pipeline or CI-queue wrapper found in the current foundation; documentation-only validation applies, no product suites or live tests run. Phase B/C and Gitea issue/queue enrollment remain with Mos/Sage. Only della/email-plan is authorized for push.
2026-10-21T21:37:07Z | Della mos-1220 / Vikunja 460 phase B cross-review | Before: independent plans Della 191dff32f and Ludwig 4a27b73ac delivered. After: docs/plans/2026-10-21_email-review-by-della.md reviews all 218 Ludwig plan lines, records ten findings, five grafts, six retained strengths, nine disagreements and eleven merged Jason questions. Ludwig review 143415779 read in full after ludwig-0002 arrived; reconciliation records new mail-access action, no agent reroute and correction of Della dataRoot credential example. Original plan unchanged. Planning only, no product/MVP/queue/live change.
2026-10-21T21:43:02Z | Della mos-1223 / Vikunja 460 phase C synthesis | Before: independent plans/reviews complete, Mos selected Della base. After: docs/plans/2026-10-21_email-plan.md merges Ludwig grafts and accepted corrections, with 16 post-v1 rows (eight READY, eight WAIT/optional), eleven deduplicated Jason questions, per-row test paths and explicit Q210 non-v1 scheduling. Original Della plan/review preserved; Ludwig originals remain on their branch. Static checks cover graft/row/question/citation anchors and original blobs. No source code, suite, mailbox, secret file, service, MVP or queue change. Candidate goes to Ludwig before Sage landing/queue handoff, copying Mos.
2026-10-21T21:45:20Z | Della mos-1223 / ludwig-0004 focused corrections | Before: b096a16cc left content-key location implicit and E05 core registration marked ready. After: merged plan 399 lines fixes external configDir secret key/lifecycle namespace outside dataRoot backup/restore, E01 schema and E03 missing/expired-key restore tests, and E05/E06 test-local registry readiness with core kinds.mjs registration gated on approved E01/Q4. Sixteen rows and eleven questions retained; originals unchanged. Focused static checks and queue guard apply, no product/live changes. Reviewer will check changed lines only.
2026-10-21T22:24:12Z | Huey | T1 item 7 sessions design r1, mos-1232, Vikunja 463 | Before: origin/refactor cca206cdc keys assistant claims/stores/messages by thread and renders Pi compaction markers; row 194 r4 excludes thread context clear. After: docs/plans/2026-10-21_t1-sessions.md proposes independent per-agent sessions within threads on Pi/Claude, scoped claims/stores/messages/workspaces, completed compaction counts, owner-write/RBAC matrix, idempotent proven-stop/fresh-successor clear, common admission/kill checks, migration and seven acceptance groups, replacement REQ-ROLE-2/REQ-THREAD-2 text and three owner questions. PRD 1.12 USER-3/4 candidate 1509c7cc5 cited separately from base. Verification: 22 pinned references resolve, eight-line summary and three exact owner quotes; manual source/consistency review, whitespace and installed queue guard checked. No code, queue/PRD edits, builds, suites, credential reads, live changes or paid calls. MVP priority ahead of Q198 S1. Review via Scrooge, then Sage/Ludwig as Mos authorized.
2026-10-21T22:28:56Z | Huey | T1 sessions design r2, scrooge-0006, mos-1235/1236, Vikunja 463 | Before: r1 347278044 lacked the exact per-session checkout/single-writer invariant and safe runner drain ordering, and admitted other role instances. After: F1 exact workspace path, fenced checkout setup, legacy shared-workspace quiescence, clear retains old dirty/untracked/unpushed checkout and creates fresh approved checkout, T2 mutant; F2 dequeue fence and generation-bound turn-boundary idle proof before runner signal, busy drain stays clearing, T4 cases; F3 THREAD_DEFINITION only per Mos scope ruling, no role eligibility change; N1 private titles/history vs owner-visible count/activity projection; N2 Claude offline adapter/managed-store-reader dependency flagged as MVP risk. Section 8 records Mos’s Q1–Q3 readings, no questions to Jason. Verification: 27 pinned references resolve, summary within 15 lines, source/delta consistency review, whitespace and installed queue guard checked. No code, queue/PRD edits, builds/suites/live changes/provider calls/credential reads.
2026-10-21T22:30:20Z | Huey | T1 sessions design r3, scrooge-0007 R1, Vikunja 463 | Before: r2 e19e30df2 nested new session checkouts under the retained legacy instance workspace, allowing project-name collision or historical parent inspection to reach live siblings. After: exact path is dataRoot/session-workspaces/business/instance/session, outside the legacy root; canonicalized ancestor/descendant overlap refuses; T2 nesting mutant covers historical mount/inspection and a legacy project named sessions. All other r2 content retained as approved, revision label updated. Verification: old nested path absent, manual R1-only delta, whitespace and installed queue guard checked; no code/builds/suites/live changes/provider calls/credential reads.
2026-10-22T00:18:12Z | Sage (Opus 5.5) | row 254 landing (#1759), library.sync human timeout | Before: the human transport gave library.sync the 5-second default, so a sync longer than that reported outcome-unknown, and six library-* codes reached the Console as 502. After: 331597709 lands Megavolt's candidate (7 paths, tree d49e4626f, manifest digest be63cfbe recomputed and matched). human-cli.mjs waits 130 s for library.sync, past the server's 120 s hold, and 5 s for other verbs. webui bus.mjs maps library-sync-busy, library-diverged and library-accept-stale to 409, library-accept-invalid to 400, and library-credential-missing and library-fetch-failed to 503. channel-proof re-pinned. Merged-tree gate on the landing commit: library 77/0, bus 628/0, webui 305/0 (1 skip), cli 749/0 (23:44:33-23:55:11Z). eb9672c91 adds the README request-timeout sentence (Dewey N1) and a DEFERRED entry for the four library codes still at 502 (Dewey N2, binds the Console library routes row). Overlap sweep before push: 268 rebased to r2 ecd9b35eb (README only, approval carried webby-0096), 194 rebased to b7. Done rev 3072, #1759 closed.
2026-10-21 21:24:47 UTC | Gander row253 #1758 design r1 | Investigation began21:12Z under sage-2162; recorded now. Before: case2 installer still renders tester TCP and case1 acceptance requires it. After: proposed design covers socket marker migration, actual configured dataRoot within unchanged hardening, target-uid metadata queries, kernel-peer acceptance observation and explicit same-uid residual. Initial inventory saved; no passing final static-check claim, product edit, test, host or live action. Duckworth design review precedes code.
### 2026-10-21 row253 implementation prepared, gate pending
Gander: after Duckworth-0100 design approval with C1-C5, prepared the two host-script socket migrations, fake fixtures, updated retirement docs and 33 unique change checks. Static syntax/diff and source inventory checks pass; behavioral tests have not run. Sage-2191 queues the requested full scope and will send CLEAR. No host, unit or live config changed.
2026-10-22 02:13:30 UTC | Gander row253 C4 committed candidate, gate pending | Sage-2335 explicitly directs committing runnable source and tests before the whole gate. The repair implements the approved after-uid supervisor with separate inner/outer bounds; the static packet is b64aa316a. Current repaired bytes have run no syntax check, test, probe or host experiment. Earlier static syntax results apply only to the old build. Results from 8a8df4171 are historical and do not carry. Duckworth static verdict and Sage CLEAR precede all gate execution. F5 is row276 / #1781 in DEFERRED.md.
+17 -1
View File
@@ -75,7 +75,8 @@ session's workspace and its own process group. Same contract, plus:
| `MOSAIC_POLICY_FILE`, `MOSAIC_TOOLS_FILE` | The bundle's gate policy and typed tools. |
| `MOSAIC_TOOL_SOCKET` | The runner's tool socket, which the typed tools call. |
| `MOSAIC_TURN_MARKER` | `pi`: the extension writes it at `agent_end`; a pi turn that exits 0 without it failed. |
| `MOSAIC_EXTENSIONS` | `pi` only: extension files loaded with `-e`; a missing one exits 2. |
| `MOSAIC_EXTENSIONS` | `pi` only: extension files loaded with `-e`; a missing one exits 2. The pi module names two: the gate and typed tools extension, then the unslop check's `tools/unslop-hook/extension.ts` (row 66). |
| `MOSAIC_UNSLOP_LOG` | The unslop check's JSONL log, `<run>/unslop.jsonl`, which the runner sets for both adapters and reads after each turn (row 66). |
| `MOSAIC_CLAUDE_SETTINGS`, `MOSAIC_CLAUDE_MCP_CONFIG` | `claude` only: the bundle's gate hook and MCP server; required. |
| `MOSAIC_THINKING` | The route's thinking level (row 65), which the harness module's `levels()` already checked. `pi` passes `--thinking` (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`) and omits it when unset, as for `run-task.sh` workers. `claude` passes `--effort` (`low`, `medium`, `high`, `xhigh`, `max`) and requires it; unset exits 2. |
| `MOSAIC_CLAUDE_BIN` | `claude` only: absolute path of the CLI to run, from the host's environment (the bus unit pins it). Unset or empty runs `claude` from `PATH`; a relative path or a non-executable file exits 2. |
@@ -110,3 +111,18 @@ argument, which Linux caps at 128 KiB; row 213 passes the file instead, so no
local user reads the prompt in `/proc/<pid>/cmdline`. Pi 0.85.1 uses the path
string itself as the prompt when the file is missing or can't be read, so the
pi adapter exits 2 unless the path is a readable regular file.
### The unslop check (row 66)
Both adapters run the mechanical writing check from `tools/unslop-hook`,
driven by one `lists.json`. `pi` loads it as its second `-e` extension.
`claude` runs `claude-hook.js` from the bundle's `settings.json`: the Stop
hook checks the turn's replies, and the UserPromptSubmit hook adds the
correction notice as context on the next turn when the last reply had
tells. Each hook command ends `; exit 0`, so neither blocks. Both log to
`MOSAIC_UNSLOP_LOG`, and the runner adds a note for the operator to the
next RESULT it sends (`packages/harness/README.md`, the runner). The
harness modules mount the three files read-only at their own paths, and
the manifest records their digests. `MOSAIC_UNSLOP_HOOK` and `UNSLOP_LISTS`
aren't in the session environment's allowlist, so a managed session can't
switch the check off or swap its lists.
+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`).
+191
View File
@@ -0,0 +1,191 @@
# Row 251 build: T27 lists no identities right after `ssh-add -t 1` (Vikunja #453)
Brief `docs/plans/2026-10-21_t27-ssh-add-lifetime.md` (rev 2931), Mos ruling mos-1185 item 2, assigned in
sage-2075. Owner Darkwing, reviewer Dewey, gate owner Sage. Branch `darkwing/row251` from 3b0aef995,
claimed in-progress at rev 2938.
## Step 1: the failure names its cause (e5e4b2e5a)
T27's two lists now carry a message that prints only when they fail. It gives the time from addKey's
start to its resolve and to each `ssh-add -l` call, the hrtime at each with how far into its second it
fell, `/proc/uptime` at each, and whether the agent process and its socket were still there. The checks
themselves did not change. The commit holds nothing else.
## Step 2: the cause
ssh-agent counts a key's lifetime in whole seconds of CLOCK_BOOTTIME, at the add and at the reap. I read
OpenSSH 10.5p1, the version on dragon-lin (`ssh -V`: OpenSSH_10.5p1), at tag V_10_5_P1 (ssh-agent.c
sha256 873691c4, misc.c sha256 ee467f43):
- `misc.c:1790-1798` and `:1828`: `monotime()` returns `ts.tv_sec` of `clock_gettime(CLOCK_BOOTTIME)`,
falling back to MONOTONIC. The fraction of the second is dropped.
- `ssh-agent.c:1315`: the `-t` constraint sets `*deathp = monotime() + seconds`. Lines 1410 and 1598
do the same for the agent's default lifetime.
- `ssh-agent.c:1064`: the reaper drops a key when `now >= id->death`, `now` being `monotime()` again.
- `ssh-agent.c:2650`: the main loop runs `reaper()` after every poll, before it reads requests. A list
that arrives after the boundary sees the key gone.
So a key added at second s plus a fraction f dies at s + N, and `-t N` keeps it N - f seconds, between
N - 1 and N. With `-t 1`, an add late in a second leaves the key milliseconds to live. That matches
Sage's probe in the brief: 8 of 80 `-t 1` adds missed, each one made at 0.9947-0.9973 of a monotonic
second and finished at 0.998-0.0008, and none earlier in the second missed. It also matches proof 5's
failure, where the first list ran 31.9 ms into the test and found no identities.
Node's hrtime is CLOCK_MONOTONIC. BOOTTIME differs from it by the time the host spent suspended. On
dragon-lin the difference read 0.000001 s at 19:09Z, so a monotonic second boundary is a BOOTTIME one
here.
`addKey` and the agent start are not the cause. `addKey` passes the lifetime through as `-t`
(`host-run.mjs:90`), and the agent starts with no default lifetime (`:175`). Production adds the key
with `timeout + 30` (`:180`), where losing up to a second makes no difference. The only caller that
can't afford the lost fraction is T27, which asks for 1 s. The fix belongs in the test's lifetime choice,
and `host-run.mjs` doesn't change.
The slot confirms this against T27 itself (below), not just from the source.
## Step 3: the fix (this commit)
T27 adds the key with `-t 2`. The agent then keeps it more than 1 s and at most 2 s. The first list
runs milliseconds after the add, so it has over a second of margin even under load. The sleep before
the second list grows from 2.5 s to 3.5 s, which keeps the old 1.5 s margin past the new 2 s ceiling.
Both checks stay: listed after the add, gone after the lifetime. A comment above the add says why the
lifetime is 2.
I considered `-t 3` with a 4.5 s sleep for a 2 s floor. One second of floor is far above anything the
first list has needed (31.9 ms in proof 5), and the concurrent-suite run below tests it under load, so I
took the smaller change.
## Base
The branch stays on 3b0aef995. By 19:15Z origin/refactor was at 6096b051b, after rows 66 and 243
landed. `git diff 3b0aef995 6096b051b` touches neither `packages/bus/src/host-run.mjs` nor
`packages/bus/tests/host-run.test.mjs`, so there's no rebase (sage-2084). Row 243 did change
`packages/bus/tests/process.test.mjs`. My slot's bus suites run that file as it was at 3b0aef995, and the
gate rerun on Sage's slot runs it as landed.
## Prediction for part A on e5e4b2e5a (written before the slot)
The hook starts `ssh-add -t 1` at the given fraction of a monotonic second. From the brief's probe, the
agent stores the key about 3 ms later. The first list reaches the agent a few ms after addKey resolves:
Node's exit event, the mark, and `ssh-add -l`'s own start. A red needs the add to land before the second
boundary and the list to land after it.
- 0.990: green. The add and the list both land before the boundary.
- 0.993, 0.995, 0.996, 0.997: red expected. The add lands before the boundary, the list after. At 0.993
and 0.997 I'm less sure, because the list (at 0.993) or the add (at 0.997) can land right at the
boundary. A green at either is in line with the cause, and so is a red.
- 0.998: green. The add lands after the boundary, so the key gets almost a full second.
Every red should show "first list got" an answer with no SHA256 line. It should also show addKey
resolving before a second boundary and the first list starting after it or within a few ms of it, with
the agent alive and its socket present.
Under sage-2084's rule, on e5e4b2e5a only a red at 0.990 or 0.998 counts toward the stop, and two such
reds stop the run. On the fix commit any red counts, and two stop it. In parts B to D, the first red stops
the run.
## Slot plan
All scripts are in `/mnt/storage/scratch/darkwing-251-out/`, launched from `/`, each behind the busy
checks and chained with `&&`, TMPDIR under `/mnt/storage/scratch/tmp`.
1. Confirmation. `align-hooks.mjs` is a load hook, outside the repo, that serves `host-run.mjs` with
`addKey` wrapped to start `ssh-add` at a chosen fraction of a monotonic second. T27 runs unchanged
under it, alone (`--test-name-pattern`). It runs at fractions 0.990, 0.993, 0.995, 0.996, 0.997 and
0.998, three times each:
- on e5e4b2e5a (step 1, `-t 1`). Expected: some first lists fail, and the step 1 message shows the
add's resolve and the list on either side of a second boundary, with the agent alive and the
socket present. These reds are the point of the run.
- on the fix commit (`-t 2`). Expected: all 18 pass.
2. `host-run.test.mjs` whole, once, on the fix commit.
3. The bus suite whole, three times in a row.
4. The bus suite whole in two clean worktrees at the same commit, started together.
## Slot results
Sage's CLEAR came in sage-2154 at 20:58:55Z. `slot-251.sh` ran parts A and B from 20:59:22Z to 21:01:33Z,
on node v26.8.1 and OpenSSH_10.5p1, with TMPDIR `/mnt/storage/scratch/tmp/r251`. At the start, BOOTTIME
minus MONOTONIC read 0.000001 s, then 0.000000 s twice (dewey-0769 N1). So the hrtime fractions below
put the agent's second boundary in the right place.
### Part A: T27 alone under `align-hooks.mjs`, 3 runs per fraction
The fraction is when the hook started `ssh-add`. The hook logged each start within 0.0001 of its target.
| fraction | e5e4b2e5a (`-t 1`) | 68f2dfd4b (`-t 2`) |
|---|---|---|
| 0.990 | 3 pass | 3 pass |
| 0.993 | 3 pass | 3 pass |
| 0.995 | 3 red | 3 pass |
| 0.996 | 3 red | 3 pass |
| 0.997 | 3 pass | 3 pass |
| 0.998 | 3 pass | 3 pass |
e5e4b2e5a went red 6 times out of 18, every time at a fraction the prediction named, so none of them
counted toward the stop. 68f2dfd4b passed all 18. The fix runs used 68f2dfd4b, whose test blob is the same
as c19b4167e's (280132c7).
Every red failed on the first list and got `"The agent has no identities.\n"`, with the agent alive and
its socket present. The step 1 message gave these hrtime fractions. "addKey start" is the mark before the
hook's wait, so its fraction is earlier than the ssh-add start.
| run | addKey resolved | first list mark |
|---|---|---|
| 0.995 rep 1 | 0.9992 (+317.6 ms) | 0.9993 |
| 0.995 rep 2 | 0.9991 (+846.5 ms) | 0.9991 |
| 0.995 rep 3 | 0.9988 (+845.0 ms) | 0.9988 |
| 0.996 rep 1 | 0.9999 (+839.8 ms) | 1.0000 |
| 0.996 rep 2 | 0.0004 (+845.0 ms) | 0.0004 |
| 0.996 rep 3 | 0.0002 (+835.9 ms) | 0.0002 |
At 0.995 the add finished about 4 ms in, before the boundary, and the list mark came just before it. The
`ssh-add -l` process then reached the agent after the boundary. At 0.996 the agent stored the key before
the boundary, and addKey resolved just after it. In both cases the key's whole second ended between the
add and the list, which is the cause step 2 gives. The two edge fractions came out green on the first
build. At 0.993 the list still reached the agent before the boundary, and at 0.997 the add landed after
it, so the key got almost a full second. The prediction allowed either result at those two.
### Part B: `host-run.test.mjs` whole on 68f2dfd4b
24 tests, 24 passed (21:01:18Z, 14.0 s).
### The stop before C1, and the resume
At 21:01:33Z the script stopped before C1, with exit 4 and no red. My own `busy.sh` matched two `sleep 60`
processes, mine, in `darkwing-r251/packages/bus`. Part B left them. They come from T27 "the start sweep
keeps a live bus dir and kills only a matching live process" (`host-run.test.mjs:528` and `:536`). Its
`t.after` SIGKILLs the `/bin/sh -c 'sleep 60; :'` parent, but the `sleep` child lives on for up to 60 s.
That code is outside this row's diff, so the row leaves it alone, and it's recorded below as a follow-up.
Sage confirmed (sage-2159) that the right move is to resume only the scope that hadn't run.
`slot-251-cd.sh` runs C and D as `slot-251.sh` has them, with the same two busy checks. Before each check,
it waits up to 75 s for those orphans to exit: my uid, my trees, a command line of exactly `sleep 60`.
It logs them as it waits. Any other process still trips the checks.
### Parts C and D: the bus suite whole on 68f2dfd4b
| run | start | tests | pass | fail | time |
|---|---|---|---|---|---|
| C1 | 21:02:48Z | 598 | 598 | 0 | 21.6 s |
| C2 | 21:04:06Z | 598 | 598 | 0 | 21.4 s |
| C3 | 21:05:26Z | 598 | 598 | 0 | 22.1 s |
| D1, darkwing-r251 | 21:06:44Z | 598 | 598 | 0 | 24.2 s |
| D2, darkwing-r251b, TMPDIR `r251-b` | 21:06:44Z | 598 | 598 | 0 | 24.2 s |
The script waited 33 s before C2, 34 s before C3, 33 s before D and 32 s before the sweep. Each time it
was waiting for that run's `sleep 60` orphans (two per suite, four after D), and nothing else. After the
wait, the sweep of my trees, the out dir and TMPDIR found 0 processes left (21:08:04Z). Sage confirmed
(sage-2159, sage-2160) that the short gitea-api and `node --check` runs they made during the slot weren't
stops. None of my busy checks refused on any of them.
These runs used the bus files as they were at 3b0aef995. Since then, origin/refactor (666173292, then
7247d80c7 and c3d88840c with row 196) has changed `broker.mjs`, `runtime.mjs` and `server.mjs`, which T27's
file imports, along with other bus files. None of those commits touch this row's two paths or
`host-run.mjs`, and `git merge-tree` of c3d88840c with 68f2dfd4b is clean. The gate rerun in
a clean worktree on Sage's slot runs T27 against them.
### Follow-up, not in this row
T27's start-sweep test leaves the `sleep 60` child of each planted shell running for up to 60 s after
the test. Killing the process group (`process.kill(-pid)`; both shells are spawned `detached`) or
`exec sleep 60` in the shell would end the child along with its parent.
@@ -0,0 +1,711 @@
{
"base": "c3d88840c",
"stage": "pre-implementation; not the passing final static check",
"occurrences": [
{
"file": "scripts/case2-unit.sh",
"line": 25,
"text": "# optional --tester-port N (1024..65535, never --port). Only factory takes"
},
{
"file": "scripts/case2-unit.sh",
"line": 163,
"text": " --tester-port) [ \"$PROFILE\" = factory ] || die \"the library profile refuses --tester-port: the tester listener takes writes from loopback with no sign-in\" ;;"
},
{
"file": "scripts/case2-unit.sh",
"line": 193,
"text": " --tester-port) tester_port=\"$value\""
},
{
"file": "scripts/case2-unit.sh",
"line": 194,
"text": " [[ \"$value\" =~ ^[1-9][0-9]{3,4}$ ]] && [ \"$value\" -ge 1024 ] && [ \"$value\" -le 65535 ] || die \"--tester-port must be 1024..65535: $value\" ;;"
},
{
"file": "scripts/case2-unit.sh",
"line": 204,
"text": " [ -z \"$tester_port\" ] || [ \"$tester_port\" -ne \"$port\" ] || die \"--tester-port must differ from --port\""
},
{
"file": "scripts/instance.sh",
"line": 114,
"text": "# this instance's, whose ExecStart takes PORT as --port or --tester-port."
},
{
"file": "scripts/instance.sh",
"line": 132,
"text": " --tester-port) [[ \"$tok\" =~ ^[0-9]{1,5}$ ]] && ports+=(\"$((10#$tok))\") ;;"
},
{
"file": "scripts/case1-accept.sh",
"line": 65,
"text": " case \"$a\" in --port) PORT=\"$v\"; i=$((i + 1)) ;; --tester-port) TESTER_PORT=\"$v\"; i=$((i + 1)) ;; esac"
},
{
"file": "scripts/case1-accept.sh",
"line": 88,
"text": " refuse \"$ARGS_FILE lacks a numeric --port or --tester-port after --\""
},
{
"file": "scripts/webui-service.sh",
"line": 43,
"text": "# Console), --tester-socket (private Unix tester), or legacy --tester-port N"
},
{
"file": "scripts/webui-service.sh",
"line": 74,
"text": "# --tester-port that another installed Console unit in the unit directory"
},
{
"file": "scripts/webui-service.sh",
"line": 96,
"text": " FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH]"
},
{
"file": "scripts/webui-service.sh",
"line": 97,
"text": " --tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener."
},
{
"file": "scripts/webui-service.sh",
"line": 137,
"text": " --tester-port) [ $# -ge 2 ] || usage; TESTER_PORT=\"$2\"; TESTER_SET=1; SERVE_SET=1; shift 2 ;;"
},
{
"file": "scripts/webui-service.sh",
"line": 182,
"text": " echo \"webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT\" >&2"
},
{
"file": "scripts/webui-service.sh",
"line": 266,
"text": " [ \"$TESTER_SET\" = 0 ] || flags=\"$flags --tester-port $TESTER_PORT\""
},
{
"file": "scripts/webui-service.sh",
"line": 462,
"text": " if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' \"$target\"; then"
},
{
"file": "scripts/webui-service.sh",
"line": 467,
"text": " if grep -Eq '^ExecStart=.* --sso( --send)?( --tester-port [0-9]+)?$' \"$target\"; then"
},
{
"file": "scripts/webui-service.sh",
"line": 471,
"text": " tester_port=\"$(sed -n 's/^ExecStart=.* --tester-port \\([0-9][0-9]*\\)$/\\1/p' \"$target\")\""
},
{
"file": "scripts/stack-release.sh",
"line": 47,
"text": " stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]"
},
{
"file": "scripts/stack-release.sh",
"line": 63,
"text": "activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the"
},
{
"file": "scripts/stack-release.sh",
"line": 126,
"text": " --tester-port) [ \"$OP\" = activate ] && [ $# -ge 2 ] && [ -n \"$2\" ] || usage; TESTER_PORT=\"$2\"; shift 2 ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 797,
"text": " elif grep -q -- '--tester-port' \"$cli\"; then supported=1; fi"
},
{
"file": "scripts/stack-release.sh",
"line": 800,
"text": " --tester-port) i=$((i + 1)); inherited=\"${BASE_CONSOLE[i]:-}\" ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 814,
"text": " [ -z \"$inherited\" ] || DROP_TESTER=\"--tester-port $inherited (replaced by private Unix socket)\""
},
{
"file": "scripts/stack-release.sh",
"line": 830,
"text": " check_refuse tester-port-invalid \"--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other\""
},
{
"file": "scripts/stack-release.sh",
"line": 832,
"text": " [ \"$supported\" = 1 ] || check_refuse tester-port-target \"$TO's Console does not support --tester-port\""
},
{
"file": "scripts/stack-release.sh",
"line": 833,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\" --tester-port \"$TESTER_PORT\")"
},
{
"file": "scripts/stack-release.sh",
"line": 837,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\") DROP_TESTER=\"--tester-port $inherited\""
},
{
"file": "scripts/stack-release.sh",
"line": 878,
"text": " # --send and --tester-port follow the pointer, so a --no-send or a tester port set after that"
},
{
"file": "scripts/stack-release.sh",
"line": 885,
"text": " --tester-port) i=$((i + 1)) ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 893,
"text": " --tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port \"${P_CONSOLE[i]:-}\") ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 897,
"text": " SSO_PLAN=\"sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them\""
},
{
"file": "scripts/stack-release.sh",
"line": 903,
"text": "# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP."
},
{
"file": "scripts/stack-release.sh",
"line": 919,
"text": " case \"${kept[i]}\" in --send | --tester-port | --tester-socket) break ;; esac"
},
{
"file": "scripts/stack-release.sh",
"line": 1076,
"text": " case \"${without_proof[i]}\" in --send | --tester-port | --tester-socket) break ;; esac"
},
{
"file": "scripts/stack-release.sh",
"line": 1134,
"text": "# Row 190: the target Console's --port (7332 when absent) and --tester-port against every other"
},
{
"file": "scripts/stack-release.sh",
"line": 1142,
"text": " --tester-port) ports+=(\"${RUN_CONSOLE[i + 1]:-}\") ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 2106,
"text": " for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do [ \"${P_CONSOLE[i]}\" != --tester-port ] || reserved+=(\"${P_CONSOLE[i + 1]:-}\"); done"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 80,
"text": "const factoryFlags = () => [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"7333\"];"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 328,
"text": " [\"--tester-port\", \"7333\", /the library profile refuses --tester-port/],"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 364,
"text": " refuses(unit(h, { profile: \"factory\", flags: [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"443\"] }), /--tester-port must be 1024\\.\\.65535|--tester-port must differ from --port/);"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 365,
"text": " refuses(unit(h, { profile: \"factory\", flags: [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"1443\", \"--port\", \"1443\"] }), /given twice/);"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 378,
"text": " refuses(unit(h, { profile: \"factory\", flags: [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"443\"] }), /--tester-port must be 1024\\.\\.65535/);"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 380,
"text": " assert.match(read(UNIT), /--tester-port must differ from --port/);"
},
{
"file": "packages/cli/tests/stack-release-fixture.mjs",
"line": 306,
"text": " for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\\n// serve accepts '--tester-port'\\n// serve accepts '--proof-header' (row 178)\\n${[C1,C].includes(id)?\"// serve accepts '--sso' (row 192)\\n\":''}`:'// fixture Console before tester listener, never run\\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 681,
"text": "const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 687,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 688,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 690,
"text": " r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 692,
"text": " assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 693,
"text": " assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 694,
"text": " r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 697,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 702,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 705,
"text": " assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 708,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 709,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 714,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 718,
"text": " assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 724,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 731,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 732,
"text": " r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 736,
"text": " edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 738,
"text": " edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 740,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 741,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 746,
"text": " let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 749,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 821,
"text": " const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 827,
"text": " const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 830,
"text": " const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 839,
"text": " const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 843,
"text": " await t.test('a target Console without --tester-port',async(t)=>{"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 846,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 848,
"text": " assert.deepEqual(r.out.split('\\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 850,
"text": " await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1056,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1057,
"text": " assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1058,
"text": " assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1129,
"text": " await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1132,
"text": " let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1135,
"text": " assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1137,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1138,
"text": " assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1141,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1162,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1351,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1352,
"text": " assert.deepEqual(want.slice(-2),['--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1367,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1468,
"text": "const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1480,
"text": " let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1481,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1483,
"text": " const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1486,
"text": " `sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1563,
"text": " const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1564,
"text": " assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1584,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1594,
"text": " const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1682,
"text": " assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1811,
"text": " writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme','--tester-port',TESTER],schema:store,activation:'manual',at:'2026-10-20T00:00:00Z'})+'\\n');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2198,
"text": " ['beta\\'s tester port on alpha\\'s port',b,['--port',free,'--tester-port',a.port],1,`webui-service: refusing port-claimed: ${held}; nothing was changed\\n`],"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2202,
"text": " ['beta\\'s tester port on 7333',b,['--port',free,'--tester-port','7333'],4,'webui-service: port-reserved: 7333 is reserved for the default instance (7330 7332 7333)\\n'],"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2217,
"text": " await refused('--tester-port','--tester-port',a.port);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2280,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2290,
"text": " r=await run(e,'activate',C1,'acme','--tester-socket','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/--tester-socket/);assert.match(r.out,/drop.*--tester-port 7333/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2292,
"text": " assert.deepEqual(pointer(e).console,[...base,...pair(proof),'--tester-socket']);assert.match(consoleText(e),/--tester-socket/);assert.doesNotMatch(consoleText(e),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2319,
"text": " assert.doesNotMatch(conExec(e),/ --send(?: |$)|--tester-port|--public-host|--proof-header/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2388,
"text": " assert.equal(context.argv.includes('--tester-socket'),marked);assert.equal(context.argv.includes('--tester-port'),false);"
},
{
"file": "packages/cli/tests/case1-accept.test.mjs",
"line": 28,
"text": " \"--node\", NODE, \"--claude\", CLAUDE, \"--pm\", \"--\", \"--port\", \"732\", \"--business\", \"mosaic-stack\", \"--tester-port\", \"7333\"];"
},
{
"file": "packages/cli/tests/case1-accept.test.mjs",
"line": 477,
"text": " \"no tester port\": [(h) => put(h.args, ARGS.slice(0, -2).join(\"\\n\") + \"\\n\", 0o644), /lacks a numeric --port or --tester-port after --$/],"
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 68,
"text": " \"[Service]\\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\\n\");"
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 215,
"text": " put(join(h.root, \"etc\", \"systemd\", \"system\", \"mosaic-webui.service\"), \"[Service]\\nExecStart=/x serve --business mosaic-stack --tester-port 7333\\n\");"
},
{
"file": "packages/cli/tests/stack-release-external.test.mjs",
"line": 75,
"text": " for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){"
},
{
"file": "packages/webui/README.md",
"line": 128,
"text": "need `--port`, and 7330, 7332 or 7333 as `--port` or `--tester-port` exits 4"
},
{
"file": "packages/webui/README.md",
"line": 130,
"text": "`--tester-port` that another installed Console unit in the unit directory"
},
{
"file": "packages/webui/README.md",
"line": 144,
"text": "`serve --sso [--send] [--tester-port N]` makes the Console an OpenID Connect"
},
{
"file": "packages/webui/README.md",
"line": 176,
"text": "- With `--tester-port`, the tester listener refuses any request carrying an"
},
{
"file": "packages/webui/README.md",
"line": 1001,
"text": "There is no tester TCP listener. `--tester-port` is retired for this copy."
},
{
"file": "packages/webui/tests/tester-cli.test.mjs",
"line": 25,
"text": " for (const flags of [['--tester-port', '7332'], ['--port', '7444', '--tester-port', '7444'], ['--tester-port', '7444', '--port', '7444'], ...['1023', '65536', '1.5', '-1', 'nope'].map(p => ['--tester-port', p])]) {"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 125,
"text": " const r = await run(dir, ['render', '--tester-port', port, ...PREVIEW, '--send']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 127,
"text": " assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 129,
"text": " assert.ok(execStart(r.stdout).endsWith(` ${PREVIEW_FLAGS} --send --tester-port ${port}`));"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 136,
"text": " const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 150,
"text": " ['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 151,
"text": " ['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 152,
"text": " ['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\\n'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 153,
"text": " ['--port', '7444', '--tester-port', '7444'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 154,
"text": " ['--tester-port', '7444', '--port', '7444'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 164,
"text": " assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 172,
"text": " const six = await run(dir, ['render', '--tester-port', '007333']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 175,
"text": " assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\\n');"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 176,
"text": " const five = await run(dir, ['render', '--tester-port', '07333']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 178,
"text": " assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 186,
"text": " const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...PREVIEW, '--send'], bin);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 188,
"text": " assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 189,
"text": " assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 199,
"text": " const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 799,
"text": " const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send', '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 813,
"text": " const both=await run(dir,['render','--tester-socket','--tester-port','7333']);"
},
{
"file": "packages/webui/src/cli.mjs",
"line": 30,
"text": " if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket');"
}
]
}
+197
View File
@@ -0,0 +1,197 @@
# Row253 build record
2026-10-21 design stage. Read brief Static check and Gate before starting. Busy check was CLEAR; claimant gander-253-start recorded rev2995. Branch gander/row253 starts from origin/refactor c3d88840c, after196 landed7247d80c7. Plain linked node_modules retained.
Only DESIGN.md and this record/inventory are written. No product code, tests, host config, units or slot files changed. BASE-PORT-INVENTORY.json records the initial source hits; it is not a passing final static check. The final check must classify every remaining hit after implementation.
Design r1 proposes configured dataRoot resolution as the target user, socket metadata plus an isolated system-Python SO_PEERCRED observation, unchanged hardening, P8 migration and honest unnamed P10 same-uid evidence. Duckworth approval is required before implementation. No test slot requested until the final static check passes; every single/pattern run also needs the slot under sage-2142.
## Implementation before slot
Duckworth-0100 approved design r1 with C1-C5. Code and new tests were written after that approval. Tests are written but unrun: sage-2142 requires a slot even for patterns, and the row253 brief requests static preparation before the slot request. Syntax checks and static source inspection only have run. No behavioral passing claim is made.
C1: component containment, dot/dotdot/double/trailing slash rejection and a no-follow component walk. T4 includes middle symlink and sibling-prefix homes, plus protected/private roots. C2: conservative ASCII path alphabet without whitespace; LC_ALL=C makes pathname length a byte count. C3: bash accepts only the canonical two-key string/boolean JSON. Root never imports the copy. C4: GNU timeout owns a process group; output is capped and the wrapper kills remaining group members and waits for its direct child on success, failure or signal. Tests cover a TERM-ignoring reader and a child that closes stdout before its parent returns. C5: both headers name the new unprivileged config query.
N1: deferred the optional broader MainPID TCP inventory. P8 retains the approved main listener plus 7331/7333 check. N2: removed accepted mos-1101 from MARKS and P_PASS.P10; healthy and failed observations both stay unnamed, per sage-2173. N3: sudo resolves through PATH, matching case1's existing fake seam. N4: F11 retains the default DATA_ROOT Discord check; generalizing it to configured dataRoot is a separate F11 follow-up.
The README had three obsolete current-usage passages. They now show the socket marker; the remaining mention explicitly says TCP is retired. No deliberate legacy source or test changed. Acceptance still runs F9's existing builtin-only settings parser as root under its existing pinned-path checks; the new query and fixed-system-Python observation run only as the stack user with env -i. P10 separately checks its parent, final socket, process uids, stable MainPID and metadata snapshots. Its probe sends no application bytes. Hostile same-uid changes before or after that snapshot remain the stated residual.
Static checks: bash -n on both scripts, node --check on both test files, git diff --check, Python AST parsing on the record drivers, all 33 variant shell syntax checks and unique-target validation. All passed. These are not behavioral tests. Five source/test/doc blobs are frozen in CANDIDATE-SHAS.json for the requested slot. UNIQUENESS.json records original and changed hashes for every definition. No suites or checks have run. Driver names, wrapper argv and controller.log avoid the busy detector's patterns; actual commands are loaded from structured JSON only after the busy guard succeeds. Every run uses bash pipefail and tee.
## Every remaining retired-port hit
Static source audit, not a test run. Each remaining hit and its reason follows.
- `packages/cli/tests/case1-accept.test.mjs:594`: New negative saved-argument fixture asserting the migration refusal. Text: `"retired TCP": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\n')+'\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],`
- `packages/cli/tests/case1-hostcheck.test.mjs:68`: Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged. Text: `"[Service]\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\n");`
- `packages/cli/tests/case1-hostcheck.test.mjs:215`: Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged. Text: `put(join(h.root, "etc", "systemd", "system", "mosaic-webui.service"), "[Service]\nExecStart=/x serve --business mosaic-stack --tester-port 7333\n");`
- `packages/cli/tests/case2-unit.test.mjs:445`: New negative factory/library fixtures asserting the migration refusal. Text: `["--tester-port", "7333", /case2-unit.sh retires tester TCP; use --tester-socket/],`
- `packages/cli/tests/case2-unit.test.mjs:496`: New negative factory/library fixtures asserting the migration refusal. Text: `refuses(unit(h, { profile: "factory", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);`
- `packages/cli/tests/stack-release-external.test.mjs:75`: Legacy stack-release external refusal fixture, excluded by brief step 5. Text: `for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){`
- `packages/cli/tests/stack-release-fixture.mjs:306`: Retained pre-196 copy fixture for legacy activation and rollback. Text: `for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\n// serve accepts '--tester-port'\n// serve accepts '--proof-header' (row 178)\n${[C1,C].includes(id)?"// serve accepts '--sso' (row 192)\n":''}`:'// fixture Console before tester listener, never run\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){`
- `packages/cli/tests/stack-release.test.mjs:681`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];`
- `packages/cli/tests/stack-release.test.mjs:687`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:688`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:690`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:692`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);`
- `packages/cli/tests/stack-release.test.mjs:693`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);`
- `packages/cli/tests/stack-release.test.mjs:694`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);`
- `packages/cli/tests/stack-release.test.mjs:697`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:702`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:705`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(r.out,/drop[^\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:708`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:709`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:714`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:718`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:724`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);`
- `packages/cli/tests/stack-release.test.mjs:731`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:732`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:736`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;`
- `packages/cli/tests/stack-release.test.mjs:738`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');`
- `packages/cli/tests/stack-release.test.mjs:740`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:741`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:746`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:749`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:821`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;`
- `packages/cli/tests/stack-release.test.mjs:827`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);`
- `packages/cli/tests/stack-release.test.mjs:830`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);`
- `packages/cli/tests/stack-release.test.mjs:839`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');`
- `packages/cli/tests/stack-release.test.mjs:843`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await t.test('a target Console without --tester-port',async(t)=>{`
- `packages/cli/tests/stack-release.test.mjs:846`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');`
- `packages/cli/tests/stack-release.test.mjs:848`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(r.out.split('\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);`
- `packages/cli/tests/stack-release.test.mjs:850`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);`
- `packages/cli/tests/stack-release.test.mjs:1056`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1057`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1058`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));`
- `packages/cli/tests/stack-release.test.mjs:1129`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{`
- `packages/cli/tests/stack-release.test.mjs:1132`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1135`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));`
- `packages/cli/tests/stack-release.test.mjs:1137`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1138`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));`
- `packages/cli/tests/stack-release.test.mjs:1141`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1162`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);`
- `packages/cli/tests/stack-release.test.mjs:1351`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);`
- `packages/cli/tests/stack-release.test.mjs:1352`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(want.slice(-2),['--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1367`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);`
- `packages/cli/tests/stack-release.test.mjs:1468`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;`
- `packages/cli/tests/stack-release.test.mjs:1480`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1481`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1483`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];`
- `packages/cli/tests/stack-release.test.mjs:1486`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: ``sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);`
- `packages/cli/tests/stack-release.test.mjs:1563`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');`
- `packages/cli/tests/stack-release.test.mjs:1564`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);`
- `packages/cli/tests/stack-release.test.mjs:1584`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);`
- `packages/cli/tests/stack-release.test.mjs:1594`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];`
- `packages/cli/tests/stack-release.test.mjs:1682`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]);`
- `packages/cli/tests/stack-release.test.mjs:1811`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme','--tester-port',TESTER],schema:store,activation:'manual',at:'2026-10-20T00:00:00Z'})+'\n');`
- `packages/cli/tests/stack-release.test.mjs:2198`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `['beta\'s tester port on alpha\'s port',b,['--port',free,'--tester-port',a.port],1,`webui-service: refusing port-claimed: ${held}; nothing was changed\n`],`
- `packages/cli/tests/stack-release.test.mjs:2202`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `['beta\'s tester port on 7333',b,['--port',free,'--tester-port','7333'],4,'webui-service: port-reserved: 7333 is reserved for the default instance (7330 7332 7333)\n'],`
- `packages/cli/tests/stack-release.test.mjs:2217`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused('--tester-port','--tester-port',a.port);`
- `packages/cli/tests/stack-release.test.mjs:2280`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:2290`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',C1,'acme','--tester-socket','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/--tester-socket/);assert.match(r.out,/drop.*--tester-port 7333/);`
- `packages/cli/tests/stack-release.test.mjs:2292`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...base,...pair(proof),'--tester-socket']);assert.match(consoleText(e),/--tester-socket/);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:2319`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.doesNotMatch(conExec(e),/ --send(?: |$)|--tester-port|--public-host|--proof-header/);`
- `packages/cli/tests/stack-release.test.mjs:2388`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.equal(context.argv.includes('--tester-socket'),marked);assert.equal(context.argv.includes('--tester-port'),false);`
- `packages/webui/README.md:1001`: Current retirement notice explicitly states no tester TCP listener. Text: `There is no tester TCP listener. `--tester-port` is retired for this copy.`
- `packages/webui/src/cli.mjs:30`: Current CLI explicit retirement refusal, not an activation path. Text: `if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket');`
- `packages/webui/tests/service.test.mjs:125`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['render', '--tester-port', port, ...PREVIEW, '--send']);`
- `packages/webui/tests/service.test.mjs:127`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));`
- `packages/webui/tests/service.test.mjs:129`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.ok(execStart(r.stdout).endsWith(` ${PREVIEW_FLAGS} --send --tester-port ${port}`));`
- `packages/webui/tests/service.test.mjs:136`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);`
- `packages/webui/tests/service.test.mjs:150`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],`
- `packages/webui/tests/service.test.mjs:151`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],`
- `packages/webui/tests/service.test.mjs:152`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\n'],`
- `packages/webui/tests/service.test.mjs:153`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--port', '7444', '--tester-port', '7444'],`
- `packages/webui/tests/service.test.mjs:154`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', '7444', '--port', '7444'],`
- `packages/webui/tests/service.test.mjs:164`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);`
- `packages/webui/tests/service.test.mjs:172`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const six = await run(dir, ['render', '--tester-port', '007333']);`
- `packages/webui/tests/service.test.mjs:175`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\n');`
- `packages/webui/tests/service.test.mjs:176`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const five = await run(dir, ['render', '--tester-port', '07333']);`
- `packages/webui/tests/service.test.mjs:178`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));`
- `packages/webui/tests/service.test.mjs:186`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...PREVIEW, '--send'], bin);`
- `packages/webui/tests/service.test.mjs:188`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);`
- `packages/webui/tests/service.test.mjs:189`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);`
- `packages/webui/tests/service.test.mjs:199`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);`
- `packages/webui/tests/service.test.mjs:799`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send', '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);`
- `packages/webui/tests/service.test.mjs:813`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const both=await run(dir,['render','--tester-socket','--tester-port','7333']);`
- `packages/webui/tests/tester-cli.test.mjs:25`: Explicit current-CLI negative calls; brief step 5. Text: `for (const flags of [['--tester-port', '7332'], ['--port', '7444', '--tester-port', '7444'], ['--tester-port', '7444', '--port', '7444'], ...['1023', '65536', '1.5', '-1', 'nope'].map(p => ['--tester-port', p])]) {`
- `scripts/case1-accept.sh:71`: New explicit migration refusal for saved old arguments. Text: `--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;`
- `scripts/case2-unit.sh:215`: New explicit migration refusal before rendering units. Text: `--tester-port) die "case2-unit.sh retires tester TCP; use --tester-socket" ;;`
- `scripts/instance.sh:114`: Legacy installed-unit port inventory/parser retained by brief step 5. Text: `# this instance's, whose ExecStart takes PORT as --port or --tester-port.`
- `scripts/instance.sh:132`: Legacy installed-unit port inventory/parser retained by brief step 5. Text: `--tester-port) [[ "$tok" =~ ^[0-9]{1,5}$ ]] && ports+=("$((10#$tok))") ;;`
- `scripts/stack-release.sh:47`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]`
- `scripts/stack-release.sh:63`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the`
- `scripts/stack-release.sh:126`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; TESTER_PORT="$2"; shift 2 ;;`
- `scripts/stack-release.sh:797`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `elif grep -q -- '--tester-port' "$cli"; then supported=1; fi`
- `scripts/stack-release.sh:800`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)); inherited="${BASE_CONSOLE[i]:-}" ;;`
- `scripts/stack-release.sh:814`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `[ -z "$inherited" ] || DROP_TESTER="--tester-port $inherited (replaced by private Unix socket)"`
- `scripts/stack-release.sh:830`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `check_refuse tester-port-invalid "--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other"`
- `scripts/stack-release.sh:832`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `[ "$supported" = 1 ] || check_refuse tester-port-target "$TO's Console does not support --tester-port"`
- `scripts/stack-release.sh:833`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `RUN_CONSOLE=("${without_tester[@]}" --tester-port "$TESTER_PORT")`
- `scripts/stack-release.sh:837`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `RUN_CONSOLE=("${without_tester[@]}") DROP_TESTER="--tester-port $inherited"`
- `scripts/stack-release.sh:878`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# --send and --tester-port follow the pointer, so a --no-send or a tester port set after that`
- `scripts/stack-release.sh:885`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)) ;;`
- `scripts/stack-release.sh:893`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port "${P_CONSOLE[i]:-}") ;;`
- `scripts/stack-release.sh:897`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `SSO_PLAN="sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them"`
- `scripts/stack-release.sh:903`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP.`
- `scripts/stack-release.sh:919`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `case "${kept[i]}" in --send | --tester-port | --tester-socket) break ;; esac`
- `scripts/stack-release.sh:1076`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `case "${without_proof[i]}" in --send | --tester-port | --tester-socket) break ;; esac`
- `scripts/stack-release.sh:1134`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# Row 190: the target Console's --port (7332 when absent) and --tester-port against every other`
- `scripts/stack-release.sh:1142`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) ports+=("${RUN_CONSOLE[i + 1]:-}") ;;`
- `scripts/stack-release.sh:2106`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do [ "${P_CONSOLE[i]}" != --tester-port ] || reserved+=("${P_CONSOLE[i + 1]:-}"); done`
- `scripts/webui-service.sh:43`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `# Console), --tester-socket (private Unix tester), or legacy --tester-port N`
- `scripts/webui-service.sh:74`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `# --tester-port that another installed Console unit in the unit directory`
- `scripts/webui-service.sh:96`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH]`
- `scripts/webui-service.sh:97`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `--tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener.`
- `scripts/webui-service.sh:137`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `--tester-port) [ $# -ge 2 ] || usage; TESTER_PORT="$2"; TESTER_SET=1; SERVE_SET=1; shift 2 ;;`
- `scripts/webui-service.sh:182`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `echo "webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT" >&2`
- `scripts/webui-service.sh:266`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `[ "$TESTER_SET" = 0 ] || flags="$flags --tester-port $TESTER_PORT"`
- `scripts/webui-service.sh:462`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' "$target"; then`
- `scripts/webui-service.sh:467`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `if grep -Eq '^ExecStart=.* --sso( --send)?( --tester-port [0-9]+)?$' "$target"; then`
- `scripts/webui-service.sh:471`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `tester_port="$(sed -n 's/^ExecStart=.* --tester-port \([0-9][0-9]*\)$/\1/p' "$target")"`
Sage-2191 accepted the requested scope and queued it behind the re-pin, 194, 202 and 254 windows. No CLEAR yet. TMPDIR will be created with `mktemp -d /mnt/storage/scratch/tmp/gXXXXXX`. STATIC.json records final syntax/diff results; no behavioral tests have run. No product commit or push has been made before the applicable suites pass.
Grant sage-2279 run1: both intended RED cases observed. First installer GREEN file 35/1 in 17.231 s: EEXIST in the pre-existing outside-secrets test because the shared hub now creates dataRoot. Changed only that mkdir to recursive; its refusal assertion stays unchanged. No product bytes changed. Recorded old/new test SHA in run1/fixture-fix.json. Harness now explicitly selects TAP for machine counts and supports GREEN-only resume. This is the first unexpected failure; a second ends the window.
Grant sage-2279 run1 ended on unexpected failure 2, reported to Sage and Mos as gander-0073, slot returned 2026-10-22T00:34:10Z. Acceptance whole 27/1 in 41.962 s: the broad new od fake intercepted F3 random-token generation as well as the ELF header read. Narrowed only that fake to -An -tx1 -N4; all other calls exec /usr/bin/od. The 12-hex assertion stays unchanged. Product blobs have not changed during the window. No tests run after the stop, and no checks/full suites/shell files started. run1/SUMMARY.json sums all raw logs; sweep count 0. Need a new CLEAR before any rerun.
Sage-2287 approves both test-only fixture fixes and requests their commit with unchanged assertions. Commit contains the two prepared row253 test files and packet, including both old/new test SHA pairs and raw run1 results. Product scripts, README and root BUILD-LOG remain uncommitted. PORT-AUDIT.md/json refresh current line numbers after the od fake grew two lines. The earlier audit above describes the pre-fix candidate. Next CLEAR scope: both whole touched files, M1-M33, cli and webui whole and ten shell scripts. Intended RED carries under unchanged product bytes; no further test has run.
## C4 static gap and addendum
Grant sage-2297: installer whole36/0 in16.449s, acceptance whole28/0 in37.550s, M1-M33 killed33/33 (bases11.808s, changed runs41.202s), CLI whole755/0 in597.084s. Webui was deliberately interrupted after4.736s for a static C4 gap; it is not a failed test under sage-2307 item5 and has no completed total. Ten shells never started. Slot returned with sweep0 as gander-0076. run2/SUMMARY.json is script-summed from the preserved results and raw logs.
The current outer GNU timeout kills its own group, but sudo(8) Process model documents a pty monitor creating a different terminal session. Signal handling says SIGKILL cannot be relayed. The exec fake does not cover that path. No host experiment ran. Duckworth-0109/0110 at2c49dd80c confirms the static gap. DESIGN.md remains unchanged; DESIGN-C4.md proposes the after-uid supervisor and independent outer bound, states the stalled-supervisor residual, compares runtime options and records the old talk watchdog as a proposed bounded follow-up.
Sage-2307/2312 authorizes writing the fake and addendum without tests. The shared case-sudo-fixture.mjs is now imported by both host-script tests. It preserves exec mode and offers a parent/setsid/catchable-signal hop mode with PID/SID records. A new installer counterexample blocks a TERM/HUP-resistant copy under both modes and checks session/PID cleanup; its explicit fixture cleanup only kills recorded fixture PIDs. This draft fake has not run, and no syntax execution is claimed for it. The two script product blobs remain exactly the granted bytes; test bytes now differ for the draft counterexample and shared seam. No repair is implemented.
The next action is Duckworth's static ruling on DESIGN-C4.md and the fake commit, not a test run. If product changes, the completed gate results are historical only and the whole gate reruns on Sage's next CLEAR. C4 now means killed at the inner bound and root never waits past the outer bound; a stopped/killed supervisor and setsid escape remain the same-uid residual. No root cross-session killer is proposed.
For the static pass, c4/*.sh.txt are non-executable snapshots of the exact granted product source. FROZEN-PRODUCT.json pins their equality to the uncommitted source blobs. They are review records, not an implemented repair or a product commit.
## C4 repair prepared for static review
Duckworth-0111 approves DESIGN-C4.md with C1-C5. The repaired scripts now contain the same inline supervisor payload after the uid drop. It uses fixed validated system binaries, starts a query session without setsid -f/-w, confirms SID equals the child PID from the kernel, reads whole stat files, scans every process group in that session, and reaps its direct child. Query bounds are inner 5 s / outer 7 s; peer bounds are 2 s / 4 s. Separate trusted timeout notices distinguish the outer refusal from a child exit status. Both headers name the bounds and tools.
The shared fake retains exec mode and a separate-session parent mode. Exact inner, outer and output-limit reasons are asserted in both modes. The setpgid child has a comm containing a closing parenthesis, space and newline. Session cleanup checks all members of recorded self-led sessions as well as exact recorded PIDs; exec mode never scans the runner's session. Every fixture cleanup kill checks the recorded SID against the kernel immediately before signalling. The stalled-supervisor case deliberately records the same-uid residual, refuses without using stdout, and performs explicit fixture cleanup. The outer timing slack is 2 s. The post-return polling budget remains 50 x 10 ms.
M1-M33 were retargeted where the repair changed source, with M34-M39 for absent inner/outer supervision, group-only cleanup, unbounded read, dropped setsid and one-copy payload drift. All 39 target strings occur exactly once in a static byte check. These are proposed checks, not kill results. The old runs remain historical evidence only: product bytes changed, so the entire gate must rerun on Sage's next CLEAR after the short static pass. No repaired-source syntax check, test, probe or host experiment has run. git diff --check reports no whitespace errors.
F5 now names row 276 / #1781 in docs/plans/DEFERRED.md (sage-2331). That path will be in the row253 manifest. c4/UPSTREAM-PATHS.json records the requested 968db0bcd..ddfb29af7 comparison and the branch-base comparison. Only DEFERRED.md overlaps upstream since c3d88840c; its current upstream text was preserved before appending this row's entry. The branch-to-refactor comparison also lists the unlanded test files on our side. No rebase was performed.
c4/REPAIR.diff, *.repair.txt and REPAIR-SHAS.json freeze the complete repair for Duckworth's C1/C3/C5 static look. The original c4/*.sh.txt and FROZEN-PRODUCT.json stay unchanged. The runnable product and test files remain uncommitted pending the applicable gate; this packet commit is records only. No node_modules path is included.
2026-10-22 02:13:30 UTC: sage-2335 publishes b64aa316a and explicitly requires a committed runnable candidate before the gate. Accordingly the seven repaired source/test/doc paths, the existing BUILD-LOG append and this record are committed now. All seven bytes still match c4/REPAIR-SHAS.json; this commit changes no reviewed packet source. CANDIDATE-SHAS.json is refreshed for the new gate. MANIFEST.sha256 covers the eight product/test/doc/log paths, including DEFERRED.md. No repaired-source verification ran; the static verdict and CLEAR remain pending, and all old results are historical only.
## duckworth-0112 delta, no runs
B1: both supervisor copies use fork-free IFS= read -r -d '' with content-based EOF/error handling. The 250 ms confirmation budget starts after the first full inventory pass. A deterministic stand-in clock test pins this placement without wall timing or real signals; M40 moves the budget back before pass one. All 40 proposed targets statically occur once. No test or syntax execution ran.
M1: BUILD-LOG.md is removed from MANIFEST.sha256. Its row253 phase lines land as a separate doc-only append after code, preserving newer upstream lines, under sage-2342. The manifest now has seven entries. The initial eight-entry manifest is superseded.
N4: SID confirmation now waits within the existing inner watchdog budget rather than only 100 sleeps. It still refuses a vanished/unreadable child or unconfirmed session, and the independent outer bound remains. N1/N2 are explicitly deferred in the existing F5 entry to row276 / #1781, as Sage rules. No case1 analogue checks were added in this delta.
The required gate base is the refactor tip Sage names at CLEAR, 378a76b85 or later. This delta is prepared on the current seat branch for static review only; no old-base gate is authorized. The final integrated gate candidate will get fresh hashes and integration records before execution. c4/delta-0112/ freezes the seven source/test/doc paths, the delta from aa81ca390, and static uniqueness. Prior frozen packets remain unchanged.
Duckworth-0113 approves 761893dac after git reads only (8d3fa3f20). B1 and M1 are closed; real timing remains unverified until the gate. N5 is harmless diagnostic noise on a vanished stat entry: the input redirection can report before stderr is redirected, and tester_capture discards supervisor stderr. No source change is made for it. N6 is recorded with R3: if the inner watchdog expires before kernel SID confirmation, finish has only the known child PID and kills that PID, not an unverified session. Other members may remain. Root still refuses, discards stdout and waits only within the outer bound. This is an explicit limit of cleanup before session identity is established, not a claim that the session was swept. The independent outer bound does not prove cross-session cleanup. Source/test bytes and the seven-entry manifest remain exactly those approved at 761893dac.
@@ -0,0 +1,9 @@
{
"scripts/case2-unit.sh": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"scripts/case1-accept.sh": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"packages/cli/tests/case-sudo-fixture.mjs": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
"packages/cli/tests/case2-unit.test.mjs": "1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44",
"packages/cli/tests/case1-accept.test.mjs": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901",
"packages/webui/README.md": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9",
"docs/plans/DEFERRED.md": "3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f"
}
+147
View File
@@ -0,0 +1,147 @@
# Row253 C4 addendum: the supervisor belongs after sudo
Draft for Duckworth's static ruling. DESIGN.md remains unchanged. Authority:
duckworth-0109/0110, RULING-c4.md at 2c49dd80c, and sage-2307/2312.
No repaired product code or new test has run. Product scripts still have the
original frozen bytes; this addendum does not claim they satisfy C4.
## Finding and evidence
The current tester_capture puts GNU timeout --signal=KILL outside sudo, then
kills that timeout's process group. It works for the exec-style sudo fake.
sudo(8), **Process model**, says the pty monitor creates a new terminal session
and forks the command there. **Signal handling** (called Signals in the local
rendering) says SIGKILL cannot be caught and relayed. Therefore an outer group
kill does not establish that the query in the new session is killed.
Evidence was a static read of the installed sudo(8)/sudoers(5) documentation,
then Duckworth's independent ruling against sudo 1.9.17p2. No real sudo, host
experiment, live unit or live-port experiment ran. The shared fake below is
written but unrun. It models the session boundary, not authentication or a pty.
## Proposed trusted supervisor and runtime trade
Recommend a reviewed inline Bash supervisor, run **after the uid drop**, using
root-validated fixed system Bash, setsid, sleep and kill/builtins. These are
host tools, never binaries or scripts from the selected copy. Resolve system
symlinks to their real targets; require executable root-owned ELF files and
safe root-owned ancestors, outside homes. Do not use PATH lookup for these
supervisor tools. env -i removes BASH_ENV and other injected runtime settings.
Add setsid to the documented host dependencies. No new configuration knob.
This is a proposal under R1's root-validated system-binary option, pending
Duckworth's approval of Bash rather than the example GNU timeout. The exact
inline supervisor must be identical in both host scripts, with budgets and
command argv passed separately. No helper is imported from the copy.
Alternatives weighed:
- GNU timeout alone inside sudo fixes the session hop but kills one process
group. It misses a grandchild that uses setpgid without setsid. It needs the
same owned-session cleanup as the proposed supervisor.
- Validated --node with inline builtins can implement the same supervisor.
The installer already validates it. Acceptance deliberately reports unsafe
--node paths through F9/F10; making that binary a required trusted supervisor
would change those existing outcomes or need another pinned runtime.
- Fixed root-validated system Python can fork a session, multiplex bounded
output and scan /proc cleanly. It is already used for P10, but making it
mandatory for the config query changes missing/unsafe Python from a failed
P10 observation into a preflight refusal. Preserve that behavior here.
## R1: own a query session, not sudo's group
The supervisor starts in sudo's possibly separate session as the stack uid.
It forks the query child, then setsid occurs in that child through the fixed
system setsid binary. The supervisor itself may already be a group leader,
so it must not attempt setsid in its own process. Disable Bash job control;
the newly forked child is not a group leader, so setsid need not fork again.
The child's PID from $! is the kernel session id. No PID comes from copy output.
Run a separate five-second watchdog inside the uid boundary. On expiry, on a
supervisor catchable signal, on output refusal, and after normal query exit:
1. Scan numeric /proc/*/stat entries for the owned session id. Parse after the
final closing parenthesis, so spaces, parentheses and newlines in comm do
not shift the kernel fields. Use the kernel session field, not ancestry.
2. SIGKILL every live PID in that session, including different process groups.
Repeat until no running members remain or a fixed 250 ms cleanup budget
expires. Never select a process from a different session.
3. Wait for the direct child and the watchdog. Refuse on a cleanup limit or
read/parse error that prevents establishing cleanup. Zombies are not running;
only direct children can be reaped here. Tests pin that distinction.
A five-second deadline concerns the inner query. The root-side deadline is
independent. Query bytes are accepted only after success and completed cleanup.
The query can never authorize a root kill: its stdout supplies no PID/session.
The supervisor runs without root authority and kills only its owned session.
## R2: independent root bound and bounded output
Root waits at most seven seconds for the metadata sudo call. Keep GNU timeout
outside sudo for this separate purpose. Keep the 4096-byte cap plus one-byte
overflow detection. All partial output is discarded on any refusal.
Give outer timeout its own operator reason, distinct from inner timeout,
child failure, output limit and cleanup refusal. A child-supplied exit value
alone must not masquerade as proof the outer timer fired. Capture GNU timeout's
trusted verbose signal notice separately, with sudo/helper stderr redirected
away from that channel. The root-owned notice file is outside the user's
home. Validate the combination of notice and status; no eval or source.
Apply the same primitive to the new P10 peer probe, with inner two seconds,
outer four seconds, a 128-byte output cap and the existing one-second socket
connect timeout. Preserve its fixed system-Python safety check and failed-P10
behavior. All observed stdout still requires successful cleanup and exact
pid/uid/gid validation. Neither path sends application bytes to the socket.
## R3: narrowed claim and residual
C4 now means: killed at the inner bound, and root never waits past the outer
bound. A hostile same-uid query can kill/stop the supervisor or setsid away.
A hang in the trusted supervisor has the same outer-bound outcome. In that
case root refuses and discards stdout; the separate session may remain.
This is the existing DESIGN.md:97-99 same-uid limit, not a new closure claim.
No root-side cross-session killer is added. It would require another row.
The stalled-supervisor fake must prove outer timing, its own refusal reason
and no use of partial stdout. It then explicitly kills its recorded leftover
session as fixture cleanup, naming that step as the R3 residual. It must not
claim the product cleaned that session. Other normal-supervisor cases assert
no running members of the recorded query session remain after the call.
## R4 and F5: headers and remaining legacy calls
Both headers will name the inner supervisor, its runtime/tool validation,
five/seven-second query bounds and two/four-second peer-probe bounds.
The old F3 talk path (currently timeout 330 around AS_USER talk) has the same
sudo session-hop issue. Propose recording it as a bounded follow-up, rather
than changing it in this slice: it predates the socket metadata/probe helpers,
drives a 300-second functional conversation and simultaneous F5 scan, and has
existing stdout/stderr diagnostics. It is not part of the new five-second
config query. This is an explicit F5 scope choice for Duckworth/Sage to accept
or reject, not a claim the old talk timer kills a separate session. Likewise,
the other legacy AS_USER diagnostics are not newly supervised by this row.
## Shared fake and next evidence
packages/cli/tests/case-sudo-fixture.mjs provides the one process-model seam.
Both existing test files import it. Mode exec retains the prior fake. Mode
hop keeps a parent, forks through setsid, records kernel PID and SID, and
relays TERM/HUP/INT only. It cannot relay KILL. The installer draft case makes
the copy ignore TERM/HUP and block; it checks recorded query PIDs and the
separate fake session, then explicitly cleans fixture-owned PIDs on failure.
This is written but not run, and is expected to expose the old product gap.
Before the next whole gate, complete both-mode cases for timeout, TERM/HUP,
setpgid grandchild, overflow, and the distinct stalled-supervisor residual.
Both host-script paths use this seam. Add changes for absent inner supervisor,
absent outer bound, leader-group-only cleanup and unbounded read. The setpgid
case must kill the group-only change. Record every restore and raw log.
Duckworth reviews this addendum and fake statically first. Product code then
implements the approved mechanism. All product changes void prior results
under sage-2307/mos-1200. Only Sage's next CLEAR permits a diagnostic or gate
run. The next gate is both whole touched files, all checks, CLI/WebUI whole
and all ten shell files. Existing RED evidence carries only if applicable
product bytes remain equal. No host deployment is authorized by this packet.
+248
View File
@@ -0,0 +1,248 @@
# Row 253: private tester socket in the host scripts
Gander, 2026-10-21. F27, #1758. Base c3d88840c, after row196 landing
7247d80c7. Brief: docs/plans/2026-10-21_tester-socket-scripts.md at
0cdead66c. Reviewer Duckworth; gate owner Sage. Status: proposed, no product
code or tests changed. Claim gander-253-start is queue rev2995.
## Outcome and scope
The factory installer renders a bare --tester-socket flag instead of a TCP
port. Case1 acceptance consumes that saved form and examines the private
listener without claiming that a private pathname defeats hostile same-uid
processes. The library profile still has no tester. No .53 or .54 host may
take a copy carrying196 before253 lands, per mos-1198.
Change scripts/case2-unit.sh, scripts/case1-accept.sh and their two existing
CLI test files. Their comments/usage and fixture docs move with the code.
Do not edit the deliberate legacy paths named in the brief. No runtime bus,
Console, role, grant, control-board, dependency or systemd-template change.
No live installer or acceptance run, provider, home/config, unit or host access.
## Choices
1. Recommended: resolve the configured dataRoot as the stack user, preserve
hardening, check the final pathname's metadata and use SO_PEERCRED on a
connection to identify its listener. This supports custom roots within the
unit's existing writable home and matches196's kernel-peer approach.
2. Assume <home>/.mosaic-dev and inspect a pathname in /proc/net/unix. This
misses a custom config and is not a reliable final-path ownership check:
196 binds a temporary name and renames it. A socket's filesystem inode also
must not be confused with its kernel socket inode. Do not use this option.
3. Add a TCP relay, relax hardening or give acceptance a configurable probe
executable. All widen the surface or trust inputs and are outside this row.
There is no second bus principal verifier. The acceptance probe observes an
OS endpoint; it grants no capability and sends no HTTP or bus request.
## Installer arguments and preflight
Factory keeps required --port (1..1023) and --business; tester remains optional
for the installer. --tester-socket is a single marker, takes no value and keeps
its position in the rendered flags. The existing duplicate-flag check applies.
A following stray value is an unknown flag. --tester-port always refuses in
serve arguments, for both profiles, before unit writes or systemctl actions:
case2-unit: case2-unit.sh retires tester TCP; use --tester-socket
Library rejects --tester-socket with an explicit library-profile refusal. Its
other flags, proof-file checks and install behavior are unchanged. No legacy
conversion or fallback to TCP is introduced in this installer.
When the factory socket marker is present, a small inline metadata query runs
the selected copy's config loader under the target uid with sudo -u USER -H,
env -i, explicit HOME and the same pinned-node-first PATH used by the unit.
Root never imports user-writable copy code as root. It invokes the previously
validated --node path after dropping uid, with the copy module path as a
separate argument. This resolves the default instance just as the rendered
unit does; no MOSAIC_CONFIG or named-instance override is inherited.
The query returns only dataRoot and whether that copy's Console carries the
MOSAIC_TESTER_SOCKET_V1 marker. It prints no config values, credentials or child
diagnostics. Bound it to five seconds and 4096 output bytes; reject errors,
extra output/keys, malformed results, missing config and unsupported targets.
No root process opens a config or credential under the user's home. Existing
root metadata checks remain root metadata checks. Both script headers explain
the new, explicitly unprivileged config query.
Before any unit writes, validate the socket location:
- dataRoot is an absolute normalized plain path, below the declared home,
with no symlink in its existing path. Never expand the unit's writable scope.
- Neither home nor dataRoot is under /home, /root, /run/user, /tmp or /var/tmp.
These roots conflict with the existing ProtectHome/PrivateTmp contract.
- <dataRoot>/console/tester.sock is at most 107 ASCII bytes, matching196.
- A missing console parent is allowed; runtime creates it. An existing parent
must be a real directory, not a symlink, owned by the target uid with no
group/other permissions. A foreign owner, non-directory or unsafe mode refuses.
- Do not create/chmod the parent or inspect/delete an existing tester entry in
the installer. A running Console may own it during a re-pin. Runtime196 owns
binding and cleanup. Reinstall still does not restart running units.
The dataRoot query and all refusal families get fake-only tests. A failed
preflight leaves both units and the systemctl log unchanged.
## How the rendered unit reaches the socket
The unit still has User/Group for the stack account, ProtectSystem=strict,
ReadWritePaths=<home>, ProtectHome=yes and PrivateTmp=yes, in the same order.
Its ExecStart contains --tester-socket, with no pathname argument. The Console
loads dataRoot and derives console/tester.sock itself. Case1/2's /srv/mosaic
home is outside ProtectHome's protected hierarchies; the allowed dataRoot is
inside the existing writable home, and the socket is outside private /tmp.
No extra capability or writable directory is added. Systemd's documented
namespace rules motivate the location refusals, rather than a hardening change.
[systemd.exec](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html)
A mutable same-uid config can change between preflight and start. The unit's
writable boundary is still home; runtime's own config/socket checks still
refuse unsafe state. This row does not promise an immutable same-uid config.
The host window must verify the rendered unit actually starts under its
namespace; scratch tests inspect its directives, not a real systemd namespace.
## Acceptance arguments and endpoint observation
The saved args must contain one --tester-socket and the numeric --port.
Reject a missing/duplicate marker, a marker with an old numeric value, and
--tester-port with the same migration wording naming case1-accept.sh. Preserve
all existing pre-- argument validation and the default-instance contract.
Malformed saved inputs still refuse with exit2 before functional checks.
Acceptance resolves the actual configured tester dataRoot with the same
unprivileged, bounded query. It does not silently use the default path. An
unreadable/invalid config or a root outside the unit's allowed home refuses
before the functional talk. The old DATA_ROOT used by F11 remains unchanged;
generalizing that separate Discord check is outside this row.
P10 independently requires the Console's systemd MainPID to be positive and
stable, and its four /proc uid fields to be the stack uid. Check the private
parent's type, owner and mode, and the final entry's non-symlink socket type,
owner and exact 0600 mode. Snapshot parent/socket dev+ino and metadata before
and after the peer probe; a replacement or MainPID change fails observation.
The port floor must still be numeric and above the privileged main Console
port, retaining that existing part of the observation.
The bounded probe runs as the stack user with an empty inherited environment,
the fixed system /usr/bin/python3 resolved to a root-owned ELF, safe root-owned
ancestors, and -I -S. This is the system Python dependency already required by
196; never use PATH python, a home shim or a configurable helper. Unsafe/missing
Python is a failed P10 observation, not a fallback. Resolve/check the system
entry and target using metadata and the ELF header; do not run copy Python code
as root. Test seams emulate these root facts and the system executable.
The probe opens AF_UNIX/SOCK_STREAM, uses a one-second connect timeout, reads
SO_PEERCRED and closes. An outer three-second process-group bound applies. It
returns only pid/uid/gid, with exact structure, bounded output and numeric
validation. pid must equal MainPID and uid the stack uid; gid is validated but
not an identity grant. No application bytes, cookies, credentials or proof
envelope are sent. Linux supplies the peer credentials for the connected
stream, so the check does not depend on the temporary bind pathname.
[unix(7), SO_PEERCRED](https://man7.org/linux/man-pages/man7/unix.7.html)
This is a snapshot. Hostile same-uid processes can alter same-uid state or
interfere with the unprivileged probe. Root, ptrace and later pathname changes
are not defeated here. The probe does not prove another uid is denied; that
remains a host-window observation, not a fake test claim.
## P8 and P10 outcome lines
P8 must change with removal of TESTER_PORT. It checks the main Console port is
loopback-only and held only by MainPID, and that nothing listens on TCP7331 or
the retired TCP7333. Socket ownership belongs to P10. Its healthy line is:
P8 lan-listeners closed <port> is on 127.0.0.1 only by MainPID <pid>; no TCP listener on 7331 or 7333; tester uses <socket>
An unexpected listener, missing main listener or wrong owner produces P8
lan-listeners unnamed with the observed listener summaries, as today.
Do not reuse accepted mos-1101 for a new Unix-socket claim: that ruling named a
TCP-port condition. Proposed safe default pending any new Mos classification:
P10 tester-socket-squat unnamed boundary=observed path=<socket> parent=0700 owner=<uid> socket=0600 peer_pid=<pid> peer_uid=<uid> floor=<floor> port=<port>; hostile same-uid substitution remains
Any missing/unsafe metadata, wrong peer, unsafe interpreter, malformed/timeout
probe, floor miss or changed snapshot produces one P10 line instead:
P10 tester-socket-squat unnamed boundary=failed reason=<bounded-code> path=<socket> MainPID=<pid>; hostile same-uid substitution remains
Healthy observation does not mark the same-uid squat path closed or invent an
accepted ruling. P checks still never change exit status. Tests pin the reason
and observed/failed distinction, so validation cannot disappear behind an
always-unnamed mark. Sage/Mos may supply a new classification at design review;
it must be explicit before changing this proposed mark. No Jason question is
needed merely to retain unnamed evidence.
## Evidence and planned checks
Keep the existing scratch fixtures, fake account database/systemctl/curl/ss/
sudo/stat and fake interpreter. Add copy config/marker fixtures and probe
metadata under those same roots. No test connects to a live socket or runs the
real host scripts without their existing seams. Fixture fakes verify the
unprivileged command argv; no test claims a real uid drop or namespace.
The fake system interpreter executes the actual probe payload against a fake
Python socket API, checking AF_UNIX/SOCK_STREAM, timeout, connect, getsockopt
arguments and returned peer bytes. This catches errors in the inline probe
without opening an OS listener. The metadata query is tested with nonzero
status plus valid-looking output as well as malformed output, so removing its
status refusal cannot hide behind a later parse error.
| Checks | Required observations |
|---|---|
| T1 factory render | Bare marker exactly once, argument order retained, no TCP flag, hardening unchanged; duplicate/stray value refuses |
| T2 profile/migration | Both profiles refuse retired TCP; library refuses socket; all refuse before writes/start |
| T3 config/target | Invalid/missing/noisy/timed-out metadata and unmarked target refuse; query uses target uid and clean environment |
| T4 location | Default and custom in-home roots work; outside-home, protected/private hierarchies, symlinks and >107-byte pathname refuse |
| T5 parent | Missing parent allowed; file/symlink/foreign owner/group-other mode refuse without mutation or systemctl calls |
| T6 saved args | Required socket marker; missing/duplicate/stray value and retired TCP refuse exit2 before talk |
| T7 P8 | Main-only loopback succeeds; LAN/wrong owner/7331 or7333 listener gives unnamed |
| T8 P10 | Exact metadata, stable MainPID and matching peer produce boundary=observed; custom configured root used |
| T9 failed P10 | Parent/socket faults, wrong PID/uid, malformed/timeout probe, unsafe Python, bad floor and changed snapshot each give failed reason |
| T10 preserved behavior | Functional checks, their exit semantics, library behavior and all other P paths retained |
Planned checks to kill, one target change at a time in isolated snapshots:
| IDs | Removed requirement |
|---|---|
| M1-M3 | Installer TCP refusal, library socket refusal, duplicate socket refusal |
| M4-M5 | Config-query failure refusal, unsupported-target refusal |
| M6-M8 | Home containment, protected/private path refusal,107-byte limit |
| M9-M11 | Existing parent type/symlink, uid, mode checks |
| M12-M15 | Acceptance TCP refusal, required marker, duplicate/stray marker rejection, config-query refusal |
| M16 | P8 retired TCP7333 check |
| M17-M18 | P10 parent and final socket metadata checks |
| M19-M21 | Peer PID, peer uid, malformed/timeout probe refusal |
| M22-M23 | Safe fixed system interpreter, stable metadata/MainPID snapshot |
| M24-M25 | Target-uid invocation in installer and acceptance metadata/probe paths |
| M26 | Privileged main-port floor observation |
| M27 | Probe returns its own PID rather than the connected peer's credentials |
Before execution, give every definition one unique source target and a named
killer test. Preserve raw logs, base times, syntax preflight and byte-identical
SHA/Git restoration. A timeout counts only under Sage's explicit rules and
must kill the process group. Refusal variants in T1-T9 must not be merely
assertions of a generic nonzero exit.
## Static inventory and gate
After implementation, enumerate every --tester-port hit in scripts/packages
in BUILD.md and a machine-readable file. The only permitted hits are the brief's
deliberate legacy producers/parsers/tests and this row's explicit refusals and
negative fixtures. Inspect arguments and generated units, not just filenames.
Readmes must describe retirement rather than produce old argv. No modification
to webui-service, stack-release, instance, their tests/fixture, case1-hostcheck
or tester-cli negative calls is planned. Stop and ask Sage if one is needed.
Then request Sage's slot. Under that grant, run CLI whole, WebUI whole, every
shell test suite whole and all named checks above. Use an isolated candidate,
plain linked node_modules, a short gXXX TMPDIR under /mnt/storage/scratch/tmp,
busy &&, pipefail and tee. Single/pattern runs need the slot too, per sage-2142.
No harness/helper/log argument may match the busy detector pattern, per
mos-1205. Two failures end the window; report any stop to Sage/Mos before a
retry. Return the slot with script-summed counts/timings and a leftover sweep.
Design approval precedes code. The manifest, immutable candidate hashes,
static inventory and gate evidence go to Duckworth before Sage's landing.
Queue moves use the helper, with a coordinated queue-write gap. Sage posts the
jarvis review request after the in-review move. BUILD-LOG/session history stays
append-only; Sage continues session registration. No host deployment is part
of row253 acceptance.
+7
View File
@@ -0,0 +1,7 @@
# Row253 committed C4 candidate manifest
MANIFEST.sha256 names seven source/test/doc landing paths and pins their bytes. Its digest is the review identifier. BUILD-LOG.md is excluded: land the row253 phase lines as a doc-only append after the code, preserving upstream appends (duckworth-0112 M1, sage-2342).
Base before the required gate integration: c3d88840c. Initial static repair packet: b64aa316a; initial runnable candidate: aa81ca390. The delta packet is c4/delta-0112/. Full records live under agents/gander/work/queue-253/ as review evidence. DEFERRED.md preserves upstream entries and names row276 / #1781 for F5 and N1/N2.
Sage-2335 authorizes the runnable commit before the whole gate. No repaired-source syntax check, test, probe or host experiment has run. No results from 8a8df4171 carry. After Duckworth's delta static verdict, the gate must use the candidate integrated onto the refactor tip named by Sage at CLEAR (378a76b85 or later under sage-2342), never this old base. Fresh integration records and hashes will identify that final gate candidate.
@@ -0,0 +1,7 @@
3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f docs/plans/DEFERRED.md
b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008 packages/cli/tests/case-sudo-fixture.mjs
6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901 packages/cli/tests/case1-accept.test.mjs
1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44 packages/cli/tests/case2-unit.test.mjs
2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9 packages/webui/README.md
c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd scripts/case1-accept.sh
d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b scripts/case2-unit.sh
+37
View File
@@ -0,0 +1,37 @@
# Row253 implementation plan
Goal: migrate the two host scripts to the private tester socket without widening
their unit or root execution boundary. Spec: DESIGN.md, approved by
duckworth-0100 with C1-C5; Sage2173 approves the two design decisions.
Execution: Gander implements inline; Duckworth reviews the final manifest.
Constraints: four product/test files only; no edits to the brief's legacy paths;
no tests before Sage's slot, including patterns. Static inspection precedes the
full slot request. All tests use the existing scratch/fake seams.
- [x] Task1: write factory/acceptance socket fixture and refusal tests first.
Add config/marker and isolated probe fakes; assert target-uid argv, strict
parsing, all C1 path variants and C4 descendant cleanup. Preserve baseline
functional/P checks while changing only the intended socket outcomes.
- [x] Task2: implement the marker parsing, bounded target-uid query, strict bash
parser, component/no-follow location checks and parent preflight in installer.
Preserve hardening and update its header, including sudo dependency.
- [x] Task3: migrate saved args and P8/P10. Implement metadata snapshots and the
bounded isolated system-Python peer observation; keep P10 unnamed with explicit
observed/failed evidence and same-uid residual. Update header and remove the
obsolete accepted-mos-1101 mark. Keep F11 default DATA_ROOT behavior unchanged.
- [x] Task4: static audit every remaining retired-port occurrence; prepare unique
M1-M33 including C1 middle-symlink/sibling-prefix, C2 character, C3 parser and C4
process-group checks. Request the slot once the candidate static check passes.
- [ ] Task5: on Sage's grant, execute RED cases against isolated prior behavior,
GREEN candidate checks, CLI/WebUI whole, all shell files and the checks. Record
counts/times, clean restoration, leak sweep and exact frozen manifest. Return
the slot, then queue review only in a coordinated write gap.
Review focus: prefix collisions; intermediate symlinks; forged output lines;
nonzero query with valid-looking JSON; ignored signals/leftover descendants;
kernel versus filesystem inode; wrong Console peer or metadata replacement.
Ruling: tests are written first, but their execution waits for the user-required
slot after static preparation. No pre-slot test or passing claim; RED/GREEN
execution is an explicit part of the granted verification, not claimed in advance.
@@ -0,0 +1,768 @@
{
"command": "git grep -n -- --tester-port scripts packages",
"hits": 127,
"entries": [
{
"file": "packages/cli/tests/case1-accept.test.mjs",
"line": 596,
"text": " \"retired TCP\": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\\n')+'\\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],",
"reason": "New negative saved-argument fixture asserting the migration refusal."
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 68,
"text": " \"[Service]\\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\\n\");",
"reason": "Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged."
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 215,
"text": " put(join(h.root, \"etc\", \"systemd\", \"system\", \"mosaic-webui.service\"), \"[Service]\\nExecStart=/x serve --business mosaic-stack --tester-port 7333\\n\");",
"reason": "Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged."
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 445,
"text": " [\"--tester-port\", \"7333\", /case2-unit.sh retires tester TCP; use --tester-socket/],",
"reason": "New negative factory/library fixtures asserting the migration refusal."
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 496,
"text": " refuses(unit(h, { profile: \"factory\", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);",
"reason": "New negative factory/library fixtures asserting the migration refusal."
},
{
"file": "packages/cli/tests/stack-release-external.test.mjs",
"line": 75,
"text": " for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){",
"reason": "Legacy stack-release external refusal fixture, excluded by brief step 5."
},
{
"file": "packages/cli/tests/stack-release-fixture.mjs",
"line": 306,
"text": " for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\\n// serve accepts '--tester-port'\\n// serve accepts '--proof-header' (row 178)\\n${[C1,C].includes(id)?\"// serve accepts '--sso' (row 192)\\n\":''}`:'// fixture Console before tester listener, never run\\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){",
"reason": "Retained pre-196 copy fixture for legacy activation and rollback."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 681,
"text": "const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 687,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 688,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 690,
"text": " r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 692,
"text": " assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 693,
"text": " assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 694,
"text": " r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 697,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 702,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 705,
"text": " assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 708,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 709,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 714,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 718,
"text": " assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 724,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 731,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 732,
"text": " r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 736,
"text": " edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 738,
"text": " edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 740,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 741,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 746,
"text": " let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 749,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 821,
"text": " const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 827,
"text": " const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 830,
"text": " const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 839,
"text": " const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 843,
"text": " await t.test('a target Console without --tester-port',async(t)=>{",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 846,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 848,
"text": " assert.deepEqual(r.out.split('\\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 850,
"text": " await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1056,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1057,
"text": " assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1058,
"text": " assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1129,
"text": " await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1132,
"text": " let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1135,
"text": " assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1137,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1138,
"text": " assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1141,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1162,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1351,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1352,
"text": " assert.deepEqual(want.slice(-2),['--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1367,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1468,
"text": "const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1480,
"text": " let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1481,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1483,
"text": " const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1486,
"text": " `sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1563,
"text": " const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1564,
"text": " assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1584,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1594,
"text": " const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1682,
"text": " assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1811,
"text": " writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme','--tester-port',TESTER],schema:store,activation:'manual',at:'2026-10-20T00:00:00Z'})+'\\n');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2198,
"text": " ['beta\\'s tester port on alpha\\'s port',b,['--port',free,'--tester-port',a.port],1,`webui-service: refusing port-claimed: ${held}; nothing was changed\\n`],",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2202,
"text": " ['beta\\'s tester port on 7333',b,['--port',free,'--tester-port','7333'],4,'webui-service: port-reserved: 7333 is reserved for the default instance (7330 7332 7333)\\n'],",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2217,
"text": " await refused('--tester-port','--tester-port',a.port);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2280,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2290,
"text": " r=await run(e,'activate',C1,'acme','--tester-socket','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/--tester-socket/);assert.match(r.out,/drop.*--tester-port 7333/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2292,
"text": " assert.deepEqual(pointer(e).console,[...base,...pair(proof),'--tester-socket']);assert.match(consoleText(e),/--tester-socket/);assert.doesNotMatch(consoleText(e),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2319,
"text": " assert.doesNotMatch(conExec(e),/ --send(?: |$)|--tester-port|--public-host|--proof-header/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2388,
"text": " assert.equal(context.argv.includes('--tester-socket'),marked);assert.equal(context.argv.includes('--tester-port'),false);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/webui/README.md",
"line": 1001,
"text": "There is no tester TCP listener. `--tester-port` is retired for this copy.",
"reason": "Current retirement notice explicitly states no tester TCP listener."
},
{
"file": "packages/webui/src/cli.mjs",
"line": 30,
"text": " if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket');",
"reason": "Current CLI explicit retirement refusal, not an activation path."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 125,
"text": " const r = await run(dir, ['render', '--tester-port', port, ...PREVIEW, '--send']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 127,
"text": " assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 129,
"text": " assert.ok(execStart(r.stdout).endsWith(` ${PREVIEW_FLAGS} --send --tester-port ${port}`));",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 136,
"text": " const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 150,
"text": " ['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 151,
"text": " ['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 152,
"text": " ['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\\n'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 153,
"text": " ['--port', '7444', '--tester-port', '7444'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 154,
"text": " ['--tester-port', '7444', '--port', '7444'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 164,
"text": " assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 172,
"text": " const six = await run(dir, ['render', '--tester-port', '007333']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 175,
"text": " assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\\n');",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 176,
"text": " const five = await run(dir, ['render', '--tester-port', '07333']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 178,
"text": " assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 186,
"text": " const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...PREVIEW, '--send'], bin);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 188,
"text": " assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 189,
"text": " assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 199,
"text": " const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 799,
"text": " const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send', '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 813,
"text": " const both=await run(dir,['render','--tester-socket','--tester-port','7333']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/tester-cli.test.mjs",
"line": 25,
"text": " for (const flags of [['--tester-port', '7332'], ['--port', '7444', '--tester-port', '7444'], ['--tester-port', '7444', '--port', '7444'], ...['1023', '65536', '1.5', '-1', 'nope'].map(p => ['--tester-port', p])]) {",
"reason": "Explicit current-CLI negative calls; brief step 5."
},
{
"file": "scripts/case1-accept.sh",
"line": 71,
"text": " --tester-port) refuse \"case1-accept.sh retires tester TCP; use --tester-socket\" ;;",
"reason": "New explicit migration refusal for saved old arguments."
},
{
"file": "scripts/case2-unit.sh",
"line": 215,
"text": " --tester-port) die \"case2-unit.sh retires tester TCP; use --tester-socket\" ;;",
"reason": "New explicit migration refusal before rendering units."
},
{
"file": "scripts/instance.sh",
"line": 114,
"text": "# this instance's, whose ExecStart takes PORT as --port or --tester-port.",
"reason": "Legacy installed-unit port inventory/parser retained by brief step 5."
},
{
"file": "scripts/instance.sh",
"line": 132,
"text": " --tester-port) [[ \"$tok\" =~ ^[0-9]{1,5}$ ]] && ports+=(\"$((10#$tok))\") ;;",
"reason": "Legacy installed-unit port inventory/parser retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 47,
"text": " stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 63,
"text": "activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 126,
"text": " --tester-port) [ \"$OP\" = activate ] && [ $# -ge 2 ] && [ -n \"$2\" ] || usage; TESTER_PORT=\"$2\"; shift 2 ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 797,
"text": " elif grep -q -- '--tester-port' \"$cli\"; then supported=1; fi",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 800,
"text": " --tester-port) i=$((i + 1)); inherited=\"${BASE_CONSOLE[i]:-}\" ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 814,
"text": " [ -z \"$inherited\" ] || DROP_TESTER=\"--tester-port $inherited (replaced by private Unix socket)\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 830,
"text": " check_refuse tester-port-invalid \"--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 832,
"text": " [ \"$supported\" = 1 ] || check_refuse tester-port-target \"$TO's Console does not support --tester-port\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 833,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\" --tester-port \"$TESTER_PORT\")",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 837,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\") DROP_TESTER=\"--tester-port $inherited\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 878,
"text": " # --send and --tester-port follow the pointer, so a --no-send or a tester port set after that",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 885,
"text": " --tester-port) i=$((i + 1)) ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 893,
"text": " --tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port \"${P_CONSOLE[i]:-}\") ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 897,
"text": " SSO_PLAN=\"sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 903,
"text": "# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP.",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 919,
"text": " case \"${kept[i]}\" in --send | --tester-port | --tester-socket) break ;; esac",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 1076,
"text": " case \"${without_proof[i]}\" in --send | --tester-port | --tester-socket) break ;; esac",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 1134,
"text": "# Row 190: the target Console's --port (7332 when absent) and --tester-port against every other",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 1142,
"text": " --tester-port) ports+=(\"${RUN_CONSOLE[i + 1]:-}\") ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 2106,
"text": " for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do [ \"${P_CONSOLE[i]}\" != --tester-port ] || reserved+=(\"${P_CONSOLE[i + 1]:-}\"); done",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 43,
"text": "# Console), --tester-socket (private Unix tester), or legacy --tester-port N",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 74,
"text": "# --tester-port that another installed Console unit in the unit directory",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 96,
"text": " FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH]",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 97,
"text": " --tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener.",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 137,
"text": " --tester-port) [ $# -ge 2 ] || usage; TESTER_PORT=\"$2\"; TESTER_SET=1; SERVE_SET=1; shift 2 ;;",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 182,
"text": " echo \"webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT\" >&2",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 266,
"text": " [ \"$TESTER_SET\" = 0 ] || flags=\"$flags --tester-port $TESTER_PORT\"",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 462,
"text": " if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' \"$target\"; then",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 467,
"text": " if grep -Eq '^ExecStart=.* --sso( --send)?( --tester-port [0-9]+)?$' \"$target\"; then",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 471,
"text": " tester_port=\"$(sed -n 's/^ExecStart=.* --tester-port \\([0-9][0-9]*\\)$/\\1/p' \"$target\")\"",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
}
]
}
+131
View File
@@ -0,0 +1,131 @@
# Row253 retired-port audit
Current static source audit after the two approved fixture fixes. Each remaining hit and its reason follows.
- `packages/cli/tests/case1-accept.test.mjs:596`: New negative saved-argument fixture asserting the migration refusal. Text: `"retired TCP": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\n')+'\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],`
- `packages/cli/tests/case1-hostcheck.test.mjs:68`: Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged. Text: `"[Service]\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\n");`
- `packages/cli/tests/case1-hostcheck.test.mjs:215`: Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged. Text: `put(join(h.root, "etc", "systemd", "system", "mosaic-webui.service"), "[Service]\nExecStart=/x serve --business mosaic-stack --tester-port 7333\n");`
- `packages/cli/tests/case2-unit.test.mjs:445`: New negative factory/library fixtures asserting the migration refusal. Text: `["--tester-port", "7333", /case2-unit.sh retires tester TCP; use --tester-socket/],`
- `packages/cli/tests/case2-unit.test.mjs:496`: New negative factory/library fixtures asserting the migration refusal. Text: `refuses(unit(h, { profile: "factory", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);`
- `packages/cli/tests/stack-release-external.test.mjs:75`: Legacy stack-release external refusal fixture, excluded by brief step 5. Text: `for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){`
- `packages/cli/tests/stack-release-fixture.mjs:306`: Retained pre-196 copy fixture for legacy activation and rollback. Text: `for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\n// serve accepts '--tester-port'\n// serve accepts '--proof-header' (row 178)\n${[C1,C].includes(id)?"// serve accepts '--sso' (row 192)\n":''}`:'// fixture Console before tester listener, never run\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){`
- `packages/cli/tests/stack-release.test.mjs:681`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];`
- `packages/cli/tests/stack-release.test.mjs:687`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:688`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:690`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:692`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);`
- `packages/cli/tests/stack-release.test.mjs:693`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);`
- `packages/cli/tests/stack-release.test.mjs:694`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);`
- `packages/cli/tests/stack-release.test.mjs:697`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:702`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:705`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(r.out,/drop[^\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:708`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:709`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:714`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:718`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:724`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);`
- `packages/cli/tests/stack-release.test.mjs:731`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:732`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:736`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;`
- `packages/cli/tests/stack-release.test.mjs:738`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');`
- `packages/cli/tests/stack-release.test.mjs:740`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:741`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:746`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:749`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:821`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;`
- `packages/cli/tests/stack-release.test.mjs:827`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);`
- `packages/cli/tests/stack-release.test.mjs:830`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);`
- `packages/cli/tests/stack-release.test.mjs:839`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');`
- `packages/cli/tests/stack-release.test.mjs:843`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await t.test('a target Console without --tester-port',async(t)=>{`
- `packages/cli/tests/stack-release.test.mjs:846`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');`
- `packages/cli/tests/stack-release.test.mjs:848`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(r.out.split('\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);`
- `packages/cli/tests/stack-release.test.mjs:850`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);`
- `packages/cli/tests/stack-release.test.mjs:1056`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1057`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1058`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));`
- `packages/cli/tests/stack-release.test.mjs:1129`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{`
- `packages/cli/tests/stack-release.test.mjs:1132`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1135`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));`
- `packages/cli/tests/stack-release.test.mjs:1137`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1138`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));`
- `packages/cli/tests/stack-release.test.mjs:1141`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1162`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);`
- `packages/cli/tests/stack-release.test.mjs:1351`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);`
- `packages/cli/tests/stack-release.test.mjs:1352`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(want.slice(-2),['--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1367`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);`
- `packages/cli/tests/stack-release.test.mjs:1468`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;`
- `packages/cli/tests/stack-release.test.mjs:1480`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1481`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1483`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];`
- `packages/cli/tests/stack-release.test.mjs:1486`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: ``sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);`
- `packages/cli/tests/stack-release.test.mjs:1563`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');`
- `packages/cli/tests/stack-release.test.mjs:1564`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);`
- `packages/cli/tests/stack-release.test.mjs:1584`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);`
- `packages/cli/tests/stack-release.test.mjs:1594`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];`
- `packages/cli/tests/stack-release.test.mjs:1682`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]);`
- `packages/cli/tests/stack-release.test.mjs:1811`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme','--tester-port',TESTER],schema:store,activation:'manual',at:'2026-10-20T00:00:00Z'})+'\n');`
- `packages/cli/tests/stack-release.test.mjs:2198`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `['beta\'s tester port on alpha\'s port',b,['--port',free,'--tester-port',a.port],1,`webui-service: refusing port-claimed: ${held}; nothing was changed\n`],`
- `packages/cli/tests/stack-release.test.mjs:2202`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `['beta\'s tester port on 7333',b,['--port',free,'--tester-port','7333'],4,'webui-service: port-reserved: 7333 is reserved for the default instance (7330 7332 7333)\n'],`
- `packages/cli/tests/stack-release.test.mjs:2217`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused('--tester-port','--tester-port',a.port);`
- `packages/cli/tests/stack-release.test.mjs:2280`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:2290`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',C1,'acme','--tester-socket','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/--tester-socket/);assert.match(r.out,/drop.*--tester-port 7333/);`
- `packages/cli/tests/stack-release.test.mjs:2292`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...base,...pair(proof),'--tester-socket']);assert.match(consoleText(e),/--tester-socket/);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:2319`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.doesNotMatch(conExec(e),/ --send(?: |$)|--tester-port|--public-host|--proof-header/);`
- `packages/cli/tests/stack-release.test.mjs:2388`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.equal(context.argv.includes('--tester-socket'),marked);assert.equal(context.argv.includes('--tester-port'),false);`
- `packages/webui/README.md:1001`: Current retirement notice explicitly states no tester TCP listener. Text: `There is no tester TCP listener. `--tester-port` is retired for this copy.`
- `packages/webui/src/cli.mjs:30`: Current CLI explicit retirement refusal, not an activation path. Text: `if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket');`
- `packages/webui/tests/service.test.mjs:125`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['render', '--tester-port', port, ...PREVIEW, '--send']);`
- `packages/webui/tests/service.test.mjs:127`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));`
- `packages/webui/tests/service.test.mjs:129`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.ok(execStart(r.stdout).endsWith(` ${PREVIEW_FLAGS} --send --tester-port ${port}`));`
- `packages/webui/tests/service.test.mjs:136`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);`
- `packages/webui/tests/service.test.mjs:150`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],`
- `packages/webui/tests/service.test.mjs:151`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],`
- `packages/webui/tests/service.test.mjs:152`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\n'],`
- `packages/webui/tests/service.test.mjs:153`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--port', '7444', '--tester-port', '7444'],`
- `packages/webui/tests/service.test.mjs:154`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', '7444', '--port', '7444'],`
- `packages/webui/tests/service.test.mjs:164`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);`
- `packages/webui/tests/service.test.mjs:172`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const six = await run(dir, ['render', '--tester-port', '007333']);`
- `packages/webui/tests/service.test.mjs:175`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\n');`
- `packages/webui/tests/service.test.mjs:176`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const five = await run(dir, ['render', '--tester-port', '07333']);`
- `packages/webui/tests/service.test.mjs:178`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));`
- `packages/webui/tests/service.test.mjs:186`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...PREVIEW, '--send'], bin);`
- `packages/webui/tests/service.test.mjs:188`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);`
- `packages/webui/tests/service.test.mjs:189`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);`
- `packages/webui/tests/service.test.mjs:199`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);`
- `packages/webui/tests/service.test.mjs:799`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send', '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);`
- `packages/webui/tests/service.test.mjs:813`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const both=await run(dir,['render','--tester-socket','--tester-port','7333']);`
- `packages/webui/tests/tester-cli.test.mjs:25`: Explicit current-CLI negative calls; brief step 5. Text: `for (const flags of [['--tester-port', '7332'], ['--port', '7444', '--tester-port', '7444'], ['--tester-port', '7444', '--port', '7444'], ...['1023', '65536', '1.5', '-1', 'nope'].map(p => ['--tester-port', p])]) {`
- `scripts/case1-accept.sh:71`: New explicit migration refusal for saved old arguments. Text: `--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;`
- `scripts/case2-unit.sh:215`: New explicit migration refusal before rendering units. Text: `--tester-port) die "case2-unit.sh retires tester TCP; use --tester-socket" ;;`
- `scripts/instance.sh:114`: Legacy installed-unit port inventory/parser retained by brief step 5. Text: `# this instance's, whose ExecStart takes PORT as --port or --tester-port.`
- `scripts/instance.sh:132`: Legacy installed-unit port inventory/parser retained by brief step 5. Text: `--tester-port) [[ "$tok" =~ ^[0-9]{1,5}$ ]] && ports+=("$((10#$tok))") ;;`
- `scripts/stack-release.sh:47`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]`
- `scripts/stack-release.sh:63`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the`
- `scripts/stack-release.sh:126`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; TESTER_PORT="$2"; shift 2 ;;`
- `scripts/stack-release.sh:797`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `elif grep -q -- '--tester-port' "$cli"; then supported=1; fi`
- `scripts/stack-release.sh:800`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)); inherited="${BASE_CONSOLE[i]:-}" ;;`
- `scripts/stack-release.sh:814`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `[ -z "$inherited" ] || DROP_TESTER="--tester-port $inherited (replaced by private Unix socket)"`
- `scripts/stack-release.sh:830`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `check_refuse tester-port-invalid "--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other"`
- `scripts/stack-release.sh:832`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `[ "$supported" = 1 ] || check_refuse tester-port-target "$TO's Console does not support --tester-port"`
- `scripts/stack-release.sh:833`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `RUN_CONSOLE=("${without_tester[@]}" --tester-port "$TESTER_PORT")`
- `scripts/stack-release.sh:837`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `RUN_CONSOLE=("${without_tester[@]}") DROP_TESTER="--tester-port $inherited"`
- `scripts/stack-release.sh:878`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# --send and --tester-port follow the pointer, so a --no-send or a tester port set after that`
- `scripts/stack-release.sh:885`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)) ;;`
- `scripts/stack-release.sh:893`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port "${P_CONSOLE[i]:-}") ;;`
- `scripts/stack-release.sh:897`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `SSO_PLAN="sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them"`
- `scripts/stack-release.sh:903`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP.`
- `scripts/stack-release.sh:919`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `case "${kept[i]}" in --send | --tester-port | --tester-socket) break ;; esac`
- `scripts/stack-release.sh:1076`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `case "${without_proof[i]}" in --send | --tester-port | --tester-socket) break ;; esac`
- `scripts/stack-release.sh:1134`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# Row 190: the target Console's --port (7332 when absent) and --tester-port against every other`
- `scripts/stack-release.sh:1142`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) ports+=("${RUN_CONSOLE[i + 1]:-}") ;;`
- `scripts/stack-release.sh:2106`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do [ "${P_CONSOLE[i]}" != --tester-port ] || reserved+=("${P_CONSOLE[i + 1]:-}"); done`
- `scripts/webui-service.sh:43`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `# Console), --tester-socket (private Unix tester), or legacy --tester-port N`
- `scripts/webui-service.sh:74`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `# --tester-port that another installed Console unit in the unit directory`
- `scripts/webui-service.sh:96`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH]`
- `scripts/webui-service.sh:97`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `--tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener.`
- `scripts/webui-service.sh:137`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `--tester-port) [ $# -ge 2 ] || usage; TESTER_PORT="$2"; TESTER_SET=1; SERVE_SET=1; shift 2 ;;`
- `scripts/webui-service.sh:182`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `echo "webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT" >&2`
- `scripts/webui-service.sh:266`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `[ "$TESTER_SET" = 0 ] || flags="$flags --tester-port $TESTER_PORT"`
- `scripts/webui-service.sh:462`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' "$target"; then`
- `scripts/webui-service.sh:467`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `if grep -Eq '^ExecStart=.* --sso( --send)?( --tester-port [0-9]+)?$' "$target"; then`
- `scripts/webui-service.sh:471`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `tester_port="$(sed -n 's/^ExecStart=.* --tester-port \([0-9][0-9]*\)$/\1/p' "$target")"`
+38
View File
@@ -0,0 +1,38 @@
# Row253 granted gate
Do not launch until Sage's CLEAR names this scope. No tests have run yet.
Use this worktree, the frozen five blobs in CANDIDATE-SHAS.json and the plain
node_modules link. No canonical-checkout node or queue writes during the slot.
Set TMPDIR with `mktemp -d /mnt/storage/scratch/tmp/gXXX`, export it and use
`set -o pipefail`. Record the grant and the exact date before launch. Use logs
under this packet's `run1/`, with the outer log named `controller.log`.
Neither helper nor wrapper argv may match the busy detector's patterns.
Sequence, stopping to inspect every result:
1. `window.py --stage focused`: isolated prior installer and acceptance RED
examples, restoring bytes/status in finally, then both focused test files
whole GREEN. Expected RED is recorded separately from gate failures.
2. `driver.py`: 33 unique single-target changes, named base cases and killer
runs. Each base must pass in under 30 seconds. bash -n is the only invalid
classification. Runtime errors kill a change; unexpected timeouts stop for
Sage/Mos. Every run records time, code, raw tee log and byte/status restore.
3. `window.py --stage whole`: cli whole, webui whole and the ten shell files
whole. No omitted tests, patterns or partial scripts in this phase.
4. Sum counts and seconds from raw logs by script. Sweep process cwd/ancestry
for this worktree's node, tee, query and probe descendants. Report existing
processes separately. Return the slot immediately, then prepare review.
Every driver invocation needs `--grant <Sage-id> --out <record-directory>`.
Before each actual test, driver.run checks Sage's busy guard and launches only
on CLEAR. It invokes a bash pipefail/tee pipeline through launch.py, which
loads the structured command after the guard. No command text is embedded in
wrapper argv. All execution is in a new process group and timed out groups
are killed and waited for. Query-owned groups are independently bounded by
product code; the orphan fixture also cleans its own recorded PID.
Two unexpected failures end the window. These drivers stop at the first
unexpected base, focused or whole-phase failure for inspection, without
automatically rerunning it. Any timeout is a stop. Send a stop to Sage/Mos
and do not turn an ungranted retry into a new run.
+50
View File
@@ -0,0 +1,50 @@
{
"at": "2026-10-21T21:52:50.545130+00:00",
"behavioral_tests_run": false,
"checks": [
{
"command": [
"bash",
"-n",
"scripts/case1-accept.sh",
"scripts/case2-unit.sh"
],
"code": 0,
"stdout": "",
"stderr": ""
},
{
"command": [
"node",
"--check",
"packages/cli/tests/case1-accept.test.mjs"
],
"code": 0,
"stdout": "",
"stderr": ""
},
{
"command": [
"node",
"--check",
"packages/cli/tests/case2-unit.test.mjs"
],
"code": 0,
"stdout": "",
"stderr": ""
},
{
"command": [
"git",
"diff",
"--check"
],
"code": 0,
"stdout": "",
"stderr": ""
}
],
"retired_port_hits": 127,
"unique_changes": 33,
"slot": "Requested gander-0071; queued by sage-2191; no CLEAR"
}
@@ -0,0 +1,233 @@
[
{
"id": "M1",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "c25003c6fea2c7693d6b3e38e1986b8397886a8a47b5c6d7c0ddb535e676171b"
},
{
"id": "M2",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "977ef3f63de87cd260bc4fa44c2695b56880066d722681fa5d9e0f45786474f3"
},
{
"id": "M3",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "9dca577302011b11fc0459517420a4862dedb792e11eba6e06a975b9978c2db9"
},
{
"id": "M4",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "538ec4d9aaa942052b62628040445103128b8894ba5b2dafdf5d6c9a52b3b4d7"
},
{
"id": "M5",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "6632ebedee012f299ff5b5b36eaa2fd7c0bcae371b8f7b11c1bfd28bce0dde80"
},
{
"id": "M6",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "4f9cfb1b87ab33c39fe2ed77286296ad1047581afca8138722ddad6dddc938d8"
},
{
"id": "M7",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "00fa60f1d341ee67622d1f89be9c5062ebe45af3c49cc82ae004670f653097a3"
},
{
"id": "M8",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "92d8b45baf861d0a12327897d6fe09f87db867b1d875d39e9769fa46242826ea"
},
{
"id": "M9",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "3ba9e143b76ab0c37c575f8a446acf69e611e604074296a176bef1b5b84aa6ea"
},
{
"id": "M10",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "6eacec588684a0ccc0a1cc009f5ec0f27b1f38341f1fb72fec37c710de38b2a9"
},
{
"id": "M11",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "dc4c4d3d4761cc8976c7b813bdcc9dd9c56fc35cf03aadf3a19d4cfec6a32233"
},
{
"id": "M12",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "6ec5c79f0a0d19f5d642dffc79a7e39815f907071fae14df0cfd66b64ebaf221"
},
{
"id": "M13",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "2af786adc1269fde529b68b7dc121bead9c8b82e2615b4dbb18207aab60e72c8"
},
{
"id": "M14",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "f3411f4887e009e1b8cc38664be6eb5e1a34379472311284b726466171572758"
},
{
"id": "M15",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "148f30346fcbfd473ca0b4a717846a2d9743af9a046d5cc009271392d958d812"
},
{
"id": "M16",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "ec27d778b49a8e5ff0b356e6b27973db8eb97a078a571cf9720ab43f15781b01"
},
{
"id": "M17",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "4c7cd4c62d2137626158091c478d20e8ace5617c5e96b258478317e57bc808f3"
},
{
"id": "M18",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "4372a2e392054f80d491fc04b328303339a57cd4c72030abf3ccdae1e0dfb024"
},
{
"id": "M19",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "766524ec13b960bace472671aa62cfb3ea69f6bdb31d27c2427758e8d8ea53ae"
},
{
"id": "M20",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "4e653a809eaa504024d57c0af132222a374c0c26313942f6f0a68b3e8d099632"
},
{
"id": "M21",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "1067c1400870258f663e7e6bdb911e9623a9c666765e86cc7ec0bda4131852f3"
},
{
"id": "M22",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "f19cc06bf32c491bdfbea56888353fcc985e10298d8416892993e0757c429421"
},
{
"id": "M23",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "f1c7326e60d5ed26d55f43107939d775d6c81dfc45398a36274bac380f95e9b3"
},
{
"id": "M24",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "be8c6cb451c4d30b621c9eb38cf2c63629bd8ae5b6d30d5d91d365e4911ca964"
},
{
"id": "M25",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "53ee61ac53156707aa11bbad35b41a0bf21f0093a5fb6d03aff215940ed023ad"
},
{
"id": "M26",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "fc9cbfc3eaa0df6adb84b67dc9cf4fb9f773b89c16597703dbebc03559977b5f"
},
{
"id": "M27",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "e727c63e45bb5a3a767c5e60870658d2a8d1cc970bfa3d6c2b8f439cb27fcbda"
},
{
"id": "M28",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "238b6bc3b2feb64de614aa2e9471dcc58867a3e44ebfad7130917d43101fd6bd"
},
{
"id": "M29",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "f7c5e06f46aea4d5c3fde288eebd732a3265583faef2d38c2b293a7364e299b7"
},
{
"id": "M30",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "d6ceb8529fa508eb48534000666f02fd4bf949dbeeeff03490c51c8d1e4ecf1f"
},
{
"id": "M31",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "c028760cf45fbae6b127b203f32ce208eeb77ea9dd646fee5af9e6e58661a6ec"
},
{
"id": "M32",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "f047a7024b67c1f7a7af503c3cb525b07c83a0e413b845818d3ea9cd0e6f9ea9"
},
{
"id": "M33",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "a197452abed015e81d0620730b95292a89cff84f8f52ea679111b0b71b17ae1d"
}
]
@@ -0,0 +1,461 @@
# Deferred list
One line per item, written from evidence, never from inertia. Add a line
when a gap is found and not fixed in the current piece; strike it (move to
Done at the bottom with the commit) when a piece closes it. Nothing here
starts on its own; a line becomes a piece only when Jason calls it. Check it
at every gate. Started 2026-09-12 during the control board MVP.
## Open
- **Inherited Discord engine test intermittency during #1512 review.** Frozen
serialized run recorded a tool-turn engine assertion failure and then timed
out at 120s. A later full TAP run with a 15s test deadline passed 351/351.
Rocko isolated the busy/settled-event test race and missing failure cleanup;
Darkwing independently reproduced the event-split assertion. Rocko also reported
a timeout-log test race and a separate engine followUp-after-timeout defect.
No live incident established. Row21 owner orch-01 notified; no Discord source
takeover. Not assumed identical to earlier concurrent hangs.
Integration held pending owner disposition; logs and scope in
`agents/darkwing/work/relaunch-activity/r1-review.md`. (2026-09-15)
Outcome 2026-09-26: 1685deb4 fixed the busy/settled race and the engine no
longer sends a follow-up (held prompts). A re-run of the frozen candidate
against 21e3e908 passed the serial acceptance command 397/397 three times.
Two engine failures remain when tests run concurrently, and they reproduce on
clean HEAD without the #1512 files, so they are #1509's (entry below). Sage
ruled that #1512 integrates on the serial evidence; it landed in af4203ca.
Record: `agents/darkwing/work/relaunch-activity/rerun-2026-09-26.md`.
- **Registration has no "set by" field.** `mosaic seat task` records the
task and `updatedAt` but not who set it, so a human session and the
orchestrator look the same on the board. One-line record change; rides
with the orchestrator assignment piece. (2026-09-12, #1504)
- **Usage-limit notice was missed.** Jason reports Rocko exhausted usage without
Jason seeing the notice, and is now available again. Investigate where the
notice appeared and what operator notification is missing as part of Claude
console integration. User-reported, not independently reproduced; no root
cause established. Keep outside #1512's relaunch-display scope. (2026-09-15)
- **Claude Code seats register but get no board row.** Rocko has no Pi
session log, and rows come only from Pi logs. Needs a second row source
or a Claude-side log reader. (2026-09-12, darkwing's #1504 report)
2026-09-15 #1511 live-test preflight reconfirmed no Rocko row: expected
`.pi/state/rocko/sessions` discovery directory absent. Its existing repo
registration names dead PID 185602, while the verified native lock owner is
3707667. No registration repair, discovery expansion or relaunch performed;
test write refused before mutation. Evidence:
`agents/darkwing/work/task-attribution/backend-live-verification.json`.
- **Fleet seats (`~/.mosaic`) stay on the old launchers.** No registration,
no board reply, no `seat task`. Moving them is the stress test and comes
after the WebUI and fleet launch work. (Jason, 2026-09-12)
- **Comms skill has no rule for senders without a pane.** The board trailer
covers it today; if another pane-less sender appears, put the rule in
`skills/ms-communications` instead of a second trailer. (2026-09-12, #1505)
- **Board: multiple sessions per seat, stopping seats, auth beyond
loopback.** Out of scope for the MVP by the plan; listed so they are not
forgotten. (plan page, Boundaries)
- **`MOSAIC-STACK-D-001` is Answered, not Completed.** Mark `--completed` in
jarvis-brain once Jason says the board answers "who is waiting on me".
(2026-09-12)
- **Uncommitted working tree.** `AGENTS.md` autonomy section,
`agents/README.md` Sage row, Dewey's `work/wui` docs and evidence,
`docs/plans/reviews/2026-09-08..10_*`, `skills/ms-{arena,blast-radius,
board,executive-update,git-operations,technical-writing}`. Jason's edits
from before 2026-09-12; they ride along in the next commit that touches
them unless staged on purpose. (2026-09-12)
- **Fleet debt (orch-01, `~/.mosaic`):** `sweep-commit.sh` deny-list regex
skips `launch.env`; needs the fix plus a regression test. (2026-09-12)
- **Parked, not cancelled:** Dewey D05 dashboard mockup work beyond design 3,
registry increment 3 (headless identity-env leak), new seat directories.
(plan page, 2026-09-12 parking note)
- **Gitea helper response-file hardening.** Predictable `/tmp/gitea-api-response.$$`
has no explicit 0600 mode; use `mktemp` with cleanup in a separately reviewed
pass. Credential validation and later reread also have a race window. Other
non-blocking ledger review notes remain in
`reviews/2026-09-12_ledger-verdict.md`. No change beyond the reviewed GET
cleanup fix in this piece. Filbert, LEDGER-1506-R1, 2026-09-12, #1506.
- **One shared checkout for every seat and the coordinator.** Any `git add`
of a shared file (CURRENT.md, SESSIONS.md, the plan page) commits another
seat's in-progress edit under the wrong commit; it happened at f80404f5
(dewey's CURRENT.md status note went in with the piece 5 brief, harmless
but unowned). Fix candidates: per-seat worktrees, or shared files edited
only by the seat named in CURRENT.md. (2026-09-12)
- **Console mockup fields absent from the board API.** Controller, generation,
config fingerprint, turn count, goal/report and activity stream are omitted.
Workspace paths are shown as board data, not invented registered workspace
nodes. Launch, observe, stop, full transcript navigation, command palette and
other screens are absent. Existing palette/appearance switches remain on the
single page. Revisit only with a new brief. (2026-09-13 UTC, #1507)
- **Board send can turn into a Pi slash command.** `tools/tmux`
`send-message.sh` pastes onto whatever is already in Pi's composer without
clearing it. On Jason's 2026-09-26 20:10:57Z send, a leftover "/" landed in
front of the agent-send header. Pi read it as plain text that time, but the
same path could run a board reply as a slash command. Belongs to CHAT-03I/B3.
No tools/tmux change until that piece. Found by Dewey in the CHAT-02 brief
evidence. (2026-09-26, #1507)
- **Discord wedge restarts have no cumulative stop.** Since 6b R2, a wedged
pi ends the connector with exit 1, and the unit restarts it. With
turnTimeoutSeconds 600, one cycle takes at least 645 s, longer than the
600 s StartLimit window. So a pi that wedges on every prompt restarts once
per prompt and never hits the limit. It only happens when someone sends a
message, and that person gets an error reply. Sage accepted it for now. The
fix is a separate policy (a wedge count kept across restarts). (2026-09-26,
#1509, Rocko 6b R2 review)
- **Connector does not journal the source it started from.** The unit runs
from the live checkout, so an automatic restart loads whatever Discord
source is in the tree, even uncommitted source. Rocko recommends a startup
journal line with HEAD, the dirty state of `packages/discord` and a digest
of the runtime files. Until then the rule is that unreviewed Discord source
stays uncommitted in the tree only as long as a review needs it.
(2026-09-26, #1509)
- **Index-export suite runs leave Docker networks behind.** Each export
directory becomes a compose project, and its `<dir>_default` network
stays after the run. On 2026-09-26 the host ran out of address pools. The
task and release suites then failed 31 and 3 tests with "all predefined
address pools have been fully subnetted". Sage removed its own five stale
networks. The dw-* ones are still there. Fix: the export recipe, or the
suites, removes the project network on exit. (2026-09-26)
2026-09-27: it happened again on the Piece D candidate (task 31 and
release 3 failures). Sage removed 12 leftover export networks from
other seats, each checked for zero attached containers first, and both
suites then passed. Still open.
- **Ledger T3 read: snapshot isolation has no test.** `readT3` runs every
query in one read transaction, but no test proves a write landing during
the read stays invisible. Filbert's note 4 on the Gate F build (e47ec6da).
Notes 1 to 3 go to Darkwing's follow-up. (2026-09-26, #1506)
- **A T3 sender role with spaces counts as human.** The SetSpark lead's
reply to Sage (21:41:24Z) used the role "main content-engine agent". The
6a rule requires a one-word role, so the ledger counts that message as
human in Sage's row. Gate F reads only Filbert's row, so it is not
affected. Filbert found it through the live diagnostic (14 went to 15).
Sage asked the SetSpark lead to use a one-word role. The rule stays as
it is. (2026-09-26, #1506)
- **Queue tests: the cold-start failure came back (reopened).** Row 33
closed this as not reproduced in 20 idle cold runs. On 2026-10-04 Sage's
index export of the row 33 candidate ran the nine suites, then
`node --test packages/queue/tests/`, and got 147 passed, 1 failed. Sage's
recipe threw the output away, so again nobody knows which test failed.
Ten more runs all passed 148/148: three reruns in the same export, four
under parallel suite load, and three fresh exports run straight after
`test-queue.sh`. It needs a first run on a fresh tree after a full
suite pass, and even then it's rare. Darkwing's n2 on row 33 (the cold
runs were idle) was right. Next time: Sage's export recipe keeps the
node test output in a file, so the first failure has a name. Then fix it
as its own row. (2026-10-04, #1508)
- **Queue: `add --issue N` also sets closes to N.** Follow-up rows under
an umbrella issue, like rows 31 and 33 under #1508, would close the issue
when they finish. Sage narrowed both by hand (revs 36 and 43). Either
`add` takes `--closes` or the lead checks closes on every add. Not
urgent while Sage adds the rows. (2026-10-04, #1508)
- **Ledger metric call can orphan curl.** The metric call in
`packages/ledger/src/ledger.mjs` still relies on execFileSync's
timeout, which kills the helper but can leave curl running. E's queue
calls already use `timeout -s KILL`; the metric call should match.
(2026-09-27, #1508)
- **Ledger: a missing `timeout` command reads as a Gitea failure.** With
no `timeout` on PATH the queue issue calls report "credential or Gitea
request failure". Optional test and clearer message. Filbert, E r2.
(2026-09-27, #1508)
- **`review record` doesn't check which issue the verdict comment is on.**
Row 5 round 1's request (26671) and Filbert's verdict (26683) are on
#1507. Darkwing's verdict 26681 went to #1508, and `review record`
accepted it, because only `review resolve` reads the comment back
(`checkComment` in `packages/queue/src/review.mjs`). The receipt
points at a real comment on the wrong issue. Sage posted a pointer on
#1507 (comment 26685). Fix: have `record` check the comment's issue and author the way
`resolve` does, or refuse a comment id it can't verify. Found by
Filbert. (2026-10-04, #1508)
- **Host seats launch Pi with `--approve`.** A future `.pi/SYSTEM.md` or
`.pi/settings.json` in the checkout would load in every host seat. This
is latent, since neither file exists today. Fix in
`scripts/agent-host-dev.sh`: pass `--no-approve` and an explicit system
prompt. Filbert, meta-harness survey section 8.4. (2026-10-04)
- **Workers can read provider keys.** The worker container passes
`ZAI_API_KEY` and `ANTHROPIC_API_KEY` in the environment, which Pi's
`bash` inherits, and Pi's `read` can open the mounted `auth.json`.
Slice 1 role tokens stay in the broker (lead decision 47). The workers'
own exposure needs a separate piece. Filbert, survey section 8.6.
(2026-10-04)
- **CHAT-03 I1 round 2 follow-ups, for the I3 brief.** Neither reviewer
held any of these as blocking (Darkwing comment 26690, Filbert comment
26694 on #1507). Four of the five are closed by slice 1 S5 (see Done);
this one stays open.
- No test proves that the processes are listed only after the cgroup
is frozen. The code waits for `frozen 1` in `cgroup.events`, and
both Filbert and Dewey confirmed it by reading it. Mutants r2-B5b and
C4 survive. Proposed: a test-only hold after `members`, or a FUSE or
privileged fixture. Filbert F1.
(2026-10-04, #1507)
- **CHAT-03 `engine.envKeys` accepts founder credential names.**
`CREDENTIAL_NAME` in `packages/conversation/src/pi-pin.mjs` matches any
`*_API_KEY` or `*_TOKEN`, including `GITEA_TOKEN` and `GH_TOKEN`, which
the S6 runner refuses as founder credentials (REQ-CRED-2). Nothing
builds a Controller outside the tests yet. Whoever wires the entry point
narrows the pattern to provider names or checks the founder list.
Filbert N2 on #1522. (2026-10-10)
- **`test-task.sh` makes a live Pi call without the Docker guard.** The
"user recall" block near line 478 runs `run-task.sh` with no
availability check, so a run with Docker unavailable fails "user recall
run succeeds" and "recalled user name" (26 pass, 2 fail). The other
live groups skip. Rocko hit it during the S2 suites. Wrap the block in
the same guard as the other live groups, so a seat without provider
access gets clean skips. (2026-10-04, #1519)
- **`node --test <dir>/` fails on Node 24.** Node 24 reads a directory
argument as a module, so every package's test command in that form
fails there, while Node 26 accepts it. The glob form
(`node --test 'packages/x/tests/*.test.mjs'`) works on both. Package
READMEs, `docs/TOOLS.md` and Sage's suite recipe should use the glob
form wherever Node 24 is a target. Darkwing, S1 packet. (2026-10-04,
#1518)
- **`stack-upgrade.sh` doesn't check `$REPO`.** Nothing confirms the
source checkout is root-owned or that its tree is clean before the
upgrade builds from it. Filbert N2 on row 233 (29922, #1735).
(2026-10-21)
- **`stack-upgrade.sh` has no signal handling between A2 and A5.** A HUP,
INT or TERM in that stretch leaves the run failed closed, but recovery
is by hand. A trap that writes the stop point and the recovery step to
the log would make it a sheet step. Filbert N3 on row 233 (29922,
#1735). (2026-10-21)
- **`checkCreative` doesn't refuse `credentials`.** Row 213's
`checkCreative` (packages/business/src/role.mjs:143 at 2d2ad2760)
limits a creative role's tools, authority and messaging, but a role
file that gave companion or research a credential would still load.
The business file could then map a credential to that instance
(business.mjs:157). Nobody has traced whether a mapped credential
reaches the run's environment. The decision paths stay closed (M6),
but a creative role with `web` holding a live token is a wider channel
than design 4 means. The fix: refuse any `credentials` in
`checkCreative`, with a role.test.mjs case. It goes after row 213
lands, as a follow-up row or in the next row that edits role.mjs.
Webby N1 on row 213 r3 (REVIEW-r3.md, 3ab3c010b), mos-1160.
(2026-10-21)
- **The boot host check orders only one business's bus.**
`mosaic-hostcheck.service` (row 240) has
`[email protected]`. `case2-unit.sh` enables the
bus as `mosaic-bus@<business>.service`, so on a host whose business id
isn't `mosaic-stack` nothing orders the two. The bus can start before
the boot stop runs, and after a boot-time drift role sessions can use
bwrap until it does. The fix: `After=mosaic-hostcheck.service` in
`packages/cli/systemd/mosaic-bus.service.in`, which orders every
instance and does nothing where the unit isn't installed. It goes in
before the first install whose business id isn't `mosaic-stack`.
Filbert N1 on row 240 (29945, #1746). (2026-10-21)
- **The run sheet writes the apt hook before the first `--check`.**
Section 4 installs `80mosaic-hostcheck`, then runs
`hostcheck.sh --check`. If that check fails, the next package run
closes bwrap before anyone has read the failure. It fails closed, but
it surprises an installer. The fix: run `--check` first, or say what a
failure at that step means. Doc-only, for the next row that edits
`docs/guides/host-run-sheet.md`. Filbert N2 on row 240 (29945, #1746).
Filbert's N3, that a closed bwrap appends to `DRIFT` on every later
apt run and boot, is by design; ops-01's STATE read should expect the
repeats. (2026-10-21)
- **The sso-config first-step hint has no business.** When the target
copy has no `console.sso`, `stack-release.sh` (`:938`) refuses with
"activate $TO without --sso first, then ... activate $TO --sso". The
usage line is `activate ID BUSINESS` (`:47`), so pasting either command
as printed gives a usage error, not the first step. It fails safe,
since the usage error changes nothing. The fix: add `$BUSINESS`
(set at `:2138`) to both commands, and the business (`acme`)
to the three expected strings in `stack-release.test.mjs`
(`:1538`, `:1689`, `:1691`). For a later row that edits
`stack-release.sh`. Webby N1 on row 238 r1 (REVIEW-r1.md, 433a95292,
#1744). (2026-10-21)
- **Lane image prep inspects a base that BuildKit never tagged.**
(N7 of the row 197 proof.) After the Compose build,
`scripts/testing/lane-image.mjs:41` (`prepareImage`) runs
`docker image inspect node:24-bookworm-slim` to put the base digest
in the receipt. BuildKit resolves the base from the registry
without tagging it in the local store, so on a host without the tag
the inspect throws "No such image" after a good build. That
happened in Sage's proof try 1 (2026-10-21 09:03Z, run cf48efccf083).
The throw fails closed: the run's terminal record is unclean, both
lanes quarantine, and `gate-lane.sh recover` clears them. Pulling the
base first fixes it, and the receipt digest then matches what the
build used (91acba91 both ways). The fix: before the build, inspect
the base and refuse with a named reason if it's missing, or pull it.
After the build, refuse if the base digest changed. Add a test with a
fake Docker that has no base tag. Kept out of row 197 r6 by Sage
(sage-1883). Mos asked for this record in mos-1162. (2026-10-21)
- **Case 1 acceptance script: root parses with the node it's checking, and the header says less than root does.**
(Duckworth N1-N3 on the row 195 manifest, REVIEW-m1 6afcee2a5.)
N1: F9 runs `$ROOT$NODE` as root (`scripts/case1-accept.sh:298`)
before F10 checks that `--node` is the `/opt/mosaic-stack/node-<pin>`
path (:303-304). On a host installed as designed, `case2-unit.sh:128-133`
refused a non-root-owned `--node` and the args file is root's, so no
escalation path exists today. Run F9's parse through `AS_USER`, or
apply F10's path check first. N2: the header (:10) and the TOOLS.md
entry say root only reads, but root also runs `curl` (F2), `sudo -l`
(P1, P3), the jarvis probe (P9), `hostcheck.sh --check` and
`systemctl`. Doc-only. N3: F6's prefix match (:261) accepts a
`--claude` with `..` after `claude-<pin>/`. That claude runs only as
`mosaic`, and F10 uses equality. Darkwing has a follow-up on
`darkwing/row195-n1` 1e3c4767c, not yet run; it gets its own row now
that 195 has landed (b65014647), Duckworth reviews. (2026-10-21)
- **Morgana's row 213 mutation set: the B3 pattern no longer matches `bundle.mjs`.**
(Webby N3 on row 221 round 1, REVIEW-r1 a0a00630e.) Row 221's
candidate changes `packages/harness/src/bundle.mjs`, and the B3
anchor at Morgana's 213 `mutants.py:48` stops matching on that
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)
- **Row 66 live checks: the unslop notice on a live managed session, both harnesses.**
(mos-1186, Vikunja #454.) Row 66's gate asked for one landing session per
harness with a prompt that draws a known tell. Mos changed that gate. The
Claude Code session ran before landing on a scratch root from the
candidate tree (run r4ab24630d323, landed 2c38325ec). Pi ran launch only there (run r1dd94171bede):
this host has no credential pi reads until P19 #383 settles. Two live
checks remain. Claude Code: in the first re-pin whose tree includes row 66,
backup first (Q173). Pi: one full session with a tell after #383. Each
attaches the operator note on the RESULT, the next-turn correction and the
`unslop.jsonl` counts. (2026-10-21)
- **Console tester listener: `testerNoSignIn` is a dead parameter.**
(Duckworth N2 on the row 196 manifest, REVIEW-m1; carried as C1 in
REVIEW-m2 cdbf7b64e, #1694 comment 30156.) Row 196 landed 7247d80c7
with the parameter and its comment still at `serve.mjs:472-475` and
passed from `webui/src/cli.mjs:109` and `sso.test.mjs:392`. Nothing
in `serve.mjs` reads it after the destructure. Remove it, or wire it
with a test. This binds the next row that touches
`packages/webui/src/serve.mjs`. (2026-10-21)
- **Console library routes: the tester listener and `library.sync`.**
(Duckworth N5 on row 196 round 2, REVIEW-m2 cdbf7b64e.) At 7247d80c7
`startServer` (`serve.mjs:475`) takes none of `libraryFile`,
`libraryStatus` or `librarySync`, so the callbacks `cli.mjs` passes
do nothing yet. The row that adds the Console library routes decides
whether the tester listener gets `library.sync`. Duckworth's view is
that it shouldn't, because sync is a human write. (2026-10-21)
- **Console library routes: four library codes still map to 502.**
(Dewey N2 on the row 254 manifest, #1759 comment 30173.) Row 254
gave six library codes a STATUS entry in `packages/webui/src/bus.mjs`.
`library-mapping-invalid`, `library-remote-invalid`,
`library-state-invalid` and `library-git` still fall through
`busStatus` to 502. The row that adds the Console library routes
decides their statuses and gives its `humanCall` for `library.sync`
a timeout over 130 seconds. (2026-10-21)
- **T27 sweep test leaves two `sleep 60` children running.**
(Darkwing row 251 slot, darkwing-0113 and darkwing-0117; Dewey on
r2, comment 30168.) The sweep test spawns two detached shells,
`planted` at `host-run.test.mjs:528` and `decoy` at `:536`, each
running `sleep 60; :` with the run dir as `$0`. `sweepRunDir` kills
the planted shell by pid (`host-run.mjs:238`), and `t.after` kills
only the decoy shell. Neither kill reaches the `sleep` child, so each
one lives for up to 60 s after the test and trips any busy check that
follows. The fix is to kill the process group in both `t.after`
hooks: `process.kill(-pid, 'SIGKILL')`. Each shell is spawned
detached, so it leads its own group. `exec sleep 60` doesn't work.
The sweep matches the run dir in `/proc/<pid>/cmdline`
(`host-run.mjs:236`), and exec drops the `$0` that carries it, so the
planted process would survive and the "planted process killed" assert
would fail. This binds the next row that touches
`packages/bus/tests/host-run.test.mjs`. (2026-10-21)
- **Real-model suite cases run outside the gate.** Row 244 moved
`test-task.sh` fork recall (M11) and user recall (M16) behind
`MOSAIC_LIVE_PROVIDER=1`, and `test-release.sh` activations to the mock
adapter. Before any re-pin whose tree changes `packages/harness` or the
session fork code (`scripts/mosaic-task.mjs` session and
`sessionForkFrom` handling, the pi adapter), an operator runs
`MOSAIC_LIVE_PROVIDER=1 bash scripts/test-task.sh` with the real pi
credential and a real `release.sh activate`, and records both results on
the re-pin row. Morgana's wording (row 244 BUILD.md), Webby N2 on row 244
r1 (#1757). (2026-10-21)
## Queue
Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
## Done
- Ledger (piece 3, #1506): shipped cd0aa5fb, Gate D written 2026-09-12 (18.9 human messages per closed issue).
- Relaunched seat shows the old last message (darkwing, Step 3 log, 2026-09-12): closed by #1512 relaunch activity, af4203ca. Filbert authored it; Darkwing and Dewey reviewed. The live board restart and Gate F remain separate.
- Sage launcher hook uncommitted (2026-09-12): closed by the agents/sage launch files commit, Dewey review R1 revise, R2 approve (`agents/dewey/work/sage-launch-review-2026-09-26.md`), launcher test now covers sage.
- Ledger counts T3 agent messages as human (2026-09-26, #1506): fixed in ef0020ad (Darkwing 6a, Filbert reviewed). `messageKind` now counts a T3 header as agent and the control-board sender as board. It changes no current count, because no source the ledger reads has a T3 header yet. That waits for the T3 thread source (Gate F).
- Control board checked the bind address but not Host or Origin (2026-09-26, #1507, found by Dewey): closed by the board guard commit. `foreignRequest` refuses a non-loopback Host, a wrong port, userinfo or a path in Host, and any foreign Origin on every route. Dewey authored it, and Rocko approved it (`agents/rocko/work/board-guard-review-2026-09-26.md`, 5a12f08e). Rocko's one low finding: URL normalization accepts a root slash, empty userinfo and an empty query in Host, so the notes' wording overstates what the raw-syntax check refuses. No bypass was found.
- Discord engine: leaked fake pi and the timeout gap in `busy` (2026-09-26, #1509): fixed in 3edb15eb (Darkwing 6b R2, Rocko approved, review ed5510a0). Tests stop the engine in `finally`. A turn pi never started now stops pi with exit 1 instead of guessing, and the unit restarts it. Known limit: see the open entry on wedge restarts.
- Combined concurrent test hangs (2026-09-14, #1509): closed on evidence from 3edb15eb. At default concurrency the union passed 406/406 three times on the R2 snapshot and 408/408 on the commit's index, where clean HEAD had hung four runs out of four. The inherited-intermittency entry from 2026-09-15 stays open until a later run shows it is the same cause.
- A nested `node --test` passes even when its tests fail (2026-09-26, #1508): closed. Queue A1 (34a72af9) clears NODE_TEST_CONTEXT on its own path. N13 fixes `test-foundation.sh` and `test-discord.sh`, each with a check that fails the suite if the clearing is lost. Darkwing wrote it (n13.patch 00868b2f) and Filbert approved it (e464be6c). Sage's grep on 2026-09-27 finds no other nested `node --test`: in scripts, only the two suites, `test-queue.sh` and `queue-commit.sh`, and in JS, only the queue test that covers the fix.
- SetSpark service accepted any string as a decision approver (2026-09-26, #1509): closed. Fixed in shared-signals cc74d92 (Rocko approved R2, lead decision 24). The operations seat (T3 12fe8cda) deployed it to VM 1022 at 00:23Z on 2026-09-27. The running `service.py` hash matches cc74d92, and `/healthz` returns 200 inside the network and through the public tunnel. The read-only README survey found one row breaking the rule (DEC-009, Proposed, version 1), one open request that isn't ready (DEC-009's), and no Accepted decision with more than 16 approvers. No rows changed. DEC-009 stays as stored (lead decisions 21 and 28). The service now refuses approvals on it, so nothing can go wrong silently. Side effect: `up -d` also recreated caddy, about 20 s of edge downtime for all hosts. That is for the shared-signals deploy procedure to fix, not Mosaic.
- Queue suite failed its live checks outside the canonical checkout
(2026-09-27, #1508): closed by Piece D, f539466f. `test-queue.sh` now
skips `verify` and `render --check` with a printed reason when the
toplevel isn't the recorded `canonicalRoot`. An index export passed
27/0, and the canonical checkout 29/0.
- Queue: an owner listed as a reviewer blocked the row (2026-09-27,
#1508): closed by row 31, bb2fa970. `add`, `set reviewers` and `assign`
now refuse the owner as a reviewer with exit 2 and write nothing. Rocko
built it in the Gate G run, and Filbert approved round 1 (#1508 comment
26629, 8 of 8 mutants caught).
- Queue follow-ups from rows 12, 13 and 31 (2026-10-04, #1508): closed by
row 33, 6fb50cc0. Filbert built it, and Darkwing approved round 1
(#1508 comment 26651).
- `genesis` refuses a map row whose owner is one of its reviewers (exit
2, nothing written). The check is on the map, not in replay, so a log
from before the rule still loads.
- The `assign` refusal now reads "can't assign row N to SEAT: SEAT is
one of its reviewers".
- `queue-commit.sh` compares HEAD with H before each canary and again if
the clean run fails. A moved HEAD exits 2 with "HEAD moved since H was
recorded"; the guard message is kept for a real guard failure.
- Every time and date the validator accepts must format back to itself,
so month 13, day 32, 2026-02-30, 2027-02-29 and 24:00 are refused.
The live log at rev 45 still replays.
- The cold-start failure (2026-09-27): not reproduced in 20 cold runs.
Each ran `node --test packages/queue/tests/` in a fresh tree, 10
shared clones and 10 exports, one after another. All 20 passed
148/148 (2960 of 2960). The 300 ms deadline test is unchanged. Counts:
`agents/filbert/work/queue-33/cold-runs.txt`.
- CHAT-03 I1 round 2 follow-ups (2026-10-04, #1507): four of five closed
by slice 1 S5 (row 40, #1522; Dewey authored, Darkwing and Filbert
reviewing). The commit is named in the row's review record when it
lands.
- Engine command and `preArgs`: `engine` takes only `extraArgs`, `cwd`
and `envKeys`. The controller always launches the pinned Pi and
checks that at construction and at bind. The fake engine comes in
through a symbol key that JSON config can't carry (N24b).
- Engine environment: an explicit list (`ENGINE_ENV` in `pi-pin.mjs`)
plus `*_API_KEY` and `*_TOKEN` names from `engine.envKeys`. Nothing
else is inherited. `HOME` passes, so Pi reads `~/.pi/agent` unless
`PI_CODING_AGENT_DIR` is set (Darkwing F3).
- `escalating` is cleared when the force-stop fence throws (Darkwing F2,
races.test.mjs).
- Takeover then Enter in one chunk: input after Ctrl-T or Ctrl-O waits
for that action, in the same chunk or a later one, and also while an
earlier action is still pending, so it is judged as if typed one key
at a time (Filbert F2; Darkwing and Filbert T1 on #1522;
flows.test.mjs).
The frozen-cgroup test (Filbert F1) stays open above.
- Gate runs mount the host's pi auth file (Webby N4 on the row 197 proof
driver, 059577886): closed by row 244 (#1757), landing e69260a3c. The
suites and the lane's `cleanEnv` export a 0600 fixture `PI_AUTH_FILE`,
`guardDocker` refuses a Compose run without one, and
`packages/cli/tests/gate-lane-auth.test.mjs` proves no gate step mounts
the host default. The real-model cases this moved off the gate are open
above.
## Row253 F5: legacy Case AS_USER supervision
Tracked as row 276, Gitea #1781 (sage-2331), owner Gander, reviewer Duckworth, after row253.
Brief: docs/plans/2026-10-22_case-sudo-legacy-followup.md, “Brief: supervised legacy AS_USER calls”.
case1-accept.sh's legacy timeout330 around AS_USER talk and its AS_USER precheck, Claude version, PM launches and Node version diagnostics predate the new socket metadata/probe helpers. sudo's pty monitor can move them to a separate session; the old outer timeout does not prove their descendants are killed. The C4 repair in row253 covers only its new config query and peer probe. These legacy paths need the reviewed supervisor or a header explicitly explaining why each remains unsupervised. The follow-up must preserve the 300-second talk/reply contract, concurrent F5 scan, diagnostics and secret-output refusals, with fake-only session-hop tests and Sage's slot. No host or live unit change is authorized.
@@ -0,0 +1,10 @@
{
"agents/gander/work/queue-253/c4/case1-accept.sh.txt": {
"source": "scripts/case1-accept.sh",
"sha256": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f"
},
"agents/gander/work/queue-253/c4/case2-unit.sh.txt": {
"source": "scripts/case2-unit.sh",
"sha256": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
# C4 repair for the short static pass
Prepared under duckworth-0111 and sage-2325/2331. No tests, syntax execution, probe, real sudo or host experiment ran on this repair. No passing claim applies to these new product bytes. DESIGN.md and the approved DESIGN-C4.md remain unchanged. The original frozen snapshots remain unchanged.
REPAIR.diff compares the repaired scripts against their original frozen source, and tests/docs against 968db0bcd. The *.repair.txt files are non-executable exact snapshots of all seven pending source/test/doc paths. REPAIR-SHAS.json pins them. The live source/test paths remain uncommitted until the gate permits the product commit.
## Conditions mapped to the diff
C1: fixed setsid without -f/-w; confirm the direct child's kernel SID equals its PID. M38 drops setsid; exact inner-timeout tests must kill it by observing inner-session instead.
C2: exact inner/outer/output-limit refusals in both fake modes, query 5/7 s and probe 2/4 s. Outer cases require at least the outer duration and less than outer + 2 s. Root discards the valid-looking stdout of a stalled supervisor. A trusted notice plus status 137 identifies the outer timer.
C3: read complete stat files, strip through the last closing parenthesis followed by space, skip vanished entries, refuse on other errors. The setpgid member has newline, closing parenthesis and space in comm. M36 kills only the leader group and must fail that case. The existing 250 ms cleanup budget and 50 x 10 ms post-return test polling remain.
C4: DEFERRED.md names row 276 / #1781 and the brief from sage-2331. It is an intended manifest path. UPSTREAM-PATHS.json records the requested comparison. Current upstream DEFERRED.md was preserved before our entry.
C5: the test compares both inline payloads byte for byte; M39 adds a comment to only one. Both payloads were prepared from supervisor-source.txt.
N1: the fake reads its mode with the Bash file-read builtin form. N2: fixture cleanup rechecks SID before each kill, including members discovered by an owned-session scan. N3: exec mode cannot safely scan the runner's session, so it checks exact PIDs and says so; hop mode checks all members of self-led recorded sessions. N4: the fake still makes no synthetic HUP on its own death. N5: polling remains bounded at 500 ms. N6: the packet must be published before the manifest round.
## Line references in matching repair snapshots
- scripts/case2-unit.sh:162: `stat_fields()`
- scripts/case2-unit.sh:180: `clean_session()`
- scripts/case2-unit.sh:220: `"$setsid_bin" "$@"`
- scripts/case2-unit.sh:221: `for ((attempt=`
- scripts/case2-unit.sh:240: `tester_tools()`
- scripts/case2-unit.sh:270: `tester_capture()`
- packages/cli/tests/case2-unit.test.mjs:192: `for(const kind`
- packages/cli/tests/case2-unit.test.mjs:219: `C4 stalled supervisor`
- packages/cli/tests/case2-unit.test.mjs:233: `C4 inline supervisor`
- packages/cli/tests/case-sudo-fixture.mjs:21: `case_sudo_run()`
- packages/cli/tests/case-sudo-fixture.mjs:84: `function sessionPairs`
- packages/cli/tests/case-sudo-fixture.mjs:75: `function killPairs`
- packages/cli/tests/case1-accept.test.mjs:309: `C4 acceptance query inner`
- packages/cli/tests/case1-accept.test.mjs:322: `C4 acceptance query outer`
- packages/cli/tests/case1-accept.test.mjs:345: `for(const fault`
- docs/plans/DEFERRED.md:456: `## Row253 F5`
## Proposed next gate
On Sage CLEAR after static approval: both touched files whole, M1-M39 with passing focused bases and byte-identical restoration, CLI whole, WebUI whole and the ten shell files. Previous completed results are historical only because product blobs changed. Use a gXXXXXX TMPDIR under /mnt/storage/scratch/tmp, pipefail and tee, a busy check before every launch, and log/wrapper names that do not match the busy detector. Stop and report according to the new grant.
REPAIR.diff uses zero context so blank context lines in the stored patch do not trigger whitespace checks on the record itself. The full matching snapshots provide surrounding context.
@@ -0,0 +1,30 @@
{
"scripts/case2-unit.sh": {
"frozen_before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"repair": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe"
},
"scripts/case1-accept.sh": {
"frozen_before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"repair": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7"
},
"packages/cli/tests/case-sudo-fixture.mjs": {
"frozen_before": "5c2cf5408944f8c1715355f75e2510a8deee814b90e2b44befb5a7dff411d6b7",
"repair": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008"
},
"packages/cli/tests/case2-unit.test.mjs": {
"frozen_before": "83cd18f05e60d489e4b0e581d9d977fbf86f897ea3d0bddaa13a63e86f22321f",
"repair": "72a07e9ad9d5c8bb01851eef18f792f2fe9b95d56803e313fad6aa1f1d6c3e8c"
},
"packages/cli/tests/case1-accept.test.mjs": {
"frozen_before": "3ed99803a7e3f81d8dce62e0577cd6c86b70a0339e688c0124bcb10d149e8de9",
"repair": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901"
},
"packages/webui/README.md": {
"frozen_before": "04b7c1165c4fa73137a72e8373d70fd7d96a5897b5511170e6d41908a7667e96",
"repair": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9"
},
"docs/plans/DEFERRED.md": {
"frozen_before": "cadfb704f03e1bfcd36e56ca7a1a3636955004e7d96be9b8765f35ea750da053",
"repair": "67bbf7bf57aa70dcade36ca4642f01cccb71ce01c130a88b84e1edb7ea674377"
}
}
+684
View File
@@ -0,0 +1,684 @@
--- a/scripts/case2-unit.sh
+++ b/scripts/case2-unit.sh
@@ -19 +19,4 @@
-# dependencies. It renders the default instance only.
+# dependencies. The inner query supervisor runs after the uid drop: 5 s inside,
+# 7 s on root's wait. Fixed system Bash, setsid, sleep, timeout and head are
+# resolved and root-validated. Only the config query uses this supervisor.
+# It renders the default instance only.
@@ -153,0 +157,113 @@
+# row253-supervisor-begin
+IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
+set +m
+seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
+child="" timer="" verified=0
+stat_fields() {
+ local pid="$1" raw rest
+ if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
+ [ -e "/proc/$pid/stat" ] && return 1
+ return 3
+ fi
+ [ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
+ rest="${raw##*) }"
+ [ "$rest" != "$raw" ] || return 1
+ read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
+ [[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
+}
+clock_cs() {
+ local boot spare
+ read -r boot spare < /proc/uptime || return 1
+ [[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
+ CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
+}
+clean_session() {
+ local entry pid rc live until
+ clock_cs || return 1
+ until=$((CLOCK_CS + 25))
+ while :; do
+ live=0
+ for entry in /proc/[0-9]*/stat; do
+ pid="${entry%/stat}"; pid="${pid##*/}"
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" != 3 ] || continue
+ [ "$rc" = 0 ] || return 1
+ [ "$ST_SESSION" = "$child" ] || continue
+ case "$ST_STATE" in Z|X) continue ;; esac
+ live=1
+ if ! kill -KILL "$pid" 2>/dev/null; then
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
+ fi
+ done
+ [ "$live" != 0 ] || return 0
+ clock_cs || return 1
+ [ "$CLOCK_CS" -lt "$until" ] || return 1
+ "$sleep_bin" 0.01
+ done
+}
+finish() {
+ local rc="$?"
+ trap - EXIT HUP INT TERM
+ if [ "$verified" = 1 ]; then
+ clean_session || rc=86
+ elif [ -n "$child" ]; then
+ kill -KILL "$child" 2>/dev/null || :
+ fi
+ [ -z "$child" ] || wait "$child" 2>/dev/null || :
+ if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
+ exit "$rc"
+}
+trap finish EXIT
+trap 'exit 88' HUP INT TERM
+"$sleep_bin" "$seconds" & timer=$!
+"$setsid_bin" "$@" & child=$!
+for ((attempt=0; attempt<100; attempt++)); do
+ rc=0; stat_fields "$child" || rc=$?
+ [ "$rc" = 0 ] || exit 85
+ if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.002
+done
+[ "$verified" = 1 ] || exit 85
+while kill -0 "$child" 2>/dev/null; do
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.01
+done
+kill -0 "$timer" 2>/dev/null || exit 81
+rc=0; wait "$child" || rc=$?
+[ "$rc" = 0 ] || exit 87
+exit 0
+MOSAIC_TESTER_SUPERVISOR
+# row253-supervisor-end
+
+tester_tools() {
+ local input target p st magic
+ TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
+ for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
+ target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
+ case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
+ [ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
+ p="$target"
+ while :; do
+ st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
+ [ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
+ [ "$p" != / ] || break
+ p="${p%/*}"; [ -n "$p" ] || p=/
+ done
+ magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
+ [ "$magic" = 7f454c46 ] || return 1
+ case "$input" in
+ /bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
+ /usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
+ /usr/bin/head) TS_HEAD="$target" ;;
+ esac
+ done
+}
+tester_capture_reason() {
+ case "$1" in
+ 81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
+ 84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
+ *) echo inner-command ;;
+ esac
+}
@@ -155,5 +271,11 @@
- local seconds="$1" maximum="$2" file child="" rc=0; shift 2
- file="$(mktemp)" || exit 1
- trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
- trap 'exit 1' HUP INT TERM
- timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
+ local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
+ tester_tools || exit 84
+ file="$(mktemp)" || exit 88
+ notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
+ trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
+ trap 'exit 88' HUP INT TERM
+ LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
+ set -o pipefail
+ maximum="$1"; head_bin="$2"; shift 2
+ "$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
+ ' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
@@ -163 +285,4 @@
- [ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
+ # Only GNU timeout and the trusted head can write this root-side channel.
+ if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
+ [ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
+ [ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
@@ -165,0 +291 @@
+
@@ -259 +385 @@
- query="$(tester_capture 5 4096 sudo -u "$USER_NAME" -H env -i "HOME=$HOME_DIR" "PATH=$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query failed"
+ query="$(tester_capture 5 7 4096 "$USER_NAME" "$HOME_DIR" "$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query $(tester_capture_reason "$?")"
--- a/scripts/case1-accept.sh
+++ b/scripts/case1-accept.sh
@@ -18,0 +19,4 @@
+# The config/probe supervisor is fixed root-validated system Bash plus setsid
+# and sleep, after the uid drop: query inner/outer 5/7 s, probe 2/4 s. Root
+# independently bounds its sudo wait. Legacy AS_USER talk and diagnostics below
+# retain their old behavior and are NOT covered by this supervisor (F5 follow-up).
@@ -110,0 +115,113 @@
+# row253-supervisor-begin
+IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
+set +m
+seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
+child="" timer="" verified=0
+stat_fields() {
+ local pid="$1" raw rest
+ if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
+ [ -e "/proc/$pid/stat" ] && return 1
+ return 3
+ fi
+ [ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
+ rest="${raw##*) }"
+ [ "$rest" != "$raw" ] || return 1
+ read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
+ [[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
+}
+clock_cs() {
+ local boot spare
+ read -r boot spare < /proc/uptime || return 1
+ [[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
+ CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
+}
+clean_session() {
+ local entry pid rc live until
+ clock_cs || return 1
+ until=$((CLOCK_CS + 25))
+ while :; do
+ live=0
+ for entry in /proc/[0-9]*/stat; do
+ pid="${entry%/stat}"; pid="${pid##*/}"
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" != 3 ] || continue
+ [ "$rc" = 0 ] || return 1
+ [ "$ST_SESSION" = "$child" ] || continue
+ case "$ST_STATE" in Z|X) continue ;; esac
+ live=1
+ if ! kill -KILL "$pid" 2>/dev/null; then
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
+ fi
+ done
+ [ "$live" != 0 ] || return 0
+ clock_cs || return 1
+ [ "$CLOCK_CS" -lt "$until" ] || return 1
+ "$sleep_bin" 0.01
+ done
+}
+finish() {
+ local rc="$?"
+ trap - EXIT HUP INT TERM
+ if [ "$verified" = 1 ]; then
+ clean_session || rc=86
+ elif [ -n "$child" ]; then
+ kill -KILL "$child" 2>/dev/null || :
+ fi
+ [ -z "$child" ] || wait "$child" 2>/dev/null || :
+ if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
+ exit "$rc"
+}
+trap finish EXIT
+trap 'exit 88' HUP INT TERM
+"$sleep_bin" "$seconds" & timer=$!
+"$setsid_bin" "$@" & child=$!
+for ((attempt=0; attempt<100; attempt++)); do
+ rc=0; stat_fields "$child" || rc=$?
+ [ "$rc" = 0 ] || exit 85
+ if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.002
+done
+[ "$verified" = 1 ] || exit 85
+while kill -0 "$child" 2>/dev/null; do
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.01
+done
+kill -0 "$timer" 2>/dev/null || exit 81
+rc=0; wait "$child" || rc=$?
+[ "$rc" = 0 ] || exit 87
+exit 0
+MOSAIC_TESTER_SUPERVISOR
+# row253-supervisor-end
+
+tester_tools() {
+ local input target p st magic
+ TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
+ for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
+ target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
+ case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
+ [ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
+ p="$target"
+ while :; do
+ st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
+ [ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
+ [ "$p" != / ] || break
+ p="${p%/*}"; [ -n "$p" ] || p=/
+ done
+ magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
+ [ "$magic" = 7f454c46 ] || return 1
+ case "$input" in
+ /bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
+ /usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
+ /usr/bin/head) TS_HEAD="$target" ;;
+ esac
+ done
+}
+tester_capture_reason() {
+ case "$1" in
+ 81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
+ 84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
+ *) echo inner-command ;;
+ esac
+}
@@ -112,5 +229,11 @@
- local seconds="$1" maximum="$2" file child="" rc=0; shift 2
- file="$(mktemp)" || exit 1
- trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
- trap 'exit 1' HUP INT TERM
- timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
+ local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
+ tester_tools || exit 84
+ file="$(mktemp)" || exit 88
+ notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
+ trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
+ trap 'exit 88' HUP INT TERM
+ LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
+ set -o pipefail
+ maximum="$1"; head_bin="$2"; shift 2
+ "$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
+ ' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
@@ -120 +243,4 @@
- [ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
+ # Only GNU timeout and the trusted head can write this root-side channel.
+ if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
+ [ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
+ [ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
@@ -122,0 +249 @@
+
@@ -196 +323 @@
-QUERY="$(tester_capture 5 4096 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query failed"
+QUERY="$(tester_capture 5 7 4096 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query $(tester_capture_reason "$?")"
@@ -536 +663 @@
- peer="$(tester_capture 3 128 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why=probe
+ peer="$(tester_capture 2 4 128 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why="probe-$(tester_capture_reason "$?")"
@@ -540 +667 @@
- else why=probe; fi
+ else [ -n "$why" ] || why=probe; fi
--- a/packages/cli/tests/case-sudo-fixture.mjs
+++ b/packages/cli/tests/case-sudo-fixture.mjs
@@ -3,0 +4,3 @@
+import { spawn } from 'node:child_process';
+import { readFileSync, readdirSync } from 'node:fs';
+
@@ -4,0 +8,13 @@
+case_sudo_entry() {
+ local record="$1" here sid; shift
+ here="$BASHPID"
+ read -r sid < <(/bin/ps -o sid= -p "$here")
+ printf "%s %s\n" "$here" "$sid" >> "$record/session-pids"
+ if { [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
+ { [ -f "$record/stall-probe" ] && [[ "$*" == *SO_PEERCRED* ]]; }; then
+ trap '' TERM HUP
+ [ ! -f "$record/stall-output" ] || /bin/cat "$record/stall-output"
+ exec /bin/sleep 60
+ fi
+ exec "$@"
+}
@@ -6,2 +22,3 @@
- local record="$1" child="" rc=0; shift
- if [ "$(cat "$record/session-mode" 2>/dev/null)" != hop ]; then exec "$@"; fi
+ local record="$1" child="" rc=0 mode=exec; shift
+ [ ! -f "$record/session-mode" ] || mode="$(<"$record/session-mode")"
+ if [ "$mode" != hop ]; then case_sudo_entry "$record" "$@"; return "$?"; fi
@@ -13,7 +30,2 @@
- /usr/bin/setsid /bin/bash -c '
- record="$1"; shift
- here="$BASHPID"
- read -r pid sid < <(/bin/ps -o pid=,sid= -p "$here")
- printf "%s %s\n" "$pid" "$sid" >> "$record/session-pids"
- exec "$@"
- ' -- "$record" "$@" &
+ export -f case_sudo_entry
+ /usr/bin/setsid /bin/bash -c 'case_sudo_entry "$@"' -- "$record" "$@" &
@@ -31 +43,3 @@
- const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>Number(line.trim().split(/\s+/)[1])));
+ // Exec fake entries share the test runner's session. Only self-led sessions
+ // are isolated and safe to scan here; exec coverage also checks exact PIDs.
+ const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number)).filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
@@ -41,0 +56,87 @@
+
+export function recordedPids(file) {
+ try{return readFileSync(file,'utf8').trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number));}
+ catch(error){if(error.code==='ENOENT')return [];throw error;}
+}
+
+export function recordedLivePids(file) {
+ return sessionPairs(recordedPids(file)).filter(([pid,sid])=>{
+ try{
+ const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
+ return Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X';
+ }catch(error){if(error.code==='ENOENT'||error.code==='ESRCH')return false;throw error;}
+ }).map(([pid])=>pid);
+}
+
+export function killRecordedPids(file) {
+ killPairs(recordedPids(file));
+}
+
+function killPairs(pairs) {
+ for(const [pid,sid] of sessionPairs(pairs)){
+ try{
+ const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
+ if(Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X')process.kill(pid,'SIGKILL');
+ }catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
+ }
+}
+
+function sessionPairs(pairs) {
+ // Only self-led records authorize a session scan. Exec-mode records belong
+ // to the runner's session and authorize checking their exact PIDs only.
+ const sessions=new Set(pairs.filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
+ const found=new Map(pairs.map(pair=>[pair[0],pair]));
+ if(sessions.size)for(const entry of readdirSync('/proc')){
+ if(!/^\d+$/.test(entry))continue;
+ try{
+ const raw=readFileSync(`/proc/${entry}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' '),sid=Number(fields[3]);
+ if(sessions.has(sid))found.set(Number(entry),[Number(entry),sid]);
+ }catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
+ }
+ return [...found.values()];
+}
+
+// Safety net for a removed-outer-bound change. It is fixture cleanup, never
+// product supervision. The guard reads only fixture records and checks sid
+// immediately before every kill. Normal outer-bound cases return before it.
+export function fixtureGuard(t,files,milliseconds=9500) {
+ const program=`const fs=require('node:fs');setTimeout(()=>{for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}},Number(process.argv[2]));`;
+ const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds)],{stdio:'ignore'});
+ const stop=async()=>{
+ if(child.exitCode!==null||child.signalCode!==null)return;
+ const exited=new Promise(resolve=>child.once('exit',resolve));
+ child.kill('SIGKILL');await exited;
+ };
+ t.after(stop);return stop;
+}
+
+export function cleanupFixture(t,files) {
+ let captured=[];
+ const capture=()=>{captured=files.flatMap(recordedPids);};
+ // tmp(t)'s directory-removal hook may run first. Retain PID/SID pairs in
+ // memory before assertions, then verify the kernel sid again before killing.
+ t.after(()=>killPairs([...captured,...files.flatMap(recordedPids)]));
+ return capture;
+}
+
+export async function pollFixtureGone(files) {
+ for(let n=0;n<50;n++){
+ if(files.every(file=>recordedLivePids(file).length===0))return [];
+ await new Promise(resolve=>setTimeout(resolve,10));
+ }
+ return files.flatMap(recordedLivePids);
+}
+
+export function blockingQuerySource(records,kind='signals',member,proof) {
+ const py=`import os,sys\nos.setpgid(0,0)\nwith open(sys.argv[2],"a") as f:f.write(str(os.getpid())+" "+str(os.getsid(0))+chr(10))\nos.execv(sys.argv[1],[sys.argv[1],"60"])`;
+ return `import fs from 'node:fs';import {spawn} from 'node:child_process';
+const records=${JSON.stringify(records)};
+function record(pid){const s=fs.readFileSync('/proc/'+pid+'/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);fs.appendFileSync(records,pid+' '+sid+String.fromCharCode(10));}
+record(process.pid);
+export function loadSystem(){
+${kind==='sleep'?`const c=spawn('/usr/bin/sleep',['60'],{stdio:'inherit'});record(c.pid);`:''}
+${kind==='signals'?`process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});`:''}
+${kind==='member'?`const c=spawn('/usr/bin/python3',['-I','-S','-c',${JSON.stringify(py)},${JSON.stringify(member)},records],{stdio:'inherit'});for(let n=0;n<200;n++){try{const s=fs.readFileSync('/proc/'+c.pid+'/stat','utf8');if(s.includes('('+${JSON.stringify(member?.split('/').at(-1))}+') ')){fs.writeFileSync(${JSON.stringify(proof)},s);break;}}catch{}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,5);}`:''}
+Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
+}`;
+}
--- a/packages/cli/tests/case2-unit.test.mjs
+++ b/packages/cli/tests/case2-unit.test.mjs
@@ -16 +16 @@
-import { CASE_SUDO_SESSION, liveRecordedSessions } from "./case-sudo-fixture.mjs";
+import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
@@ -96 +96 @@
- (trap '' TERM; exec sleep 60) </dev/null >/dev/null 2>&1 &
+ (trap '' TERM; here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here"); printf '%s %s\\n' "$here" "$sid" >'${root}/query-owned'; exec sleep 60) </dev/null >/dev/null 2>&1 &
@@ -189,2 +189,4 @@
-// Draft counterexample for DESIGN-C4. Written after sage-2307, not run.
-test('row253 C4 query cleanup handles both exec and sudo session-hop fakes', async t=>{
+// C4 cases run through both sudo process models. Exec shares the runner's
+// session, so its session-wide check would be vacuous; exact recorded PIDs
+// are checked in both modes. Hop additionally isolates the fake session.
+for(const kind of ['sleep','signals','member']) test(`row253 C4 ${kind} query has exact inner reason and cleans all members`,async t=>{
@@ -192 +194 @@
- const h=hub(t),pids=join(h.root,'session-pids'),queryPids=join(h.root,'query-pids'),copy=join(copyDir(h),'packages/cli/src/config.mjs');
+ const h=hub(t),sessions=join(h.root,'session-pids'),queries=join(h.root,'query-pids'),files=[sessions,queries];
@@ -194,21 +196,10 @@
- writeFileSync(pids,'');
- writeFileSync(copy,`import fs from 'node:fs';process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});fs.appendFileSync(${JSON.stringify(queryPids)},process.pid+'\\n');const s=fs.readFileSync('/proc/self/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);if(sid===process.pid)fs.appendFileSync(${JSON.stringify(pids)},process.pid+' '+sid+'\\n');export function loadSystem(){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);}`);
- t.after(()=>{
- if(!existsSync(pids))return;
- // Explicit fixture cleanup is safe only for these recorded fixture PIDs.
- for(const line of [read(pids),existsSync(queryPids)?read(queryPids):''].join('\n').trim().split('\n')){
- const pid=Number(line.trim().split(/\s+/)[0]);
- if(pid>0)try{process.kill(pid,'SIGKILL');}catch{}
- }
- });
- const r=unit(h,{profile:'factory'});refuses(r,/query/);
- for(let n=0;n<50;n++){
- if(!liveRecordedSessions(pids,read,()=>readdirSync('/proc')).length)break;
- await new Promise(resolve=>setTimeout(resolve,10));
- }
- assert.deepEqual(liveRecordedSessions(pids,read,()=>readdirSync('/proc')),[],mode);
- for(const pid of read(queryPids).trim().split('\n')){
- let state='gone';try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{}
- assert.ok(state==='gone'||state==='Z',`${mode}: query ${pid} still ${state}`);
- }
- assert.equal(existsSync(h.log),false,mode);
+ const member=join(h.root,'q ) '+String.fromCharCode(10)+'mem'),proof=join(h.root,'comm-proof');
+ if(kind==='member')symlinkSync('/usr/bin/sleep',member);
+ writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),blockingQuerySource(queries,kind,member,proof));
+ const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
+ const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
+ refuses(r,/^case2-unit: tester config query inner-timeout\n$/);
+ assert.ok(seconds<7,`${kind}/${mode}: ${seconds}s exceeds outer bound`);
+ assert.deepEqual(await pollFixtureGone(files),[],mode);
+ if(kind==='member')assert.ok(read(proof).includes('('+member.split('/').at(-1)+') '),'member comm with newline and closing parenthesis was observed');
+ assert.equal(existsSync(h.log),false,mode);await stopGuard();
@@ -215,0 +207,30 @@
+});
+
+test('row253 C4 overflow is output-limit in both sudo modes',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=hub(t),files=[join(h.root,'session-pids')];writeFileSync(join(h.root,'session-mode'),mode);const capture=cleanupFixture(t,files);
+ writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:${JSON.stringify(h.dataRoot)}});`);
+ const r=unit(h,{profile:'factory'});capture();refuses(r,/^case2-unit: tester config query output-limit\n$/);
+ assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
+ assert.deepEqual(await pollFixtureGone(files),[]);
+ }
+});
+
+test('row253 C4 stalled supervisor uses the outer reason and explicit residual cleanup',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=hub(t),file=join(h.root,'session-pids'),files=[file];writeFileSync(join(h.root,'session-mode'),mode);
+ writeFileSync(join(h.root,'stall-config'),'');writeFileSync(join(h.root,'stall-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
+ const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
+ const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
+ refuses(r,/^case2-unit: tester config query outer-timeout\n$/);
+ assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s slack includes root preflight`);
+ assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
+ if(mode==='hop')assert.ok(recordedLivePids(file).length>0,'R3 residual exists before fixture cleanup');
+ killRecordedPids(file);assert.deepEqual(await pollFixtureGone(files),[], 'explicit fixture cleanup of R3 residual');await stopGuard();
+ }
+});
+
+test('row253 C4 inline supervisor is byte-identical in both host scripts',()=>{
+ const extract=s=>s.split('# row253-supervisor-begin\n')[1]?.split('# row253-supervisor-end')[0];
+ const a=extract(read(UNIT)),b=extract(read(join(REPO,'scripts/case1-accept.sh')));
+ assert.ok(a?.length>100);assert.equal(a,b);
@@ -222,2 +243,2 @@
- t.after(()=>{if(pid)try{process.kill(pid,'SIGKILL');}catch{}});
- const r=unit(h,{profile:'factory'});
+ const capture=cleanupFixture(t,[join(h.root,'query-owned')]);
+ const r=unit(h,{profile:'factory'});capture();
@@ -235,2 +256,2 @@
-test('row253 config parser is strict and output capture is bounded',()=>{
- const source=read(UNIT), body=source.slice(source.indexOf('tester_capture() ('),source.indexOf('tester_location() {'));
+test('row253 config parser is strict',()=>{
+ const source=read(UNIT), body=source.slice(source.indexOf('tester_facts() {'),source.indexOf('tester_location() {'));
@@ -244,2 +264,0 @@
- const r=spawnSync('/bin/bash',['-c',body+'\ntester_capture 1 32 /usr/bin/head -c 100 /dev/zero']);
- assert.notEqual(r.status,0);assert.equal(r.stdout.length,0);
--- a/packages/cli/tests/case1-accept.test.mjs
+++ b/packages/cli/tests/case1-accept.test.mjs
@@ -14 +14 @@
-import { CASE_SUDO_SESSION } from "./case-sudo-fixture.mjs";
+import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
@@ -90 +90,2 @@
-case_sudo_run "$HC" "$ROOT_DIR$cmd" "\${args[@]}"`,
+[ ! -e "$ROOT_DIR$cmd" ] || cmd="$ROOT_DIR$cmd"
+case_sudo_run "$HC" "$cmd" "\${args[@]}"`,
@@ -100 +101,5 @@
-[ -f "$HC/peer-hang" ] && { echo "$BASHPID" > "$HC/peer-pid"; exec sleep 60; }
+[ -f "$HC/peer-hang" ] && {
+ here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here")
+ echo "$here" > "$HC/peer-pid"; printf '%s %s\\n' "$here" "$sid" >> "$HC/query-pids"
+ trap '' TERM HUP; exec sleep 60
+}
@@ -303,0 +309,58 @@
+test('row253 C4 acceptance query inner reason holds under both sudo models',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];
+ put(join(h.hc,'session-mode'),mode);
+ put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),blockingQuerySource(files[1]));
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
+ assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query inner-timeout\n');
+ assert.ok(seconds<7,`${mode}: ${seconds}s`);assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
+ assert.deepEqual(await pollFixtureGone(files),[]);await stop();
+ }
+});
+
+test('row253 C4 acceptance query outer reason discards stdout and names residual cleanup',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-config'),'');
+ put(join(h.hc,'stall-output'),JSON.stringify({dataRoot:HOME+'/.mosaic-dev',socket:true}));
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
+ assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query outer-timeout\n');
+ assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s preflight slack`);
+ assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
+ if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'R3 session remains before fixture cleanup');
+ killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit R3 fixture cleanup');await stop();
+ }
+});
+
+test('row253 C4 acceptance query overflow is output-limit in both models',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);const capture=cleanupFixture(t,files);
+ put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:'${HOME}/.mosaic-dev'});`);
+ const r=run(h);capture();assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query output-limit\n');
+ assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);assert.deepEqual(await pollFixtureGone(files),[]);
+ }
+});
+
+for(const fault of ['inner','outer','overflow'])test(`row253 C4 P10 ${fault} reason holds under both sudo models`,async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];put(join(h.hc,'session-mode'),mode);
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files,7000);
+ if(fault==='inner')put(join(h.hc,'peer-hang'),'');
+ if(fault==='outer'){put(join(h.hc,'stall-probe'),'');put(join(h.hc,'stall-output'),'200 997 997\n');}
+ if(fault==='overflow'){put(join(h.hc,'peer-output'),'200 997 997\n'+'X'.repeat(256));put(join(h.hc,'peer-status'),'0');}
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
+ assert.equal(r.status,0,r.out+r.err);
+ const reason=fault==='inner'?'inner-timeout':fault==='outer'?'outer-timeout':'output-limit';
+ assert.match(r.out,new RegExp('P10 tester-socket-squat unnamed boundary=failed reason=probe-'+reason+' '));
+ assert.doesNotMatch(r.out,/boundary=observed/);
+ if(fault==='inner')assert.ok(seconds<4,`${mode}: ${seconds}s includes functional checks`);
+ if(fault==='outer'){
+ assert.ok(seconds>=4&&seconds<6,`${mode}: ${seconds}s; 2s functional-check slack`);
+ if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'stalled supervisor is R3 residual');
+ for(const file of files)killRecordedPids(file);
+ }
+ assert.deepEqual(await pollFixtureGone(files),[],fault==='outer'?'explicit R3 cleanup':mode);await stop();
+ }
+});
+
--- a/packages/webui/README.md
+++ b/packages/webui/README.md
@@ -128 +128 @@
-need `--port`, and 7330, 7332 or 7333 as `--port` or `--tester-port` exits 4
+need `--port`, and 7330, 7332 or 7333 as the main or legacy tester port exits 4
@@ -130 +130 @@
-`--tester-port` that another installed Console unit in the unit directory
+legacy tester port that another installed Console unit in the unit directory
@@ -144 +144 @@
-`serve --sso [--send] [--tester-port N]` makes the Console an OpenID Connect
+`serve --sso [--send] [--tester-socket]` makes the Console an OpenID Connect
@@ -176 +176 @@
-- With `--tester-port`, the tester listener refuses any request carrying an
+- With `--tester-socket`, the tester listener refuses any request carrying an
--- a/docs/plans/DEFERRED.md
+++ b/docs/plans/DEFERRED.md
@@ -273,14 +272,0 @@
-- **Gate runs mount the host's pi auth file.** `compose.yaml:43` mounts
- `${PI_AUTH_FILE:-/home/jwoltje/.pi/agent/auth.json}` read-only into
- the agent container. No suite sets `PI_AUTH_FILE`, so the
- `test-task.sh` cases that call `scripts/run-task.sh`, and
- `test-release.sh` through `release.sh` `health_check` (`:41-54`), run
- Compose with the host file. Row 197's lane `cleanEnv`
- (`scripts/testing/gate-lane.mjs:109`) keeps the default too: it neither
- sets nor passes `PI_AUTH_FILE`. The mount is read-only, and the legacy
- gate already worked this way before row 197. `test-auth.sh` uses its
- own fixtures but calls `auth.sh`, not Compose. The fix: a fixture
- `PI_AUTH_FILE` for the Compose-reaching cases, set by the suites or by
- the lane's `cleanEnv`, and a test that no gate step mounts the host
- default. It needs its own row. Webby N4 on the row 197 proof driver
- (bba870ee9), traced by Herb (herb-0051). (2026-10-21)
@@ -362,0 +349,34 @@
+- **Console library routes: four library codes still map to 502.**
+ (Dewey N2 on the row 254 manifest, #1759 comment 30173.) Row 254
+ gave six library codes a STATUS entry in `packages/webui/src/bus.mjs`.
+ `library-mapping-invalid`, `library-remote-invalid`,
+ `library-state-invalid` and `library-git` still fall through
+ `busStatus` to 502. The row that adds the Console library routes
+ decides their statuses and gives its `humanCall` for `library.sync`
+ a timeout over 130 seconds. (2026-10-21)
+- **T27 sweep test leaves two `sleep 60` children running.**
+ (Darkwing row 251 slot, darkwing-0113 and darkwing-0117; Dewey on
+ r2, comment 30168.) The sweep test spawns two detached shells,
+ `planted` at `host-run.test.mjs:528` and `decoy` at `:536`, each
+ running `sleep 60; :` with the run dir as `$0`. `sweepRunDir` kills
+ the planted shell by pid (`host-run.mjs:238`), and `t.after` kills
+ only the decoy shell. Neither kill reaches the `sleep` child, so each
+ one lives for up to 60 s after the test and trips any busy check that
+ follows. The fix is to kill the process group in both `t.after`
+ hooks: `process.kill(-pid, 'SIGKILL')`. Each shell is spawned
+ detached, so it leads its own group. `exec sleep 60` doesn't work.
+ The sweep matches the run dir in `/proc/<pid>/cmdline`
+ (`host-run.mjs:236`), and exec drops the `$0` that carries it, so the
+ planted process would survive and the "planted process killed" assert
+ would fail. This binds the next row that touches
+ `packages/bus/tests/host-run.test.mjs`. (2026-10-21)
+- **Real-model suite cases run outside the gate.** Row 244 moved
+ `test-task.sh` fork recall (M11) and user recall (M16) behind
+ `MOSAIC_LIVE_PROVIDER=1`, and `test-release.sh` activations to the mock
+ adapter. Before any re-pin whose tree changes `packages/harness` or the
+ session fork code (`scripts/mosaic-task.mjs` session and
+ `sessionForkFrom` handling, the pi adapter), an operator runs
+ `MOSAIC_LIVE_PROVIDER=1 bash scripts/test-task.sh` with the real pi
+ credential and a real `release.sh activate`, and records both results on
+ the re-pin row. Morgana's wording (row 244 BUILD.md), Webby N2 on row 244
+ r1 (#1757). (2026-10-21)
@@ -427,0 +448,14 @@
+- Gate runs mount the host's pi auth file (Webby N4 on the row 197 proof
+ driver, 059577886): closed by row 244 (#1757), landing e69260a3c. The
+ suites and the lane's `cleanEnv` export a 0600 fixture `PI_AUTH_FILE`,
+ `guardDocker` refuses a Compose run without one, and
+ `packages/cli/tests/gate-lane-auth.test.mjs` proves no gate step mounts
+ the host default. The real-model cases this moved off the gate are open
+ above.
+
+## Row253 F5: legacy Case AS_USER supervision
+
+Tracked as row 276, Gitea #1781 (sage-2331), owner Gander, reviewer Duckworth, after row253.
+Brief: docs/plans/2026-10-22_case-sudo-legacy-followup.md, “Brief: supervised legacy AS_USER calls”.
+
+case1-accept.sh's legacy timeout330 around AS_USER talk and its AS_USER precheck, Claude version, PM launches and Node version diagnostics predate the new socket metadata/probe helpers. sudo's pty monitor can move them to a separate session; the old outer timeout does not prove their descendants are killed. The C4 repair in row253 covers only its new config query and peer probe. These legacy paths need the reviewed supervisor or a header explicitly explaining why each remains unsupervised. The follow-up must preserve the 300-second talk/reply contract, concurrent F5 scan, diagnostics and secret-output refusals, with fake-only session-hop tests and Sage's slot. No host or live unit change is authorized.
@@ -0,0 +1,275 @@
[
{
"id": "M1",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "ec09134dd5e735d5e2d3c9a92162a6d0dd9cc2e1718cf2e2efd3d508a7db3d13"
},
{
"id": "M2",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "5757898fb9003312b67c98037a524dcf06a8dc43e07572a21aae1afb0067e0ff"
},
{
"id": "M3",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "e9df1616060cb45e228e8c3a633299cbd2cc8e8d913544678eab3299bb3a8548"
},
{
"id": "M4",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "3cfe29b917726d13819fabc390845df1367423bb251d3cce2cd270164fe143d0"
},
{
"id": "M5",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "41f91ef6413543602ef46f412e8e289a6dd5ce24bd271cebba05c819242ebe56"
},
{
"id": "M6",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "19d71aed6a102e519f97151ca83d01dc9534b7c4722b1365b0fc365daac4bba9"
},
{
"id": "M7",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "7c629e0da6101109cd9ae7f8028fd68bb16a43e2b799ee0c8de87f13e1055652"
},
{
"id": "M8",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "dcc4a888a21c8a6cbfbfbe48df892c81b8fd038868e0dcd9b1acabf2dceb5313"
},
{
"id": "M9",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "2dccf96df3518c4e604e36245e62a2d446719e1ae1bd5bd47890fc692acad43c"
},
{
"id": "M10",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "ba59a37dac46254f148a0f40367186172ca9e8bbb98020a0c97d7d325c5e86e0"
},
{
"id": "M11",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "58f9e51f48d41c0887894f34e278937911c380d18a63770342a8e1afd1f5dc59"
},
{
"id": "M12",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "3a755dd95af1d11f0cc0bf1ae59a676b62ee7d7b72174ebb88388c6d48fa41ff"
},
{
"id": "M13",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "b230bc14a1197c9986ddb99689a0b0dc71453903f1a6a07328c9d10ceec6ff30"
},
{
"id": "M14",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "0179e9d29cc9461b665e807b00e683ce30f30e2903efc1f8ec4a1599876681f4"
},
{
"id": "M15",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "1d93cd35e92a079cbe31b71a69314fd98d1547e35d62c9a152a5fa4a67c873eb"
},
{
"id": "M16",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5c60833fa9e7fa40bfdd10290cc3e91c7476b5b1f0975367760bbedddbf91c19"
},
{
"id": "M17",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "845c7ccf5c178fb783cbf8c88d329780a0f9e4c63a2c262b6473f0eebd0f0c5e"
},
{
"id": "M18",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "ab32a811142fd84de78fe93ac541b6ee823d7d9422f816e6bd15035149a32ac2"
},
{
"id": "M19",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5ee29b36b15b45f031a6651e82898d6bb1a5ebeff4b47c98b5777f00ab535ed3"
},
{
"id": "M20",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "1c85dc5f63d4766226bb13b701b0358536e7a2a918b224841650acf244a0638a"
},
{
"id": "M21",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "b13942590fb6f518446302340c66465a16ba9ab9e4429f6fcc01714cbc53c63e"
},
{
"id": "M22",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5a87dc77e3bba09ecb9f6b90c653cbfa38117c13674c770c0af0d941c2c63fe1"
},
{
"id": "M23",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "2b7d4d59bfe4ec8f2c9d7c5562be1c57a4ecc6e4d0b4960c1ae44958f2ad8dd9"
},
{
"id": "M24",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "e0f388ef748d5e6a3df40ac486891c14d91fcafdb8ce03b36ef20f93aebbdfe2"
},
{
"id": "M25",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5ba47cf8b8a045967887d108d71a7456cba89e73c096cbdfbf76e010b4d3c5f2"
},
{
"id": "M26",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "cca6f1d39fde22603cbd649d6712c0e05041bd1a86eb7cd9b1c410f58c734a09"
},
{
"id": "M27",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "8b6b71c84ca96f3caf6e832988573a608fccd3c74fa7798468565d0b8b202c5a"
},
{
"id": "M28",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "25e1d996be7c62091b1a3a837fd8fe95871bc01f2a2d509eb1ae09785b51ea48"
},
{
"id": "M29",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "eb5e9d9a069ce226ab70baef11dc7897ae4e3d44ac4807cbed838ba1ded47494"
},
{
"id": "M30",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "280ba5c20d3b42423d681e69312731aa60b2396b1605e875a55d3e87d908eba1"
},
{
"id": "M31",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "2884a7d730d0357e4545cfccd446cad1fb5a98f7f1d86698f778063b0025b451"
},
{
"id": "M32",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "51b7bce4b8f88de20c68797afac71e4dd43d29119e68bcf65cb056f630a87011"
},
{
"id": "M33",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "dc86d9be155f56667ddabf5183d757b55fa339ff86af40ce039f0d63e6e228a6"
},
{
"id": "M34",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "3ef45b6044db0722ff25bd85260c818be9217cf43ea26be0e3f77872af0e70b1"
},
{
"id": "M35",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "c3de12c5a897407a2e0443536bfe5c97d4cefb757f23a5ba301b9479400c09d5"
},
{
"id": "M36",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "d5326f6da46ca4eae3b4043d2efa0a66d280627e4e652da5a62b860d6c6f2a72"
},
{
"id": "M37",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "f7d758bcd9752e6d7e6580ac935982ec9e9ff68b212c0be3d584a9b9757b9d5f"
},
{
"id": "M38",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "bf1796db389b3381dd5cec1fb9a64f493b1e36188462e531cad6fbba08775cd2"
},
{
"id": "M39",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "dbff45cd7878b4feae890a5b5d8bfdf9d95e151a9583e0d11a6802a4a7c83a45"
}
]
@@ -0,0 +1,12 @@
{
"968db0bcd..ddfb29af7": [
"docs/plans/DEFERRED.md",
"packages/cli/tests/case-sudo-fixture.mjs",
"packages/cli/tests/case1-accept.test.mjs",
"packages/cli/tests/case2-unit.test.mjs"
],
"c3d88840c..ddfb29af7": [
"docs/plans/DEFERRED.md"
],
"interpretation": "The branch-to-refactor comparison includes our unlanded test additions. From the actual branch base, only DEFERRED.md overlaps. Preserved upstream DEFERRED.md and appended the row 276 / #1781 entry. No rebase performed."
}
@@ -0,0 +1,142 @@
// Shared fake process model for the two Case host-script tests. No real sudo.
// Call case_sudo_run RECORD_DIR COMMAND... after the caller's argv checks.
// Missing session-mode preserves the original exec-style fake.
import { spawn } from 'node:child_process';
import { readFileSync, readdirSync } from 'node:fs';
export const CASE_SUDO_SESSION = String.raw`
case_sudo_entry() {
local record="$1" here sid; shift
here="$BASHPID"
read -r sid < <(/bin/ps -o sid= -p "$here")
printf "%s %s\n" "$here" "$sid" >> "$record/session-pids"
if { [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
{ [ -f "$record/stall-probe" ] && [[ "$*" == *SO_PEERCRED* ]]; }; then
trap '' TERM HUP
[ ! -f "$record/stall-output" ] || /bin/cat "$record/stall-output"
exec /bin/sleep 60
fi
exec "$@"
}
case_sudo_run() {
local record="$1" child="" rc=0 mode=exec; shift
[ ! -f "$record/session-mode" ] || mode="$(<"$record/session-mode")"
if [ "$mode" != hop ]; then case_sudo_entry "$record" "$@"; return "$?"; fi
# The fake stays as parent, just as sudo does on its pty path. KILL cannot
# be caught or forwarded. The child creates the separate terminal session.
trap '[ -z "$child" ] || kill -TERM -- "-$child" 2>/dev/null || :' TERM
trap '[ -z "$child" ] || kill -HUP -- "-$child" 2>/dev/null || :' HUP
trap '[ -z "$child" ] || kill -INT -- "-$child" 2>/dev/null || :' INT
export -f case_sudo_entry
/usr/bin/setsid /bin/bash -c 'case_sudo_entry "$@"' -- "$record" "$@" &
child=$!
# An interrupted wait returns while the child still runs; keep waiting.
while :; do
wait "$child"; rc=$?
kill -0 "$child" 2>/dev/null || break
done
return "$rc"
}
`;
export function liveRecordedSessions(recordFile, readFile, procDirs) {
// Exec fake entries share the test runner's session. Only self-led sessions
// are isolated and safe to scan here; exec coverage also checks exact PIDs.
const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number)).filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
const live=[];
for(const entry of procDirs()){
if(!/^\d+$/.test(entry))continue;
try{
const stat=readFile(`/proc/${entry}/stat`), fields=stat.slice(stat.lastIndexOf(') ')+2).split(' ');
if(sessions.has(Number(fields[3])) && fields[0]!=='Z') live.push(Number(entry));
}catch{}
}
return live;
}
export function recordedPids(file) {
try{return readFileSync(file,'utf8').trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number));}
catch(error){if(error.code==='ENOENT')return [];throw error;}
}
export function recordedLivePids(file) {
return sessionPairs(recordedPids(file)).filter(([pid,sid])=>{
try{
const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
return Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X';
}catch(error){if(error.code==='ENOENT'||error.code==='ESRCH')return false;throw error;}
}).map(([pid])=>pid);
}
export function killRecordedPids(file) {
killPairs(recordedPids(file));
}
function killPairs(pairs) {
for(const [pid,sid] of sessionPairs(pairs)){
try{
const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
if(Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X')process.kill(pid,'SIGKILL');
}catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
}
}
function sessionPairs(pairs) {
// Only self-led records authorize a session scan. Exec-mode records belong
// to the runner's session and authorize checking their exact PIDs only.
const sessions=new Set(pairs.filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
const found=new Map(pairs.map(pair=>[pair[0],pair]));
if(sessions.size)for(const entry of readdirSync('/proc')){
if(!/^\d+$/.test(entry))continue;
try{
const raw=readFileSync(`/proc/${entry}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' '),sid=Number(fields[3]);
if(sessions.has(sid))found.set(Number(entry),[Number(entry),sid]);
}catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
}
return [...found.values()];
}
// Safety net for a removed-outer-bound change. It is fixture cleanup, never
// product supervision. The guard reads only fixture records and checks sid
// immediately before every kill. Normal outer-bound cases return before it.
export function fixtureGuard(t,files,milliseconds=9500) {
const program=`const fs=require('node:fs');setTimeout(()=>{for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}},Number(process.argv[2]));`;
const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds)],{stdio:'ignore'});
const stop=async()=>{
if(child.exitCode!==null||child.signalCode!==null)return;
const exited=new Promise(resolve=>child.once('exit',resolve));
child.kill('SIGKILL');await exited;
};
t.after(stop);return stop;
}
export function cleanupFixture(t,files) {
let captured=[];
const capture=()=>{captured=files.flatMap(recordedPids);};
// tmp(t)'s directory-removal hook may run first. Retain PID/SID pairs in
// memory before assertions, then verify the kernel sid again before killing.
t.after(()=>killPairs([...captured,...files.flatMap(recordedPids)]));
return capture;
}
export async function pollFixtureGone(files) {
for(let n=0;n<50;n++){
if(files.every(file=>recordedLivePids(file).length===0))return [];
await new Promise(resolve=>setTimeout(resolve,10));
}
return files.flatMap(recordedLivePids);
}
export function blockingQuerySource(records,kind='signals',member,proof) {
const py=`import os,sys\nos.setpgid(0,0)\nwith open(sys.argv[2],"a") as f:f.write(str(os.getpid())+" "+str(os.getsid(0))+chr(10))\nos.execv(sys.argv[1],[sys.argv[1],"60"])`;
return `import fs from 'node:fs';import {spawn} from 'node:child_process';
const records=${JSON.stringify(records)};
function record(pid){const s=fs.readFileSync('/proc/'+pid+'/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);fs.appendFileSync(records,pid+' '+sid+String.fromCharCode(10));}
record(process.pid);
export function loadSystem(){
${kind==='sleep'?`const c=spawn('/usr/bin/sleep',['60'],{stdio:'inherit'});record(c.pid);`:''}
${kind==='signals'?`process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});`:''}
${kind==='member'?`const c=spawn('/usr/bin/python3',['-I','-S','-c',${JSON.stringify(py)},${JSON.stringify(member)},records],{stdio:'inherit'});for(let n=0;n<200;n++){try{const s=fs.readFileSync('/proc/'+c.pid+'/stat','utf8');if(s.includes('('+${JSON.stringify(member?.split('/').at(-1))}+') ')){fs.writeFileSync(${JSON.stringify(proof)},s);break;}}catch{}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,5);}`:''}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
}`;
}
@@ -0,0 +1,707 @@
#!/usr/bin/env bash
# Row 195 (#1693): the Case 1 acceptance on 10.1.1.53, design r7 section 5
# (agents/darkwing/work/queue-195/DESIGN.md). Deviations and decisions: agents/darkwing/work/queue-195/BUILD.md.
#
# case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N
#
# 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, main port and tester socket marker 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 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. A bounded config/target query imports the copy only as the stack
# user, with sudo and an empty environment. Root parses its two facts in bash. P10 uses the
# fixed system Python, also as the stack user, for a read-only SO_PEERCRED observation.
# The config/probe supervisor is fixed root-validated system Bash plus setsid
# and sleep, after the uid drop: query inner/outer 5/7 s, probe 2/4 s. Root
# independently bounds its sudo wait. Legacy AS_USER talk and diagnostics below
# retain their old behavior and are NOT covered by this supervisor (F5 follow-up).
#
# 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
# `unnamed`, which means no Q182, Q183 or Mos ruling names the path; each unnamed line goes to Mos.
#
# Exit 0 when every F check passes, 1 when one fails, 2 on usage or a refusal (a bad args file, or
# --root misused). A P check never changes the exit.
#
# Test seam, for packages/cli/tests/case1-accept.test.mjs only. Root takes none:
# --root DIR prefixes the args file, /etc, /proc, the stack's home, the host check and node, and
# reads the account database from DIR/etc/passwd and DIR/etc/group instead of getent
set -uo pipefail
usage() { echo "usage: case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N" >&2; exit 2; }
refuse() { echo "case1-accept: refusing: $*" >&2; exit 2; }
BUSINESS="" ARGS_FILE="" CLAUDE_PIN="" NODE_PIN="" ROOT=""
while [ $# -gt 0 ]; do
case "$1" in
--business) [ $# -ge 2 ] && [[ "$2" =~ ^[a-z0-9][a-z0-9-]*$ ]] || usage; BUSINESS="$2"; shift 2 ;;
--args) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] || usage; ARGS_FILE="$2"; shift 2 ;;
--claude-pin) [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; CLAUDE_PIN="$2"; shift 2 ;;
--node-pin) [ $# -ge 2 ] && [[ "$2" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; NODE_PIN="$2"; shift 2 ;;
--root) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [ -d "$2" ] || usage; ROOT="${2%/}"; shift 2 ;;
*) usage ;;
esac
done
[ -n "$BUSINESS" ] && [ -n "$ARGS_FILE" ] && [ -n "$CLAUDE_PIN" ] && [ -n "$NODE_PIN" ] || usage
if [ "$(id -u)" = 0 ]; then
[ -z "$ROOT" ] || refuse "--root is a test seam, and root takes none"
PATH=/usr/sbin:/usr/bin:/sbin:/bin
else
[ -n "$ROOT" ] || refuse "run it as root"
fi
cd / || refuse "cannot cd to /"
# --- The args file, by stack-upgrade.sh's read_args rules, plus the serve ports ---
f="$ROOT$ARGS_FILE"
[ -f "$f" ] && [ ! -L "$f" ] || refuse "no regular file $ARGS_FILE"
st="$(stat -c '%u %a' -- "$f")" || refuse "cannot stat $ARGS_FILE"
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) ||
refuse "$ARGS_FILE must be $(id -un)'s with no group or other write (it is uid ${st% *}, mode ${st#* })"
mapfile -t ARGS < "$f" || refuse "cannot read $ARGS_FILE"
S_USER="" S_HOME="" A_BUSINESS="" NODE="" COPY_ID="" CLAUDE="" PROFILE="" PORT="" TESTER_SOCKET=0
copies=0 past=""
for ((i = 0; i < ${#ARGS[@]}; i++)); do
a="${ARGS[i]}" v="${ARGS[i + 1]:-}"
if [ -n "$past" ]; then
case "$a" in
--port) PORT="$v"; i=$((i + 1)) ;;
--business) [ "$v" = "$BUSINESS" ] || refuse "serve business differs"; i=$((i + 1)) ;;
--tester-socket) TESTER_SOCKET=$((TESTER_SOCKET + 1)) ;;
--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;
*) refuse "unknown serve flag $a" ;;
esac
continue
fi
case "$a" in
--) past=1 ;;
--pm) ;;
--user) S_USER="$v"; i=$((i + 1)) ;;
--home) S_HOME="$v"; i=$((i + 1)) ;;
--business) A_BUSINESS="$v"; i=$((i + 1)) ;;
--node) NODE="$v"; i=$((i + 1)) ;;
--claude) CLAUDE="$v"; i=$((i + 1)) ;;
--profile) PROFILE="$v"; i=$((i + 1)) ;;
--copy) COPY_ID="$v"; copies=$((copies + 1)); i=$((i + 1)) ;;
*) refuse "$ARGS_FILE has $a before --, which case2-unit.sh does not take" ;;
esac
done
[ "$copies" = 1 ] || refuse "$ARGS_FILE needs exactly one --copy before --, not $copies"
[[ "$S_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] && [[ "$S_HOME" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$A_BUSINESS" =~ ^[a-z0-9][a-z0-9-]*$ ]] &&
[[ "$NODE" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] && [[ "$CLAUDE" =~ ^/[A-Za-z0-9/._+@-]+$ ]] ||
refuse "$ARGS_FILE lacks a plain --user, --home, --business, --node, --claude or --copy"
[ "$PROFILE" = factory ] || refuse "$ARGS_FILE has --profile '${PROFILE}', not factory"
[ "$A_BUSINESS" = "$BUSINESS" ] || refuse "$ARGS_FILE is for business $A_BUSINESS, not $BUSINESS"
[[ "$PORT" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$PORT" -le 1023 ] || refuse "$ARGS_FILE lacks a privileged numeric --port after --"
[ "$TESTER_SOCKET" = 1 ] || refuse "$ARGS_FILE needs one bare --tester-socket after --"
COPY="$S_HOME/.local/share/mosaic-stack/copies/$COPY_ID"
DATA_ROOT="$S_HOME/.mosaic-dev"
CRED="$S_HOME/.config/mosaic-dev/secrets/claude-session.env"
BUSINESS_FILE="$S_HOME/.config/mosaic-dev/businesses/$BUSINESS.json"
BUS_UNIT="mosaic-bus@$BUSINESS.service" CON_UNIT=mosaic-webui.service
HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh
SETTINGS=/etc/claude-code/managed-settings.json
LABEL_OUTER='bwrap (enforce)' LABEL_CHILD='bwrap//&unpriv_bwrap (enforce)'
AS_USER=(sudo -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin")
WORK="$(mktemp -d)" || refuse "cannot make a temp dir"
trap 'rm -rf -- "$WORK"' EXIT
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
# row253-supervisor-begin
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
stat_fields() {
local pid="$1" raw rest
if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
[ -e "/proc/$pid/stat" ] && return 1
return 3
fi
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
[[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
}
clock_cs() {
local boot spare
read -r boot spare < /proc/uptime || return 1
[[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
}
clean_session() {
local entry pid rc live until
clock_cs || return 1
until=$((CLOCK_CS + 25))
while :; do
live=0
for entry in /proc/[0-9]*/stat; do
pid="${entry%/stat}"; pid="${pid##*/}"
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" != 3 ] || continue
[ "$rc" = 0 ] || return 1
[ "$ST_SESSION" = "$child" ] || continue
case "$ST_STATE" in Z|X) continue ;; esac
live=1
if ! kill -KILL "$pid" 2>/dev/null; then
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
fi
done
[ "$live" != 0 ] || return 0
clock_cs || return 1
[ "$CLOCK_CS" -lt "$until" ] || return 1
"$sleep_bin" 0.01
done
}
finish() {
local rc="$?"
trap - EXIT HUP INT TERM
if [ "$verified" = 1 ]; then
clean_session || rc=86
elif [ -n "$child" ]; then
kill -KILL "$child" 2>/dev/null || :
fi
[ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
"$setsid_bin" "$@" & child=$!
for ((attempt=0; attempt<100; attempt++)); do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 85
if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
tester_tools() {
local input target p st magic
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
p="$target"
while :; do
st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
[ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
[ "$p" != / ] || break
p="${p%/*}"; [ -n "$p" ] || p=/
done
magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
case "$input" in
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
/usr/bin/head) TS_HEAD="$target" ;;
esac
done
}
tester_capture_reason() {
case "$1" in
81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
*) echo inner-command ;;
esac
}
tester_capture() (
local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
tester_tools || exit 84
file="$(mktemp)" || exit 88
notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
trap 'exit 88' HUP INT TERM
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
set -o pipefail
maximum="$1"; head_bin="$2"; shift 2
"$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
# Only GNU timeout and the trusted head can write this root-side channel.
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
[ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
[ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
[ "${5:-}" != observe ] || return 0
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# --- Output ---
MARKS=(pass FAIL closed "accepted Q182+Q183" "accepted Q183" "accepted mos-1096" unnamed)
declare -A COUNT=()
F_FAILED=0
# Text the stack's user wrote, made safe to print: printable ASCII, one line, at most 200 bytes.
clean() { tr -c '[:print:]' ' ' <<< "$1" | cut -c1-200 | sed 's/ *$//'; }
line() { local id="$1" path="$2" mark="$3"; shift 3; echo "$id $path $mark $*"; COUNT[$mark]=$(( ${COUNT[$mark]:-0} + 1 )); }
fpass() { line "$1" "$2" pass "${@:3}"; }
ffail() { line "$1" "$2" FAIL "${@:3}"; F_FAILED=1; }
# --- The account database: getent on the host, the seam's files under --root ---
db() {
if [ -n "$ROOT" ]; then
if [ $# -gt 1 ]; then awk -F: -v k="$2" '$1 == k' "$ROOT/etc/$1"; else cat "$ROOT/etc/$1"; fi
else
getent "$@"
fi
}
uid_of() { db passwd "$1" | cut -d: -f3; }
gid_of() { db passwd "$1" | cut -d: -f4; }
# The group ids USER belongs to, as `id -G USER` prints them: the primary gid, then each group
# whose member list names USER.
groups_of() {
local g; g="$(gid_of "$1")"
[ -n "$g" ] || return 0
{ echo "$g"; db group | awk -F: -v u="$1" '{ n = split($4, m, ","); for (i = 1; i <= n; i++) if (m[i] == u) print $3 }'; } | awk '!seen[$0]++'
}
names_of_groups() { local g; for g in "$@"; do db group | awk -F: -v g="$g" '$3 == g { print $1 }'; done; }
MOSAIC_UID="$(uid_of "$S_USER")"
[[ "$MOSAIC_UID" =~ ^[1-9][0-9]*$ ]] || refuse "stack account has no non-root uid"
[ "$(db passwd "$S_USER" | cut -d: -f6)" = "$S_HOME" ] || refuse "saved home differs from account home"
QUERY="$(tester_capture 5 7 4096 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query $(tester_capture_reason "$?")"
tester_facts "$QUERY" || refuse "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || refuse "target does not support --tester-socket"
tester_location "$TS_DATA" "$S_HOME" "$MOSAIC_UID" "$ROOT" observe || refuse "tester path refuses $TS_REASON"
TESTER_PATH="$TS_SOCKET"
# --- systemd, /proc and listeners ---
# KEY=VALUE lines for UNIT's properties.
unit_show() { local u="$1"; shift; systemctl show "$u" "${@/#/-p}" 2>/dev/null; }
prop() { sed -n "s/^$2=//p" <<< "$1" | tail -n 1; }
# The real, effective, saved and filesystem uids of PID, space-separated.
proc_uids() { awk '$1 == "Uid:" { print $2, $3, $4, $5 }' "$ROOT/proc/$1/status" 2>/dev/null; }
proc_label() { local l; l="$(cat "$ROOT/proc/$1/attr/current" 2>/dev/null)" || return 1; printf '%s' "${l%$'\n'}"; }
# One `ADDRESS PIDS` line per TCP listener on port $1, PIDS comma-separated.
listeners() {
ss -Hltnp 2>/dev/null | awk -v p="$1" '{
n = split($4, a, ":"); if (a[n] != p) next
addr = substr($4, 1, length($4) - length(p) - 1); pids = ""; s = $0
while (match(s, /pid=[0-9]+/)) { pids = pids (pids == "" ? "" : ",") substr(s, RSTART + 4, RLENGTH - 4); s = substr(s, RSTART + RLENGTH) }
print addr, pids
}'
}
# 0 when port $1 has a listener and every one is on 127.0.0.1, held by pid $2 alone.
loopback_by() {
local addr pids p any=0
[ -n "$2" ] && [ "$2" != 0 ] || return 1
while read -r addr pids; do
[ -n "$addr" ] || continue
any=1
[ "$addr" = 127.0.0.1 ] || return 1
for p in ${pids//,/ }; do [ "$p" = "$2" ] || return 1; done
[ -n "$pids" ] || return 1
done <<< "$(listeners "$1")"
[ "$any" = 1 ]
}
listener_text() { local t; t="$(listeners "$1" | tr '\n' ';')"; echo "${t:-none}"; }
BUS_SHOW="$(unit_show "$BUS_UNIT" ActiveState User MainPID)"
CON_SHOW="$(unit_show "$CON_UNIT" ActiveState User MainPID)"
CON_PID="$(prop "$CON_SHOW" MainPID)"
# --- F checks ---
check_f1() {
local u show state user pid uids why=""
for u in "$BUS_UNIT" "$CON_UNIT"; do
if [ "$u" = "$BUS_UNIT" ]; then show="$BUS_SHOW"; else show="$CON_SHOW"; fi
state="$(prop "$show" ActiveState)" user="$(prop "$show" User)" pid="$(prop "$show" MainPID)"
uids="$(proc_uids "${pid:-0}")"
[ "$state" = active ] || { why+="$u is ${state:-unknown}; "; continue; }
[ "$user" = "$S_USER" ] || { why+="$u has User=${user:-unset}; "; continue; }
[ -n "$MOSAIC_UID" ] && [ "$uids" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] ||
why+="$u MainPID ${pid:-none} has uids '${uids:-unreadable}', not $S_USER's ${MOSAIC_UID:-unknown}; "
done
if [ -z "$why" ]; then fpass F1 units-user "$BUS_UNIT and $CON_UNIT are active as $S_USER (uid $MOSAIC_UID)"
else ffail F1 units-user "${why%; }"; fi
}
check_f2() {
local rc=0
curl -fsS --max-time 10 -o /dev/null "http://127.0.0.1:$PORT/healthz" 2>/dev/null || rc=$?
if [ "$rc" != 0 ]; then ffail F2 console-loopback "curl http://127.0.0.1:$PORT/healthz exited $rc"
elif ! loopback_by "$PORT" "$CON_PID"; then ffail F2 console-loopback "port $PORT listeners are '$(listener_text "$PORT")', not 127.0.0.1 by the Console's MainPID ${CON_PID:-none}"
else fpass F2 console-loopback "healthz answers, and $PORT is bound to 127.0.0.1 only, by MainPID $CON_PID"; fi
}
# F5's scan: an outer /usr/bin/bwrap owned by the stack's user with a claude descendant. Sets F5_OK
# when one pair has both labels exactly, F5_WHY to the last mismatch otherwise.
F5_OK="" F5_WHY="no /usr/bin/bwrap process of $S_USER with a claude descendant was seen while the reply was pending"
scan_f5() {
local pid ppid uids kids q c argv0 comm lo lc
local -A parent=() children=() bw=()
# One read of every PPid line and one of every exe link, so a scan stays short on a busy host.
while read -r pid ppid; do
parent[$pid]="$ppid" children[$ppid]+=" $pid"
done < <(grep -H '^PPid:' "$ROOT"/proc/[0-9]*/status 2>/dev/null | sed -n 's#^.*/proc/\([0-9]*\)/status:PPid:[[:space:]]*\([0-9]*\)$#\1 \2#p')
while read -r pid; do
[[ "$pid" =~ ^[0-9]+$ ]] && [ -n "${parent[$pid]:-}" ] && bw[$pid]=1
done < <(find "$ROOT/proc" -mindepth 2 -maxdepth 2 -name exe -lname /usr/bin/bwrap -printf '%h\n' 2>/dev/null | sed 's#.*/##')
for pid in "${!bw[@]}"; do
[ -z "${bw[${parent[$pid]}]:-}" ] || continue
uids="$(proc_uids "$pid")"
[ -n "$MOSAIC_UID" ] && [ "${uids%% *}" = "$MOSAIC_UID" ] || continue
# Every descendant, breadth first.
kids=() q=("$pid")
while [ ${#q[@]} -gt 0 ]; do
for c in ${children[${q[0]}]:-}; do kids+=("$c") q+=("$c"); done
q=("${q[@]:1}")
done
for c in "${kids[@]}"; do
argv0="$(tr '\0' '\n' < "$ROOT/proc/$c/cmdline" 2>/dev/null | sed -n 1p)"
comm="$(cat "$ROOT/proc/$c/comm" 2>/dev/null)"
[[ "${argv0##*/}" == claude* ]] || [[ "$comm" == claude* ]] || continue
lo="$(proc_label "$pid")" lc="$(proc_label "$c")"
if [ "$lo" = "$LABEL_OUTER" ] && [ "$lc" = "$LABEL_CHILD" ]; then
F5_OK="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc'"; return 0
fi
F5_WHY="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc', not '$LABEL_OUTER' and '$LABEL_CHILD'"
done
done
return 1
}
# F3 and F5 together: the message goes out in the background and the scan runs until it is answered.
check_f3_f5() {
local marker text rc=0 out err n tpid
marker="case1-accept-$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"
text="$marker: this is the Case 1 acceptance; reply with one short line."
timeout 330 "${AS_USER[@]}" "$COPY/scripts/mosaic" talk chief "$text" --business "$BUSINESS" --tester --wait 300 > "$WORK/talk.out" 2> "$WORK/talk.err" &
tpid=$!
while :; do
scan_f5 && break
kill -0 "$tpid" 2>/dev/null || break
sleep 0.5
done
wait "$tpid" || rc=$?
# Both the sent line and the reply are on stdout; an error is on stderr.
out="$(cat "$WORK/talk.out")" err="$(cat "$WORK/talk.err")"
n="$(grep -c . <<< "$out")"
if [ "$rc" = 0 ] && grep -q "^sent [^ ]* to chief" <<< "$out" && [ "$n" -ge 2 ]; then
fpass F3 tester-reply "chief replied to $marker"
else
ffail F3 tester-reply "talk exited $rc with $n lines on stdout; last: $(clean "$(tail -n 1 <<< "${err:-$out}")")"
fi
if [ -n "$F5_OK" ]; then F5_LINE=(fpass "$F5_OK"); else F5_LINE=(ffail "$F5_WHY"); fi
}
check_f4() {
local rc=0 out
out="$("${AS_USER[@]}" "$COPY/scripts/case2-precheck.sh" --business "$BUSINESS" --profile factory 2>&1)" || rc=$?
if [ "$rc" = 0 ]; then fpass F4 precheck "case2-precheck.sh --profile factory exits 0"
else ffail F4 precheck "case2-precheck.sh --profile factory exited $rc; last: $(clean "$(tail -n 1 <<< "$out")")"; fi
}
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
}
# The PM check confirm runs (stack-release.sh pm_external): exactly one live launches list entry for
# launch.by, for this business. The stack's user runs the whole check and prints one result line.
F7_JS='
const {readFileSync}=require("node:fs");const {spawnSync}=require("node:child_process");
const [file,cli,b]=process.argv.slice(1);
let by;
try{by=JSON.parse(readFileSync(file,"utf8")).launch?.by}catch(e){console.log(`fail cannot read ${file}: ${e.code??"not JSON"}`);process.exit(0)}
if(typeof by!=="string"||!by){console.log("fail the business file has no launch.by");process.exit(0)}
const r=spawnSync(cli,["launches","list","--json"],{encoding:"utf8"});
if(r.status!==0){console.log(`fail launches list exited ${r.status}`);process.exit(0)}
let l;try{l=JSON.parse(r.stdout)}catch{console.log("fail launches list printed no JSON");process.exit(0)}
const live=Array.isArray(l)?l.filter((x)=>x&&x.instance===by&&x.live===true):[];
console.log(live.length!==1?`fail ${live.length} live launches of ${by}`:live[0].business!==b?`fail the live launch of ${by} is for ${live[0].business}`:`ok ${by} run ${live[0].run}`);'
check_f7() {
local out
out="$("${AS_USER[@]}" "$NODE" -e "$F7_JS" -- "$BUSINESS_FILE" "$COPY/scripts/mosaic" "$BUSINESS" 2>/dev/null | sed -n 1p)"
if [[ "$out" =~ ^ok\ [a-z0-9][a-z0-9-]*\ run\ [A-Za-z0-9._-]+$ ]]; then fpass F7 pm-live "one live launch: ${out#ok }"
else ffail F7 pm-live "$(clean "${out#fail }")"; fi
}
check_f8() {
local s
s="$(stat -c '%U %a %F' -- "$ROOT$CRED" 2>/dev/null)" || s="missing"
if [ ! -L "$ROOT$CRED" ] && [ "$s" = "$S_USER 600 regular file" ]; then fpass F8 credential-file "$CRED is '$s'"
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 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
}
check_f10() {
local want="/opt/mosaic-stack/node-$NODE_PIN/bin/node" v
[ "$NODE" = "$want" ] || { ffail F10 node-pin "--node $NODE is not $want"; return; }
v="$("${AS_USER[@]}" "$NODE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$NODE_PIN" ]; then fpass F10 node-pin "$NODE prints $v"
else ffail F10 node-pin "$NODE --version prints '$(clean "$v")', not $NODE_PIN"; fi
}
check_f11() {
local d="$ROOT$DATA_ROOT/discord" found
if [ -L "$d" ]; then ffail F11 no-discord "$DATA_ROOT/discord is a symlink"; return; fi
[ -d "$d" ] || { fpass F11 no-discord "$DATA_ROOT/discord is absent"; return; }
found="$(find "$d" -maxdepth 1 -name '*.json' -printf '%f ' 2>/dev/null)"
if [ -z "$found" ]; then fpass F11 no-discord "$DATA_ROOT/discord holds no binding"
else ffail F11 no-discord "$DATA_ROOT/discord holds bindings: ${found% }"; fi
}
# --- P checks ---
check_p1() {
local out
out="$(sudo -l -U "$S_USER" 2>&1)"
if grep -q "is not allowed to run sudo" <<< "$out"; then line P1 "$S_USER-sudo" closed "sudo -l: $(clean "$(sed -n 1p <<< "$out")")"
else line P1 "$S_USER-sudo" unnamed "sudo -l: $(clean "$(tr '\n' ' ' <<< "$out")")"; fi
}
check_p2() {
local gs keys="$ROOT$S_HOME/.ssh/authorized_keys" kstate n
gs="$(groups_of "$S_USER" | tr '\n' ' ')"; gs="${gs% }"
n="$(wc -w <<< "$gs")"
if [ ! -e "$keys" ] && [ ! -L "$keys" ]; then kstate=absent
elif [ -f "$keys" ] && [ ! -L "$keys" ] && [ ! -s "$keys" ]; then kstate=empty
else kstate=present; fi
if [ "$n" = 1 ] && [ "$kstate" != present ]; then line P2 "$S_USER-groups-keys" closed "id -G is '$gs'; authorized_keys is $kstate"
else line P2 "$S_USER-groups-keys" unnamed "id -G is '$gs'; authorized_keys is $kstate"; fi
}
check_p3() {
local n
n="$(sudo -l -U localadmin 2>&1 | grep -c NOPASSWD)"
line P3 localadmin-sudo "accepted Q183" "sudo -l -U localadmin lists $n NOPASSWD lines"
}
check_p4() { line P4 pve-root-pbs "accepted Q182+Q183" "root on pve over VM 1008, and PBS backups that hold $S_HOME with the credential; this script can't see pve"; }
check_p5() {
local gs reach
if [ -z "$(uid_of jarvis)" ]; then line P5 jarvis-reach closed "no jarvis account"; return; fi
gs="$(names_of_groups $(groups_of jarvis) | tr '\n' ' ')"
reach="$(tr ' ' '\n' <<< "$gs" | grep -xE 'sudo|docker|lxd' | tr '\n' ' ')"
if [ -z "$reach" ]; then line P5 jarvis-reach closed "jarvis is in none of sudo, docker and lxd (groups: ${gs% })"
else line P5 jarvis-reach unnamed "jarvis is in ${reach% }"; fi
}
check_p6() {
local g m members bad=0
for g in sudo docker lxd; do
members="$(db group "$g" | cut -d: -f4)"
for m in ${members//,/ }; do
[ "$g" = sudo ] && [ "$m" = localadmin ] && continue
line P6 "group-$g" unnamed "$m is a member of $g"; bad=1
done
done
[ "$bad" = 1 ] || line P6 group-members closed "sudo holds only localadmin, and docker and lxd have no members"
}
check_p7() {
local found
found="$(grep -rlw -- "$S_USER" "$ROOT/etc/sudoers.d" 2>/dev/null | sed "s#^$ROOT##" | tr '\n' ' ')"
if [ -z "$found" ]; then line P7 sudoers-d closed "no file under /etc/sudoers.d names $S_USER"
else line P7 sudoers-d unnamed "files naming $S_USER: ${found% }"; fi
}
check_p8() {
local other
other="$(listeners 7331)$(listeners 7333)"
if loopback_by "$PORT" "$CON_PID" && [ -z "$other" ]; then
line P8 lan-listeners closed "$PORT is on 127.0.0.1 only by MainPID $CON_PID; no TCP listener on 7331 or 7333; tester uses $TESTER_PATH"
else
line P8 lan-listeners unnamed "$PORT: $(listener_text "$PORT"); 7333: $(listener_text 7333); 7331: $(listener_text 7331); Console MainPID ${CON_PID:-none}"
fi
}
check_p9() {
local out n missing=() rc=0 err mg others
out="$("$ROOT$HOSTCHECK" --check 2>/dev/null)"
for n in 1 2 3 4 5 6; do grep -q "^ok $n " <<< "$out" || missing+=("hostcheck item $n has no ok line")
done
err="$(sudo -u jarvis /usr/bin/bwrap --version 2>&1 >/dev/null)" || rc=$?
[ "$rc" != 0 ] && grep -q "Permission denied" <<< "$err" || missing+=("sudo -u jarvis /usr/bin/bwrap --version exited $rc without Permission denied")
mg="$(gid_of "$S_USER")"
[ -n "$mg" ] && [ -z "$(db group "$S_USER" | cut -d: -f4)" ] || missing+=("group $S_USER lists members, or $S_USER has no group")
others="$(db passwd | awk -F: -v g="$mg" -v u="$S_USER" '$4 == g && $1 != u { print $1 }' | tr '\n' ' ')"
[ -z "$others" ] || missing+=("other accounts have $S_USER's gid $mg: ${others% }")
if [ ${#missing[@]} = 0 ]; then
line P9 bwrap-userns "accepted mos-1096" "hostcheck items 1-6 ok, jarvis is denied bwrap, and group $S_USER has no other member"
else
for n in "${missing[@]}"; do line P9 bwrap-userns unnamed "$n"; done
fi
}
# P10 is an observation of mutable same-uid state, never a closure ruling.
TESTER_PEER='import socket,struct,sys
s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
try:
s.settimeout(1)
s.connect(sys.argv[1])
p=struct.unpack("3i",s.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
print(*p)
finally:
s.close()'
tester_snapshot() {
local parent="${TESTER_PATH%/*}" p s pid
[ ! -L "$ROOT$parent" ] && [ ! -L "$ROOT$TESTER_PATH" ] || return 1
p="$(stat -c '%u %a %F %d %i' -- "$ROOT$parent" 2>/dev/null)" || return 1
s="$(stat -c '%u %a %F %d %i' -- "$ROOT$TESTER_PATH" 2>/dev/null)" || return 1
[[ "$p" =~ ^$MOSAIC_UID\ 700\ directory\ [0-9]+\ [0-9]+$ ]] || return 1
[[ "$s" =~ ^$MOSAIC_UID\ 600\ socket\ [0-9]+\ [0-9]+$ ]] || return 1
pid="$(prop "$(unit_show "$CON_UNIT" MainPID)" MainPID)"
[[ "$pid" =~ ^[1-9][0-9]*$ ]] && [ "$pid" = "$CON_PID" ] || return 1
[ "$(proc_uids "$pid")" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] || return 1
printf '%s|%s|%s' "$pid" "$p" "$s"
}
tester_python() {
local real bad magic
# Resolve the system entry, then validate the target and every ancestor.
[ -f "$ROOT/usr/bin/python3" ] && [ -x "$ROOT/usr/bin/python3" ] || return 1
real="$(realpath -e -- "$ROOT/usr/bin/python3" 2>/dev/null)" || return 1
[ -z "$ROOT" ] || real="${real#"$ROOT"}"
case "$real" in /home/*|/root/*|/run/user/*|"$S_HOME"/*) return 1 ;; esac
bad="$(not_root_only "$real")"; [ -z "$bad" ] || return 1
bad="$(not_root_only /usr/bin)"; [ -z "$bad" ] || return 1
magic="$(od -An -tx1 -N4 -- "$ROOT$real" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
printf '%s' "$real"
}
check_p10() {
local floor before after python peer pid uid gid why="" pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})$'
floor="$(cat "$ROOT/proc/sys/net/ipv4/ip_unprivileged_port_start" 2>/dev/null)"
[[ "$floor" =~ ^[0-9]{1,5}$ ]] && [ "$floor" -gt "$PORT" ] || why=floor
before="$(tester_snapshot)" || why=metadata
python="$(tester_python)" || why=interpreter
if [ -z "$why" ]; then
peer="$(tester_capture 2 4 128 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why="probe-$(tester_capture_reason "$?")"
if [[ "$peer" =~ $pattern ]]; then
pid="${BASH_REMATCH[1]}" uid="${BASH_REMATCH[2]}" gid="${BASH_REMATCH[3]}"
[ "$pid" = "$CON_PID" ] && [ "$uid" = "$MOSAIC_UID" ] && [ "$gid" -le 2147483647 ] || why=peer
else [ -n "$why" ] || why=probe; fi
after="$(tester_snapshot)" || why=metadata
[ "$before" = "$after" ] || why=changed
fi
if [ -z "$why" ]; then
line P10 tester-socket-squat unnamed "boundary=observed path=$TESTER_PATH parent=0700 owner=$MOSAIC_UID socket=0600 peer_pid=$pid peer_uid=$uid floor=$floor port=$PORT; hostile same-uid substitution remains"
else
line P10 tester-socket-squat unnamed "boundary=failed reason=$why path=$TESTER_PATH MainPID=${CON_PID:-none}; hostile same-uid substitution remains"
fi
}
check_p11() {
local en before
en="$(systemctl is-enabled mosaic-hostcheck.service 2>/dev/null)"
before="$(prop "$(unit_show mosaic-hostcheck.service Before)" Before)"
if [ "$en" = enabled ] && grep -qxF -- "$BUS_UNIT" <<< "${before// /$'\n'}"; then
line P11 hostcheck-before-bus closed "mosaic-hostcheck.service is enabled, and its Before= names $BUS_UNIT"
else
line P11 hostcheck-before-bus unnamed "mosaic-hostcheck.service is '${en:-unknown}', Before='$before'; it needs enabled and $BUS_UNIT"
fi
}
check_f1
check_f2
F5_LINE=()
check_f3_f5
check_f4
"${F5_LINE[0]}" F5 bwrap-label "${F5_LINE[@]:1}"
check_f6
check_f7
check_f8
check_f9
check_f10
check_f11
check_p1; check_p2; check_p3; check_p4; check_p5; check_p6
check_p7; check_p8; check_p9; check_p10; check_p11
for m in "${MARKS[@]}"; do echo "count $m ${COUNT[$m]:-0}"; done
if [ "$F_FAILED" = 0 ]; then echo "case1-accept: every F check passes; send the whole output to Duckworth, and each unnamed line to Mos"; exit 0; fi
echo "case1-accept: an F check failed, so the row fails"
exit 1
@@ -0,0 +1,580 @@
#!/usr/bin/env bash
# Row 195 (#1693): the Case 1 acceptance on 10.1.1.53, design r7 section 5
# (agents/darkwing/work/queue-195/DESIGN.md). Deviations and decisions: agents/darkwing/work/queue-195/BUILD.md.
#
# case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N
#
# 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, main port and tester socket marker 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 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. A bounded config/target query imports the copy only as the stack
# user, with sudo and an empty environment. Root parses its two facts in bash. P10 uses the
# fixed system Python, also as the stack user, for a read-only SO_PEERCRED observation.
#
# 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
# `unnamed`, which means no Q182, Q183 or Mos ruling names the path; each unnamed line goes to Mos.
#
# Exit 0 when every F check passes, 1 when one fails, 2 on usage or a refusal (a bad args file, or
# --root misused). A P check never changes the exit.
#
# Test seam, for packages/cli/tests/case1-accept.test.mjs only. Root takes none:
# --root DIR prefixes the args file, /etc, /proc, the stack's home, the host check and node, and
# reads the account database from DIR/etc/passwd and DIR/etc/group instead of getent
set -uo pipefail
usage() { echo "usage: case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N" >&2; exit 2; }
refuse() { echo "case1-accept: refusing: $*" >&2; exit 2; }
BUSINESS="" ARGS_FILE="" CLAUDE_PIN="" NODE_PIN="" ROOT=""
while [ $# -gt 0 ]; do
case "$1" in
--business) [ $# -ge 2 ] && [[ "$2" =~ ^[a-z0-9][a-z0-9-]*$ ]] || usage; BUSINESS="$2"; shift 2 ;;
--args) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] || usage; ARGS_FILE="$2"; shift 2 ;;
--claude-pin) [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; CLAUDE_PIN="$2"; shift 2 ;;
--node-pin) [ $# -ge 2 ] && [[ "$2" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; NODE_PIN="$2"; shift 2 ;;
--root) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [ -d "$2" ] || usage; ROOT="${2%/}"; shift 2 ;;
*) usage ;;
esac
done
[ -n "$BUSINESS" ] && [ -n "$ARGS_FILE" ] && [ -n "$CLAUDE_PIN" ] && [ -n "$NODE_PIN" ] || usage
if [ "$(id -u)" = 0 ]; then
[ -z "$ROOT" ] || refuse "--root is a test seam, and root takes none"
PATH=/usr/sbin:/usr/bin:/sbin:/bin
else
[ -n "$ROOT" ] || refuse "run it as root"
fi
cd / || refuse "cannot cd to /"
# --- The args file, by stack-upgrade.sh's read_args rules, plus the serve ports ---
f="$ROOT$ARGS_FILE"
[ -f "$f" ] && [ ! -L "$f" ] || refuse "no regular file $ARGS_FILE"
st="$(stat -c '%u %a' -- "$f")" || refuse "cannot stat $ARGS_FILE"
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) ||
refuse "$ARGS_FILE must be $(id -un)'s with no group or other write (it is uid ${st% *}, mode ${st#* })"
mapfile -t ARGS < "$f" || refuse "cannot read $ARGS_FILE"
S_USER="" S_HOME="" A_BUSINESS="" NODE="" COPY_ID="" CLAUDE="" PROFILE="" PORT="" TESTER_SOCKET=0
copies=0 past=""
for ((i = 0; i < ${#ARGS[@]}; i++)); do
a="${ARGS[i]}" v="${ARGS[i + 1]:-}"
if [ -n "$past" ]; then
case "$a" in
--port) PORT="$v"; i=$((i + 1)) ;;
--business) [ "$v" = "$BUSINESS" ] || refuse "serve business differs"; i=$((i + 1)) ;;
--tester-socket) TESTER_SOCKET=$((TESTER_SOCKET + 1)) ;;
--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;
*) refuse "unknown serve flag $a" ;;
esac
continue
fi
case "$a" in
--) past=1 ;;
--pm) ;;
--user) S_USER="$v"; i=$((i + 1)) ;;
--home) S_HOME="$v"; i=$((i + 1)) ;;
--business) A_BUSINESS="$v"; i=$((i + 1)) ;;
--node) NODE="$v"; i=$((i + 1)) ;;
--claude) CLAUDE="$v"; i=$((i + 1)) ;;
--profile) PROFILE="$v"; i=$((i + 1)) ;;
--copy) COPY_ID="$v"; copies=$((copies + 1)); i=$((i + 1)) ;;
*) refuse "$ARGS_FILE has $a before --, which case2-unit.sh does not take" ;;
esac
done
[ "$copies" = 1 ] || refuse "$ARGS_FILE needs exactly one --copy before --, not $copies"
[[ "$S_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] && [[ "$S_HOME" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$A_BUSINESS" =~ ^[a-z0-9][a-z0-9-]*$ ]] &&
[[ "$NODE" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] && [[ "$CLAUDE" =~ ^/[A-Za-z0-9/._+@-]+$ ]] ||
refuse "$ARGS_FILE lacks a plain --user, --home, --business, --node, --claude or --copy"
[ "$PROFILE" = factory ] || refuse "$ARGS_FILE has --profile '${PROFILE}', not factory"
[ "$A_BUSINESS" = "$BUSINESS" ] || refuse "$ARGS_FILE is for business $A_BUSINESS, not $BUSINESS"
[[ "$PORT" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$PORT" -le 1023 ] || refuse "$ARGS_FILE lacks a privileged numeric --port after --"
[ "$TESTER_SOCKET" = 1 ] || refuse "$ARGS_FILE needs one bare --tester-socket after --"
COPY="$S_HOME/.local/share/mosaic-stack/copies/$COPY_ID"
DATA_ROOT="$S_HOME/.mosaic-dev"
CRED="$S_HOME/.config/mosaic-dev/secrets/claude-session.env"
BUSINESS_FILE="$S_HOME/.config/mosaic-dev/businesses/$BUSINESS.json"
BUS_UNIT="mosaic-bus@$BUSINESS.service" CON_UNIT=mosaic-webui.service
HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh
SETTINGS=/etc/claude-code/managed-settings.json
LABEL_OUTER='bwrap (enforce)' LABEL_CHILD='bwrap//&unpriv_bwrap (enforce)'
AS_USER=(sudo -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin")
WORK="$(mktemp -d)" || refuse "cannot make a temp dir"
trap 'rm -rf -- "$WORK"' EXIT
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
tester_capture() (
local seconds="$1" maximum="$2" file child="" rc=0; shift 2
file="$(mktemp)" || exit 1
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
trap 'exit 1' HUP INT TERM
timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
[ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
[ "${5:-}" != observe ] || return 0
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# --- Output ---
MARKS=(pass FAIL closed "accepted Q182+Q183" "accepted Q183" "accepted mos-1096" unnamed)
declare -A COUNT=()
F_FAILED=0
# Text the stack's user wrote, made safe to print: printable ASCII, one line, at most 200 bytes.
clean() { tr -c '[:print:]' ' ' <<< "$1" | cut -c1-200 | sed 's/ *$//'; }
line() { local id="$1" path="$2" mark="$3"; shift 3; echo "$id $path $mark $*"; COUNT[$mark]=$(( ${COUNT[$mark]:-0} + 1 )); }
fpass() { line "$1" "$2" pass "${@:3}"; }
ffail() { line "$1" "$2" FAIL "${@:3}"; F_FAILED=1; }
# --- The account database: getent on the host, the seam's files under --root ---
db() {
if [ -n "$ROOT" ]; then
if [ $# -gt 1 ]; then awk -F: -v k="$2" '$1 == k' "$ROOT/etc/$1"; else cat "$ROOT/etc/$1"; fi
else
getent "$@"
fi
}
uid_of() { db passwd "$1" | cut -d: -f3; }
gid_of() { db passwd "$1" | cut -d: -f4; }
# The group ids USER belongs to, as `id -G USER` prints them: the primary gid, then each group
# whose member list names USER.
groups_of() {
local g; g="$(gid_of "$1")"
[ -n "$g" ] || return 0
{ echo "$g"; db group | awk -F: -v u="$1" '{ n = split($4, m, ","); for (i = 1; i <= n; i++) if (m[i] == u) print $3 }'; } | awk '!seen[$0]++'
}
names_of_groups() { local g; for g in "$@"; do db group | awk -F: -v g="$g" '$3 == g { print $1 }'; done; }
MOSAIC_UID="$(uid_of "$S_USER")"
[[ "$MOSAIC_UID" =~ ^[1-9][0-9]*$ ]] || refuse "stack account has no non-root uid"
[ "$(db passwd "$S_USER" | cut -d: -f6)" = "$S_HOME" ] || refuse "saved home differs from account home"
QUERY="$(tester_capture 5 4096 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query failed"
tester_facts "$QUERY" || refuse "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || refuse "target does not support --tester-socket"
tester_location "$TS_DATA" "$S_HOME" "$MOSAIC_UID" "$ROOT" observe || refuse "tester path refuses $TS_REASON"
TESTER_PATH="$TS_SOCKET"
# --- systemd, /proc and listeners ---
# KEY=VALUE lines for UNIT's properties.
unit_show() { local u="$1"; shift; systemctl show "$u" "${@/#/-p}" 2>/dev/null; }
prop() { sed -n "s/^$2=//p" <<< "$1" | tail -n 1; }
# The real, effective, saved and filesystem uids of PID, space-separated.
proc_uids() { awk '$1 == "Uid:" { print $2, $3, $4, $5 }' "$ROOT/proc/$1/status" 2>/dev/null; }
proc_label() { local l; l="$(cat "$ROOT/proc/$1/attr/current" 2>/dev/null)" || return 1; printf '%s' "${l%$'\n'}"; }
# One `ADDRESS PIDS` line per TCP listener on port $1, PIDS comma-separated.
listeners() {
ss -Hltnp 2>/dev/null | awk -v p="$1" '{
n = split($4, a, ":"); if (a[n] != p) next
addr = substr($4, 1, length($4) - length(p) - 1); pids = ""; s = $0
while (match(s, /pid=[0-9]+/)) { pids = pids (pids == "" ? "" : ",") substr(s, RSTART + 4, RLENGTH - 4); s = substr(s, RSTART + RLENGTH) }
print addr, pids
}'
}
# 0 when port $1 has a listener and every one is on 127.0.0.1, held by pid $2 alone.
loopback_by() {
local addr pids p any=0
[ -n "$2" ] && [ "$2" != 0 ] || return 1
while read -r addr pids; do
[ -n "$addr" ] || continue
any=1
[ "$addr" = 127.0.0.1 ] || return 1
for p in ${pids//,/ }; do [ "$p" = "$2" ] || return 1; done
[ -n "$pids" ] || return 1
done <<< "$(listeners "$1")"
[ "$any" = 1 ]
}
listener_text() { local t; t="$(listeners "$1" | tr '\n' ';')"; echo "${t:-none}"; }
BUS_SHOW="$(unit_show "$BUS_UNIT" ActiveState User MainPID)"
CON_SHOW="$(unit_show "$CON_UNIT" ActiveState User MainPID)"
CON_PID="$(prop "$CON_SHOW" MainPID)"
# --- F checks ---
check_f1() {
local u show state user pid uids why=""
for u in "$BUS_UNIT" "$CON_UNIT"; do
if [ "$u" = "$BUS_UNIT" ]; then show="$BUS_SHOW"; else show="$CON_SHOW"; fi
state="$(prop "$show" ActiveState)" user="$(prop "$show" User)" pid="$(prop "$show" MainPID)"
uids="$(proc_uids "${pid:-0}")"
[ "$state" = active ] || { why+="$u is ${state:-unknown}; "; continue; }
[ "$user" = "$S_USER" ] || { why+="$u has User=${user:-unset}; "; continue; }
[ -n "$MOSAIC_UID" ] && [ "$uids" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] ||
why+="$u MainPID ${pid:-none} has uids '${uids:-unreadable}', not $S_USER's ${MOSAIC_UID:-unknown}; "
done
if [ -z "$why" ]; then fpass F1 units-user "$BUS_UNIT and $CON_UNIT are active as $S_USER (uid $MOSAIC_UID)"
else ffail F1 units-user "${why%; }"; fi
}
check_f2() {
local rc=0
curl -fsS --max-time 10 -o /dev/null "http://127.0.0.1:$PORT/healthz" 2>/dev/null || rc=$?
if [ "$rc" != 0 ]; then ffail F2 console-loopback "curl http://127.0.0.1:$PORT/healthz exited $rc"
elif ! loopback_by "$PORT" "$CON_PID"; then ffail F2 console-loopback "port $PORT listeners are '$(listener_text "$PORT")', not 127.0.0.1 by the Console's MainPID ${CON_PID:-none}"
else fpass F2 console-loopback "healthz answers, and $PORT is bound to 127.0.0.1 only, by MainPID $CON_PID"; fi
}
# F5's scan: an outer /usr/bin/bwrap owned by the stack's user with a claude descendant. Sets F5_OK
# when one pair has both labels exactly, F5_WHY to the last mismatch otherwise.
F5_OK="" F5_WHY="no /usr/bin/bwrap process of $S_USER with a claude descendant was seen while the reply was pending"
scan_f5() {
local pid ppid uids kids q c argv0 comm lo lc
local -A parent=() children=() bw=()
# One read of every PPid line and one of every exe link, so a scan stays short on a busy host.
while read -r pid ppid; do
parent[$pid]="$ppid" children[$ppid]+=" $pid"
done < <(grep -H '^PPid:' "$ROOT"/proc/[0-9]*/status 2>/dev/null | sed -n 's#^.*/proc/\([0-9]*\)/status:PPid:[[:space:]]*\([0-9]*\)$#\1 \2#p')
while read -r pid; do
[[ "$pid" =~ ^[0-9]+$ ]] && [ -n "${parent[$pid]:-}" ] && bw[$pid]=1
done < <(find "$ROOT/proc" -mindepth 2 -maxdepth 2 -name exe -lname /usr/bin/bwrap -printf '%h\n' 2>/dev/null | sed 's#.*/##')
for pid in "${!bw[@]}"; do
[ -z "${bw[${parent[$pid]}]:-}" ] || continue
uids="$(proc_uids "$pid")"
[ -n "$MOSAIC_UID" ] && [ "${uids%% *}" = "$MOSAIC_UID" ] || continue
# Every descendant, breadth first.
kids=() q=("$pid")
while [ ${#q[@]} -gt 0 ]; do
for c in ${children[${q[0]}]:-}; do kids+=("$c") q+=("$c"); done
q=("${q[@]:1}")
done
for c in "${kids[@]}"; do
argv0="$(tr '\0' '\n' < "$ROOT/proc/$c/cmdline" 2>/dev/null | sed -n 1p)"
comm="$(cat "$ROOT/proc/$c/comm" 2>/dev/null)"
[[ "${argv0##*/}" == claude* ]] || [[ "$comm" == claude* ]] || continue
lo="$(proc_label "$pid")" lc="$(proc_label "$c")"
if [ "$lo" = "$LABEL_OUTER" ] && [ "$lc" = "$LABEL_CHILD" ]; then
F5_OK="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc'"; return 0
fi
F5_WHY="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc', not '$LABEL_OUTER' and '$LABEL_CHILD'"
done
done
return 1
}
# F3 and F5 together: the message goes out in the background and the scan runs until it is answered.
check_f3_f5() {
local marker text rc=0 out err n tpid
marker="case1-accept-$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"
text="$marker: this is the Case 1 acceptance; reply with one short line."
timeout 330 "${AS_USER[@]}" "$COPY/scripts/mosaic" talk chief "$text" --business "$BUSINESS" --tester --wait 300 > "$WORK/talk.out" 2> "$WORK/talk.err" &
tpid=$!
while :; do
scan_f5 && break
kill -0 "$tpid" 2>/dev/null || break
sleep 0.5
done
wait "$tpid" || rc=$?
# Both the sent line and the reply are on stdout; an error is on stderr.
out="$(cat "$WORK/talk.out")" err="$(cat "$WORK/talk.err")"
n="$(grep -c . <<< "$out")"
if [ "$rc" = 0 ] && grep -q "^sent [^ ]* to chief" <<< "$out" && [ "$n" -ge 2 ]; then
fpass F3 tester-reply "chief replied to $marker"
else
ffail F3 tester-reply "talk exited $rc with $n lines on stdout; last: $(clean "$(tail -n 1 <<< "${err:-$out}")")"
fi
if [ -n "$F5_OK" ]; then F5_LINE=(fpass "$F5_OK"); else F5_LINE=(ffail "$F5_WHY"); fi
}
check_f4() {
local rc=0 out
out="$("${AS_USER[@]}" "$COPY/scripts/case2-precheck.sh" --business "$BUSINESS" --profile factory 2>&1)" || rc=$?
if [ "$rc" = 0 ]; then fpass F4 precheck "case2-precheck.sh --profile factory exits 0"
else ffail F4 precheck "case2-precheck.sh --profile factory exited $rc; last: $(clean "$(tail -n 1 <<< "$out")")"; fi
}
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
}
# The PM check confirm runs (stack-release.sh pm_external): exactly one live launches list entry for
# launch.by, for this business. The stack's user runs the whole check and prints one result line.
F7_JS='
const {readFileSync}=require("node:fs");const {spawnSync}=require("node:child_process");
const [file,cli,b]=process.argv.slice(1);
let by;
try{by=JSON.parse(readFileSync(file,"utf8")).launch?.by}catch(e){console.log(`fail cannot read ${file}: ${e.code??"not JSON"}`);process.exit(0)}
if(typeof by!=="string"||!by){console.log("fail the business file has no launch.by");process.exit(0)}
const r=spawnSync(cli,["launches","list","--json"],{encoding:"utf8"});
if(r.status!==0){console.log(`fail launches list exited ${r.status}`);process.exit(0)}
let l;try{l=JSON.parse(r.stdout)}catch{console.log("fail launches list printed no JSON");process.exit(0)}
const live=Array.isArray(l)?l.filter((x)=>x&&x.instance===by&&x.live===true):[];
console.log(live.length!==1?`fail ${live.length} live launches of ${by}`:live[0].business!==b?`fail the live launch of ${by} is for ${live[0].business}`:`ok ${by} run ${live[0].run}`);'
check_f7() {
local out
out="$("${AS_USER[@]}" "$NODE" -e "$F7_JS" -- "$BUSINESS_FILE" "$COPY/scripts/mosaic" "$BUSINESS" 2>/dev/null | sed -n 1p)"
if [[ "$out" =~ ^ok\ [a-z0-9][a-z0-9-]*\ run\ [A-Za-z0-9._-]+$ ]]; then fpass F7 pm-live "one live launch: ${out#ok }"
else ffail F7 pm-live "$(clean "${out#fail }")"; fi
}
check_f8() {
local s
s="$(stat -c '%U %a %F' -- "$ROOT$CRED" 2>/dev/null)" || s="missing"
if [ ! -L "$ROOT$CRED" ] && [ "$s" = "$S_USER 600 regular file" ]; then fpass F8 credential-file "$CRED is '$s'"
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 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
}
check_f10() {
local want="/opt/mosaic-stack/node-$NODE_PIN/bin/node" v
[ "$NODE" = "$want" ] || { ffail F10 node-pin "--node $NODE is not $want"; return; }
v="$("${AS_USER[@]}" "$NODE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$NODE_PIN" ]; then fpass F10 node-pin "$NODE prints $v"
else ffail F10 node-pin "$NODE --version prints '$(clean "$v")', not $NODE_PIN"; fi
}
check_f11() {
local d="$ROOT$DATA_ROOT/discord" found
if [ -L "$d" ]; then ffail F11 no-discord "$DATA_ROOT/discord is a symlink"; return; fi
[ -d "$d" ] || { fpass F11 no-discord "$DATA_ROOT/discord is absent"; return; }
found="$(find "$d" -maxdepth 1 -name '*.json' -printf '%f ' 2>/dev/null)"
if [ -z "$found" ]; then fpass F11 no-discord "$DATA_ROOT/discord holds no binding"
else ffail F11 no-discord "$DATA_ROOT/discord holds bindings: ${found% }"; fi
}
# --- P checks ---
check_p1() {
local out
out="$(sudo -l -U "$S_USER" 2>&1)"
if grep -q "is not allowed to run sudo" <<< "$out"; then line P1 "$S_USER-sudo" closed "sudo -l: $(clean "$(sed -n 1p <<< "$out")")"
else line P1 "$S_USER-sudo" unnamed "sudo -l: $(clean "$(tr '\n' ' ' <<< "$out")")"; fi
}
check_p2() {
local gs keys="$ROOT$S_HOME/.ssh/authorized_keys" kstate n
gs="$(groups_of "$S_USER" | tr '\n' ' ')"; gs="${gs% }"
n="$(wc -w <<< "$gs")"
if [ ! -e "$keys" ] && [ ! -L "$keys" ]; then kstate=absent
elif [ -f "$keys" ] && [ ! -L "$keys" ] && [ ! -s "$keys" ]; then kstate=empty
else kstate=present; fi
if [ "$n" = 1 ] && [ "$kstate" != present ]; then line P2 "$S_USER-groups-keys" closed "id -G is '$gs'; authorized_keys is $kstate"
else line P2 "$S_USER-groups-keys" unnamed "id -G is '$gs'; authorized_keys is $kstate"; fi
}
check_p3() {
local n
n="$(sudo -l -U localadmin 2>&1 | grep -c NOPASSWD)"
line P3 localadmin-sudo "accepted Q183" "sudo -l -U localadmin lists $n NOPASSWD lines"
}
check_p4() { line P4 pve-root-pbs "accepted Q182+Q183" "root on pve over VM 1008, and PBS backups that hold $S_HOME with the credential; this script can't see pve"; }
check_p5() {
local gs reach
if [ -z "$(uid_of jarvis)" ]; then line P5 jarvis-reach closed "no jarvis account"; return; fi
gs="$(names_of_groups $(groups_of jarvis) | tr '\n' ' ')"
reach="$(tr ' ' '\n' <<< "$gs" | grep -xE 'sudo|docker|lxd' | tr '\n' ' ')"
if [ -z "$reach" ]; then line P5 jarvis-reach closed "jarvis is in none of sudo, docker and lxd (groups: ${gs% })"
else line P5 jarvis-reach unnamed "jarvis is in ${reach% }"; fi
}
check_p6() {
local g m members bad=0
for g in sudo docker lxd; do
members="$(db group "$g" | cut -d: -f4)"
for m in ${members//,/ }; do
[ "$g" = sudo ] && [ "$m" = localadmin ] && continue
line P6 "group-$g" unnamed "$m is a member of $g"; bad=1
done
done
[ "$bad" = 1 ] || line P6 group-members closed "sudo holds only localadmin, and docker and lxd have no members"
}
check_p7() {
local found
found="$(grep -rlw -- "$S_USER" "$ROOT/etc/sudoers.d" 2>/dev/null | sed "s#^$ROOT##" | tr '\n' ' ')"
if [ -z "$found" ]; then line P7 sudoers-d closed "no file under /etc/sudoers.d names $S_USER"
else line P7 sudoers-d unnamed "files naming $S_USER: ${found% }"; fi
}
check_p8() {
local other
other="$(listeners 7331)$(listeners 7333)"
if loopback_by "$PORT" "$CON_PID" && [ -z "$other" ]; then
line P8 lan-listeners closed "$PORT is on 127.0.0.1 only by MainPID $CON_PID; no TCP listener on 7331 or 7333; tester uses $TESTER_PATH"
else
line P8 lan-listeners unnamed "$PORT: $(listener_text "$PORT"); 7333: $(listener_text 7333); 7331: $(listener_text 7331); Console MainPID ${CON_PID:-none}"
fi
}
check_p9() {
local out n missing=() rc=0 err mg others
out="$("$ROOT$HOSTCHECK" --check 2>/dev/null)"
for n in 1 2 3 4 5 6; do grep -q "^ok $n " <<< "$out" || missing+=("hostcheck item $n has no ok line")
done
err="$(sudo -u jarvis /usr/bin/bwrap --version 2>&1 >/dev/null)" || rc=$?
[ "$rc" != 0 ] && grep -q "Permission denied" <<< "$err" || missing+=("sudo -u jarvis /usr/bin/bwrap --version exited $rc without Permission denied")
mg="$(gid_of "$S_USER")"
[ -n "$mg" ] && [ -z "$(db group "$S_USER" | cut -d: -f4)" ] || missing+=("group $S_USER lists members, or $S_USER has no group")
others="$(db passwd | awk -F: -v g="$mg" -v u="$S_USER" '$4 == g && $1 != u { print $1 }' | tr '\n' ' ')"
[ -z "$others" ] || missing+=("other accounts have $S_USER's gid $mg: ${others% }")
if [ ${#missing[@]} = 0 ]; then
line P9 bwrap-userns "accepted mos-1096" "hostcheck items 1-6 ok, jarvis is denied bwrap, and group $S_USER has no other member"
else
for n in "${missing[@]}"; do line P9 bwrap-userns unnamed "$n"; done
fi
}
# P10 is an observation of mutable same-uid state, never a closure ruling.
TESTER_PEER='import socket,struct,sys
s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
try:
s.settimeout(1)
s.connect(sys.argv[1])
p=struct.unpack("3i",s.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
print(*p)
finally:
s.close()'
tester_snapshot() {
local parent="${TESTER_PATH%/*}" p s pid
[ ! -L "$ROOT$parent" ] && [ ! -L "$ROOT$TESTER_PATH" ] || return 1
p="$(stat -c '%u %a %F %d %i' -- "$ROOT$parent" 2>/dev/null)" || return 1
s="$(stat -c '%u %a %F %d %i' -- "$ROOT$TESTER_PATH" 2>/dev/null)" || return 1
[[ "$p" =~ ^$MOSAIC_UID\ 700\ directory\ [0-9]+\ [0-9]+$ ]] || return 1
[[ "$s" =~ ^$MOSAIC_UID\ 600\ socket\ [0-9]+\ [0-9]+$ ]] || return 1
pid="$(prop "$(unit_show "$CON_UNIT" MainPID)" MainPID)"
[[ "$pid" =~ ^[1-9][0-9]*$ ]] && [ "$pid" = "$CON_PID" ] || return 1
[ "$(proc_uids "$pid")" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] || return 1
printf '%s|%s|%s' "$pid" "$p" "$s"
}
tester_python() {
local real bad magic
# Resolve the system entry, then validate the target and every ancestor.
[ -f "$ROOT/usr/bin/python3" ] && [ -x "$ROOT/usr/bin/python3" ] || return 1
real="$(realpath -e -- "$ROOT/usr/bin/python3" 2>/dev/null)" || return 1
[ -z "$ROOT" ] || real="${real#"$ROOT"}"
case "$real" in /home/*|/root/*|/run/user/*|"$S_HOME"/*) return 1 ;; esac
bad="$(not_root_only "$real")"; [ -z "$bad" ] || return 1
bad="$(not_root_only /usr/bin)"; [ -z "$bad" ] || return 1
magic="$(od -An -tx1 -N4 -- "$ROOT$real" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
printf '%s' "$real"
}
check_p10() {
local floor before after python peer pid uid gid why="" pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})$'
floor="$(cat "$ROOT/proc/sys/net/ipv4/ip_unprivileged_port_start" 2>/dev/null)"
[[ "$floor" =~ ^[0-9]{1,5}$ ]] && [ "$floor" -gt "$PORT" ] || why=floor
before="$(tester_snapshot)" || why=metadata
python="$(tester_python)" || why=interpreter
if [ -z "$why" ]; then
peer="$(tester_capture 3 128 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why=probe
if [[ "$peer" =~ $pattern ]]; then
pid="${BASH_REMATCH[1]}" uid="${BASH_REMATCH[2]}" gid="${BASH_REMATCH[3]}"
[ "$pid" = "$CON_PID" ] && [ "$uid" = "$MOSAIC_UID" ] && [ "$gid" -le 2147483647 ] || why=peer
else why=probe; fi
after="$(tester_snapshot)" || why=metadata
[ "$before" = "$after" ] || why=changed
fi
if [ -z "$why" ]; then
line P10 tester-socket-squat unnamed "boundary=observed path=$TESTER_PATH parent=0700 owner=$MOSAIC_UID socket=0600 peer_pid=$pid peer_uid=$uid floor=$floor port=$PORT; hostile same-uid substitution remains"
else
line P10 tester-socket-squat unnamed "boundary=failed reason=$why path=$TESTER_PATH MainPID=${CON_PID:-none}; hostile same-uid substitution remains"
fi
}
check_p11() {
local en before
en="$(systemctl is-enabled mosaic-hostcheck.service 2>/dev/null)"
before="$(prop "$(unit_show mosaic-hostcheck.service Before)" Before)"
if [ "$en" = enabled ] && grep -qxF -- "$BUS_UNIT" <<< "${before// /$'\n'}"; then
line P11 hostcheck-before-bus closed "mosaic-hostcheck.service is enabled, and its Before= names $BUS_UNIT"
else
line P11 hostcheck-before-bus unnamed "mosaic-hostcheck.service is '${en:-unknown}', Before='$before'; it needs enabled and $BUS_UNIT"
fi
}
check_f1
check_f2
F5_LINE=()
check_f3_f5
check_f4
"${F5_LINE[0]}" F5 bwrap-label "${F5_LINE[@]:1}"
check_f6
check_f7
check_f8
check_f9
check_f10
check_f11
check_p1; check_p2; check_p3; check_p4; check_p5; check_p6
check_p7; check_p8; check_p9; check_p10; check_p11
for m in "${MARKS[@]}"; do echo "count $m ${COUNT[$m]:-0}"; done
if [ "$F_FAILED" = 0 ]; then echo "case1-accept: every F check passes; send the whole output to Duckworth, and each unnamed line to Mos"; exit 0; fi
echo "case1-accept: an F check failed, so the row fails"
exit 1
@@ -0,0 +1,717 @@
// Row 195 (#1693): scripts/case1-accept.sh, root's acceptance run on the Case 1 host. Design r7 section 5
// (agents/darkwing/work/queue-195/DESIGN.md); deviations in agents/darkwing/work/queue-195/BUILD.md.
// Every case runs the script with --root on a scratch host tree: /etc, /proc, /srv/mosaic, /opt and the
// host check live under it. Fake systemctl, curl, ss, sudo and stat on PATH record their argv; the fake
// sudo runs the stack's user's commands from the tree. Nothing here reads or writes the real /etc,
// /proc or /srv, and the stand-in credential is not a credential.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
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";
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const SCRIPT = join(REPO, "scripts", "case1-accept.sh");
const COPY = "0123456789abcdef0123456789abcdef01234567";
const NODE_PIN = "v24.1.0", CLAUDE_PIN = "2.1.0";
const NODE = `/opt/mosaic-stack/node-${NODE_PIN}/bin/node`;
const CLAUDE = `/opt/mosaic-stack/claude-${CLAUDE_PIN}/node_modules/@anthropic-ai/claude-code/bin/claude.exe`;
const HOME = "/srv/mosaic";
const CLI = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/mosaic`;
const PRECHECK = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/case2-precheck.sh`;
const CRED = `${HOME}/.config/mosaic-dev/secrets/claude-session.env`;
const CRED_VALUE = "stand-in-not-a-credential-7f3e2a";
const AS_MOSAIC = `-u mosaic -H env PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin`;
const ARGS = ["--profile", "factory", "--user", "mosaic", "--home", HOME, "--business", "mosaic-stack", "--copy", COPY,
"--node", NODE, "--claude", CLAUDE, "--pm", "--", "--port", "732", "--business", "mosaic-stack", "--tester-socket"];
const OUTER = "bwrap (enforce)", CHILD = "bwrap//&unpriv_bwrap (enforce)";
const MARKS = ["pass", "FAIL", "closed", "accepted Q182+Q183", "accepted Q183", "accepted mos-1096", "unnamed"];
const P_PASS = { P1: "closed", P2: "closed", P3: "accepted Q183", P4: "accepted Q182+Q183", P5: "closed", P6: "closed",
P7: "closed", P8: "closed", P9: "accepted mos-1096", P10: "unnamed", P11: "closed" };
const PASSWD = "root:x:0:0:root:/root:/bin/bash\nmosaic:x:997:997::/srv/mosaic:/usr/sbin/nologin\nlocaladmin:x:1000:1000::/home/localadmin:/bin/bash\njarvis:x:1001:1001::/home/jarvis:/bin/bash\n";
const GROUP = "root:x:0:\nsudo:x:27:localadmin\nmosaic:x:997:\nlxd:x:998:\ndocker:x:999:\nlocaladmin:x:1000:\njarvis:x:1001:\n";
const SS = "LISTEN 0 511 127.0.0.1:732 0.0.0.0:* users:((\"node\",pid=200,fd=21))\n"
+ "LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:((\"sshd\",pid=900,fd=3))\n";
const HOSTCHECK_OK = [1, 2, 3, 4, 5, 6, 7].map((n) => `ok ${n} item ${n} holds`).join("\n") + "\nnote bubblewrap 0.9.0 is the install pin\n";
function put(file, text, mode) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, text);
if (mode) chmodSync(file, mode);
}
const edit = (file, f) => writeFileSync(file, f(readFileSync(file, "utf8")));
const log = (name) => `printf '%s' ${JSON.stringify(name)} >> "$HC/calls.log"; printf ' %s' "$@" >> "$HC/calls.log"; echo >> "$HC/calls.log"`;
// Fakes on PATH. Each records its argv and answers from a file in the state directory.
const FAKES = {
systemctl: `case "$1" in
show) [ -f "$HC/show-$2" ] && cat "$HC/show-$2" ;;
is-enabled) cat "$HC/enabled-$2" 2>/dev/null || { echo disabled; exit 1; } ;;
esac`,
curl: `exit "$(cat "$HC/curl-rc")"`,
ss: `cat "$HC/ss"`,
od: `if [ "$1" = -An ] && [ "$2" = -tx1 ] && [ "$3" = -N4 ]; then
[ -f "$HC/elf" ] && cat "$HC/elf" || echo 7f454c46
else exec /usr/bin/od "$@"; fi`,
// The credential and the settings file answer from the state directory, as root would see them, and
// only those calls are recorded; everything else is the real stat, which reads the args file.
stat: `case "\${@: -1}" in
*claude-session.env) ${log("stat")}; cat "$HC/stat-cred" ;;
*console/tester.sock) cat "$HC/stat-socket" ;;
*console) [ "$2" = '%u %a %F %d %i' ] && cat "$HC/stat-parent" || echo "997 700" ;;
*managed-settings.json) ${log("stat")}; cat "$HC/stat-settings" ;;
*) exec /usr/bin/stat "$@" ;;
esac`,
// -l -U USER prints the state file; -u jarvis is the bwrap probe; -u mosaic -H env PATH=... CMD runs
// CMD from the tree, with the PATH entries and each absolute argument that exists there moved under it.
sudo: `${CASE_SUDO_SESSION}
case "$1" in
-l) [ -f "$HC/sudo-l-$3" ] && cat "$HC/sudo-l-$3"; exit 0 ;;
-u) ;;
*) exit 97 ;;
esac
user="$2"; shift 2
if [ "$user" = jarvis ]; then cat "$HC/jarvis-bwrap" >&2; exit "$(cat "$HC/jarvis-bwrap-rc")"; fi
[ "$user" = mosaic ] && [ "$1" = -H ] && [ "$2" = env ] || exit 98
shift 2
if [ "$1" = -i ]; then
[ "$2" = HOME=/srv/mosaic ] || exit 98
shift 2
fi
[[ "$1" == PATH=* ]] || exit 98
p="\${1#PATH=}"; shift
np=""; IFS=: read -ra parts <<< "$p"; for x in "\${parts[@]}"; do np+="$ROOT_DIR$x:"; done
cmd="$1"; shift
args=(); for a in "$@"; do if [[ "$a" == /* ]] && [ -e "$ROOT_DIR$a" ]; then args+=("$ROOT_DIR$a"); else args+=("$a"); fi; done
export HOME="$ROOT_DIR/srv/mosaic" PATH="$np$PATH"
[ ! -e "$ROOT_DIR$cmd" ] || cmd="$ROOT_DIR$cmd"
case_sudo_run "$HC" "$cmd" "\${args[@]}"`,
};
// 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" "$@"`;
// The real Python payload runs against a mock socket object, never an OS listener.
const PYTHON_STUB = `[ -f "$HC/peer-output" ] && { cat "$HC/peer-output"; exit "$(cat "$HC/peer-status")"; }
[ -f "$HC/peer-hang" ] && {
here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here")
echo "$here" > "$HC/peer-pid"; printf '%s %s\\n' "$here" "$sid" >> "$HC/query-pids"
trap '' TERM HUP; exec sleep 60
}
if [ -f "$HC/peer-change" ]; then cat "$HC/peer-change" > "$HC/stat-socket"; fi
if [ -f "$HC/peer-pid-change" ]; then cat "$HC/peer-pid-change" > "$HC/show-mosaic-webui.service"; fi
exec /usr/bin/python3 -I -S -c '
import os,socket,struct,sys
class Fake:
def __init__(self,*a): assert a==(socket.AF_UNIX,socket.SOCK_STREAM)
def settimeout(self,n): assert n==1
def connect(self,p): assert p.endswith("/console/tester.sock")
def getsockopt(self,*a):
assert a==(socket.SOL_SOCKET,socket.SO_PEERCRED,12)
return struct.pack("3i",*[int(x) for x in open(os.environ["HC"]+"/peer").read().split()])
def close(self): pass
socket.socket=Fake
payload=sys.argv[4]
sys.argv=["probe",sys.argv[5]]
exec(payload)
' "$@"`;
const TREE = {
"/usr/bin/python3": PYTHON_STUB,
[NODE]: NODE_STUB,
[CLAUDE]: `[ "\${1:-}" = --version ] && cat "$HC/claude-version"; exit 0`,
[CLI]: `${log("mosaic")}
case "$1" in
talk) case "$(cat "$HC/talk-mode")" in
reply) echo "sent ab12cd34 to chief"; echo "[chief -> tester] seen" ;;
noreply) echo "sent ab12cd34 to chief"; echo "no reply from chief within 300 s" >&2; exit 1 ;;
sentonly) echo "sent ab12cd34 to chief" ;;
esac ;;
launches) cat "$HC/launches.json" ;;
*) exit 2 ;;
esac`,
[PRECHECK]: `${log("precheck")}
echo "precheck: done"; exit "$(cat "$HC/precheck-rc")"`,
"/usr/local/lib/mosaic-stack/hostcheck.sh": `${log("hostcheck")}
cat "$HC/hostcheck-out"`,
};
function proc(root, pid, { ppid, uid = 997, exe, argv0 = "x", comm = "x", label }) {
const d = join(root, "proc", String(pid));
put(join(d, "status"), `Name:\t${comm}\nState:\tS (sleeping)\nPPid:\t${ppid}\nUid:\t${uid}\t${uid}\t${uid}\t${uid}\n`);
put(join(d, "cmdline"), `${argv0}\0--x\0`);
put(join(d, "comm"), `${comm}\n`);
rmSync(join(d, "exe"), { force: true });
if (exe) symlinkSync(exe, join(d, "exe"));
if (label !== undefined) put(join(d, "attr", "current"), label);
}
// A scratch Case 1 host where every F check passes and every P check is closed or accepted.
function host(t) {
const dir = tmp(t, "c1accept-");
const root = join(dir, "root"), hc = join(dir, "hc"), bin = join(dir, "bin");
const args = join(root, "etc", "mosaic-stack", "dev.args");
put(args, ARGS.join("\n") + "\n", 0o644);
put(join(root, "etc", "passwd"), PASSWD);
put(join(root, "etc", "group"), GROUP);
put(join(root, "etc", "sudoers.d", "90-localadmin"), "localadmin ALL=(ALL) NOPASSWD: ALL\n");
put(join(root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: "1" } }));
put(join(root, CRED), `CLAUDE_CODE_OAUTH_TOKEN=${CRED_VALUE}\n`, 0o600);
put(join(root, HOME, ".config", "mosaic-dev", "businesses", "mosaic-stack.json"), JSON.stringify({ id: "mosaic-stack", launch: { by: "pm" } }));
put(join(root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
proc(root, 1, { ppid: 0, uid: 0, exe: "/usr/lib/systemd/systemd", comm: "systemd" });
proc(root, 100, { ppid: 1, exe: NODE, argv0: NODE, comm: "node" });
proc(root, 200, { ppid: 1, exe: NODE, argv0: NODE, comm: "node" });
proc(root, 300, { ppid: 100, exe: "/usr/bin/bwrap", argv0: "/usr/bin/bwrap", comm: "bwrap", label: `${OUTER}\n` });
proc(root, 301, { ppid: 300, exe: NODE, argv0: NODE, comm: "node", label: `${CHILD}\n` });
proc(root, 302, { ppid: 301, exe: CLAUDE, argv0: CLAUDE, comm: "claude.exe", label: `${CHILD}\n` });
// A root bwrap with a mislabelled claude child, which F5 must pass over.
proc(root, 400, { ppid: 1, uid: 0, exe: "/usr/bin/bwrap", argv0: "/usr/bin/bwrap", comm: "bwrap", label: "unconfined\n" });
proc(root, 401, { ppid: 400, uid: 0, argv0: "claude", comm: "claude", label: "unconfined\n" });
const state = {
"[email protected]": "ActiveState=active\nUser=mosaic\nMainPID=100\n",
"show-mosaic-webui.service": "ActiveState=active\nUser=mosaic\nMainPID=200\n",
"show-mosaic-hostcheck.service": "[email protected] mosaic-webui.service multi-user.target\n",
"enabled-mosaic-hostcheck.service": "enabled\n",
"curl-rc": "0", ss: SS,
"stat-parent": "997 700 directory 9 10\n", "stat-socket": "997 600 socket 9 11\n", peer: "200 997 997\n",
"stat-cred": "mosaic 600 regular file\n", "stat-settings": "root:root 644 regular file\n",
"sudo-l-mosaic": "User mosaic is not allowed to run sudo on dev-53.\n",
"sudo-l-localadmin": "User localadmin may run the following commands on dev-53:\n (ALL : ALL) ALL\n (ALL) NOPASSWD: ALL\n",
"jarvis-bwrap": "sudo: unable to execute /usr/bin/bwrap: Permission denied\n", "jarvis-bwrap-rc": "1",
"node-version": `${NODE_PIN}\n`, "claude-version": `${CLAUDE_PIN} (Claude Code)\n`,
"talk-mode": "reply", "precheck-rc": "0", "hostcheck-out": HOSTCHECK_OK,
"launches.json": JSON.stringify([
{ instance: "pm", live: true, business: "mosaic-stack", run: "r-0002" },
{ instance: "pm", live: false, business: "mosaic-stack", run: "r-0001" },
{ instance: "dev-1", live: true, business: "mosaic-stack", run: "r-0003" },
]),
};
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);
const copyRoot = join(root, HOME, '.local/share/mosaic-stack/copies', COPY);
put(join(copyRoot, 'packages/cli/src/config.mjs'), `import fs from 'node:fs';export const loadSystem=()=>({dataRoot:fs.readFileSync(${JSON.stringify(join(hc,'data-root'))},'utf8').trim()});`);
put(join(copyRoot, 'packages/webui/src/cli.mjs'), '// MOSAIC_TESTER_SOCKET_V1\n');
put(join(hc, 'data-root'), `${HOME}/.mosaic-dev\n`);
put(join(root, HOME, '.mosaic-dev/console/tester.sock'), 'stand-in socket metadata only');
chmodSync(join(root, HOME, '.mosaic-dev/console'), 0o700);
// 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 };
}
function run(h, extra = {}, argv = ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN, "--root", h.root]) {
const r = spawnSync(SCRIPT, argv, {
encoding: "utf8",
env: { ...process.env, PATH: `${h.bin}:${process.env.PATH}`, HC: h.hc, ROOT_DIR: h.root, REAL_NODE: process.execPath, TMPDIR: h.dir, ...extra },
});
const lines = r.stdout.split("\n").filter(Boolean);
// Each check line as { id, path, mark, evidence }.
const checks = lines.filter((l) => /^[FP][0-9]+ /.test(l)).map((l) => {
const [id, path, ...rest] = l.split(" ");
const tail = rest.join(" ");
const mark = MARKS.find((m) => tail === m || tail.startsWith(`${m} `));
return { id, path, mark, evidence: tail.slice(mark?.length ?? 0).trim(), line: l };
});
return { status: r.status, out: r.stdout, err: r.stderr, lines, checks };
}
const calls = (h, name) => (existsSync(join(h.hc, "calls.log")) ? readFileSync(join(h.hc, "calls.log"), "utf8") : "")
.split("\n").filter((l) => l === name || l.startsWith(`${name} `)).map((l) => l.slice(name.length + 1));
const failed = (r) => r.checks.filter((c) => c.mark === "FAIL").map((c) => c.id);
const marksOf = (r, id) => r.checks.filter((c) => c.id === id).map((c) => c.mark);
const mustRun = (h, extra) => { const r = run(h, extra); assert.ok(r.status === 0 || r.status === 1, r.out + r.err); return r; };
test('row253 socket observation uses configured root, metadata and the Console peer without sending bytes', t=>{
const h=host(t), data='/srv/mosaic/custom-data';
put(join(h.hc,'data-root'),data+'\n');
put(join(h.root,data,'console/tester.sock'),'metadata stand-in');
const r=run(h);
assert.equal(r.status,0,r.out+r.err);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=observed path=\/srv\/mosaic\/custom-data\/console\/tester.sock/);
assert.match(r.out,/peer_pid=200 peer_uid=997/);
assert.match(r.out,/hostile same-uid substitution remains/);
assert.doesNotMatch(r.out,/accepted mos-1101/);
assert.deepEqual(marksOf(r,'P8'),['closed']);
});
test('row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure',t=>{
const faults={
'parent-mode': h=>put(join(h.hc,'stat-parent'),'997 755 directory 9 10\n'),
'parent-owner': h=>put(join(h.hc,'stat-parent'),'998 700 directory 9 10\n'),
'socket-mode': h=>put(join(h.hc,'stat-socket'),'997 660 socket 9 11\n'),
'socket-owner': h=>put(join(h.hc,'stat-socket'),'998 600 socket 9 11\n'),
'socket-type': h=>put(join(h.hc,'stat-socket'),'997 600 regular file 9 11\n'),
symlink:h=>{const p=join(h.root,HOME,'.mosaic-dev/console/tester.sock');rmSync(p);symlinkSync('/absent',p);},
peer:h=>put(join(h.hc,'peer'),'200 998 997\n'),
'peer-pid':h=>put(join(h.hc,'peer'),'555 997 997\n'),
'python-mode':h=>chmodSync(join(h.root,'usr/bin/python3'),0o777),
'python-missing':h=>rmSync(join(h.root,'usr/bin/python3')),
'python-not-elf':h=>put(join(h.hc,'elf'),'23212f62\n'),
'peer-malformed':h=>{put(join(h.hc,'peer-output'),'200 997 997 extra\n');put(join(h.hc,'peer-status'),'0');},
'peer-error':h=>{put(join(h.hc,'peer-output'),'200 997 997\n');put(join(h.hc,'peer-status'),'1');},
'peer-uid-fields':h=>proc(h.root,200,{ppid:1,uid:998,exe:NODE,comm:'node'}),
'changed-inode':h=>put(join(h.hc,'peer-change'),'997 600 socket 9 99\n'),
'changed-pid':h=>put(join(h.hc,'peer-pid-change'),'ActiveState=active\nUser=mosaic\nMainPID=201\n'),
floor:h=>put(join(h.root,'proc/sys/net/ipv4/ip_unprivileged_port_start'),'700\n'),
};
for(const [name,fault] of Object.entries(faults)){
const h=host(t);fault(h);const r=run(h);
assert.equal(r.status,name==='peer-uid-fields'?1:0,`${name}: ${r.out}${r.err}`);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=failed/,name);
assert.doesNotMatch(r.out,/boundary=observed/,name);
}
});
test('row253 acceptance rejects bad config and unsupported copies before talk',t=>{
const faults={
outside:h=>put(join(h.hc,'data-root'),'/srv/mosaic2/data\n'),
dot:h=>put(join(h.hc,'data-root'),'/srv/mosaic/./data\n'),
absent:h=>rmSync(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs')),
unmarked:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/webui/src/cli.mjs'),'// old copy'),
schema:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),'console.log(JSON.stringify({dataRoot:"/srv/mosaic/.mosaic-dev",socket:true,extra:1}));process.exit(0);'),
failed:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),'console.log(JSON.stringify({dataRoot:"/srv/mosaic/.mosaic-dev",socket:true}));process.exit(1);'),
};
for(const [name,fault] of Object.entries(faults)){
const h=host(t);fault(h);const r=run(h);
assert.equal(r.status,2,`${name}: ${r.out}${r.err}`);assert.equal(r.out,'');
assert.match(r.err,/tester (config query|path refuses)|support/);
assert.deepEqual(calls(h,'mosaic'),[]);
}
});
test('row253 stalled peer observation ends within its bound without leaving a reader',async t=>{
const h=host(t);put(join(h.hc,'peer-hang'),'');
const r=run(h);assert.equal(r.status,0,r.out+r.err);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=failed reason=probe/);
const pid=Number(readFileSync(join(h.hc,'peer-pid'),'utf8'));
let state='gone';
for(let n=0;n<50;n++){
try{state=readFileSync(`/proc/${pid}/stat`,'utf8').split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`peer helper ${pid} still ${state}`);
});
test('row253 P8 refuses both retired TCP and board listeners',t=>{
for(const port of [7333,7331]){
const h=host(t);put(join(h.hc,'ss'),SS+`LISTEN 0 511 127.0.0.1:${port} 0.0.0.0:* users:(("node",pid=200,fd=22))\n`);
assert.deepEqual(marksOf(run(h),'P8'),['unnamed']);
}
});
test('row253 C4 acceptance query inner reason holds under both sudo models',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];
put(join(h.hc,'session-mode'),mode);
put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),blockingQuerySource(files[1]));
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query inner-timeout\n');
assert.ok(seconds<7,`${mode}: ${seconds}s`);assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
assert.deepEqual(await pollFixtureGone(files),[]);await stop();
}
});
test('row253 C4 acceptance query outer reason discards stdout and names residual cleanup',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-config'),'');
put(join(h.hc,'stall-output'),JSON.stringify({dataRoot:HOME+'/.mosaic-dev',socket:true}));
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query outer-timeout\n');
assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s preflight slack`);
assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'R3 session remains before fixture cleanup');
killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit R3 fixture cleanup');await stop();
}
});
test('row253 C4 acceptance query overflow is output-limit in both models',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);const capture=cleanupFixture(t,files);
put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:'${HOME}/.mosaic-dev'});`);
const r=run(h);capture();assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query output-limit\n');
assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);assert.deepEqual(await pollFixtureGone(files),[]);
}
});
for(const fault of ['inner','outer','overflow'])test(`row253 C4 P10 ${fault} reason holds under both sudo models`,async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];put(join(h.hc,'session-mode'),mode);
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files,7000);
if(fault==='inner')put(join(h.hc,'peer-hang'),'');
if(fault==='outer'){put(join(h.hc,'stall-probe'),'');put(join(h.hc,'stall-output'),'200 997 997\n');}
if(fault==='overflow'){put(join(h.hc,'peer-output'),'200 997 997\n'+'X'.repeat(256));put(join(h.hc,'peer-status'),'0');}
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,0,r.out+r.err);
const reason=fault==='inner'?'inner-timeout':fault==='outer'?'outer-timeout':'output-limit';
assert.match(r.out,new RegExp('P10 tester-socket-squat unnamed boundary=failed reason=probe-'+reason+' '));
assert.doesNotMatch(r.out,/boundary=observed/);
if(fault==='inner')assert.ok(seconds<4,`${mode}: ${seconds}s includes functional checks`);
if(fault==='outer'){
assert.ok(seconds>=4&&seconds<6,`${mode}: ${seconds}s; 2s functional-check slack`);
if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'stalled supervisor is R3 residual');
for(const file of files)killRecordedPids(file);
}
assert.deepEqual(await pollFixtureGone(files),[],fault==='outer'?'explicit R3 cleanup':mode);await stop();
}
});
// One fault per F check; each makes that check, and only that one, fail.
const F_FAULTS = {
F1: (h) => proc(h.root, 100, { ppid: 1, uid: 0, exe: NODE, comm: "node" }),
F2: (h) => put(join(h.hc, "ss"), SS.replace("127.0.0.1:732 ", "0.0.0.0:732 ")),
F3: (h) => put(join(h.hc, "talk-mode"), "noreply"),
F4: (h) => put(join(h.hc, "precheck-rc"), "1"),
F5: (h) => put(join(h.root, "proc", "302", "attr", "current"), "unconfined\n"),
F6: (h) => put(join(h.hc, "claude-version"), "2.0.9 (Claude Code)\n"),
F7: (h) => put(join(h.hc, "launches.json"), JSON.stringify([{ instance: "pm", live: false, business: "mosaic-stack", run: "r-1" }])),
F8: (h) => put(join(h.hc, "stat-cred"), "mosaic 640 regular file\n"),
F9: (h) => put(join(h.root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: "0" } })),
F10: (h) => put(join(h.hc, "node-version"), "v24.0.0\n"),
F11: (h) => put(join(h.root, HOME, ".mosaic-dev", "discord", "binding.json"), "{}"),
};
test("row 195 accept: every check passing exits 0, with each P mark and counts that match the lines", (t) => {
const h = host(t);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
assert.deepEqual(r.checks.map((c) => c.id), ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11",
"P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9", "P10", "P11"]);
for (const c of r.checks.filter((x) => x.id.startsWith("F"))) assert.equal(c.mark, "pass", c.line);
for (const [id, mark] of Object.entries(P_PASS)) assert.deepEqual(marksOf(r, id), [mark], id);
const counts = r.lines.filter((l) => l.startsWith("count ")).map((l) => l.slice(6));
assert.deepEqual(counts, MARKS.map((m) => `${m} ${r.checks.filter((c) => c.mark === m).length}`));
assert.match(r.lines.at(-1), /^case1-accept: every F check passes/);
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) => {
for (const [id, fault] of Object.entries(F_FAULTS)) {
const h = host(t);
fault(h);
const r = mustRun(h);
assert.equal(r.status, 1, `${id}: ${r.out}`);
assert.deepEqual(failed(r), [id], `${id}: ${r.out}`);
assert.equal(r.lines.at(-1), "case1-accept: an F check failed, so the row fails");
}
});
test("row 195 accept: F3 fails on exit 0 with no reply line, and its evidence takes talk's error", (t) => {
let h = host(t);
put(join(h.hc, "talk-mode"), "sentonly");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F3"]);
assert.match(r.out, /^F3 tester-reply FAIL talk exited 0 with 1 lines on stdout; last: sent ab12cd34 to chief$/m);
h = host(t);
put(join(h.hc, "talk-mode"), "noreply");
r = mustRun(h);
assert.match(r.out, /^F3 tester-reply FAIL talk exited 1 with 1 lines on stdout; last: no reply from chief within 300 s$/m);
});
test("row 195 accept: F1 fails on a unit that isn't active or isn't User=mosaic", (t) => {
for (const show of ["ActiveState=failed\nUser=mosaic\nMainPID=0\n", "ActiveState=active\nUser=root\nMainPID=100\n"]) {
const h = host(t);
put(join(h.hc, "show-mosaic-webui.service"), show);
const r = mustRun(h);
assert.equal(r.status, 1);
assert.ok(failed(r).includes("F1"), r.out);
}
});
test("row 195 accept: F2 fails when healthz doesn't answer and when another pid holds the port", (t) => {
let h = host(t);
put(join(h.hc, "curl-rc"), "7");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F2"]);
assert.match(r.out, /^F2 console-loopback FAIL curl http:\/\/127\.0\.0\.1:732\/healthz exited 7$/m);
h = host(t);
put(join(h.hc, "ss"), SS.replace("pid=200,fd=21", "pid=555,fd=4"));
r = mustRun(h);
assert.deepEqual(failed(r), ["F2"]);
});
test("row 195 accept: F5 fails on unpriv_bwrap (enforce), on complain and on a trailing space", (t) => {
for (const label of ["unpriv_bwrap (enforce)\n", "bwrap//&unpriv_bwrap (complain)\n", `${CHILD} \n`]) {
const h = host(t);
put(join(h.root, "proc", "302", "attr", "current"), label);
const r = mustRun(h);
assert.deepEqual(failed(r), ["F5"], `${JSON.stringify(label)}: ${r.out}`);
}
const h = host(t);
put(join(h.root, "proc", "300", "attr", "current"), `${OUTER} \n`);
assert.deepEqual(failed(mustRun(h)), ["F5"]);
});
test("row 195 accept: F5 fails with no bwrap of mosaic's, and passes on comm alone", (t) => {
let h = host(t);
proc(h.root, 300, { ppid: 100, uid: 1001, exe: "/usr/bin/bwrap", comm: "bwrap", label: `${OUTER}\n` });
let r = mustRun(h);
assert.deepEqual(failed(r), ["F5"]);
assert.match(r.out, /^F5 bwrap-label FAIL no \/usr\/bin\/bwrap process of mosaic/m);
h = host(t);
proc(h.root, 302, { ppid: 301, argv0: "/proc/self/fd/3", comm: "claude", label: `${CHILD}\n` });
assert.deepEqual(failed(mustRun(h)), []);
});
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);
assert.deepEqual(failed(r), ["F6"]);
h = host(t);
const other = CLAUDE.replace(`claude-${CLAUDE_PIN}`, "claude-2.0.9");
put(h.args, ARGS.map((a) => (a === CLAUDE ? other : 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"]);
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) => {
const two = [{ instance: "pm", live: true, business: "mosaic-stack", run: "a" }, { instance: "pm", live: true, business: "mosaic-stack", run: "b" }];
for (const [file, text] of [["launches.json", JSON.stringify(two)], ["launches.json", JSON.stringify([{ instance: "pm", live: true, business: "other", run: "a" }])]]) {
const h = host(t);
put(join(h.hc, file), text);
assert.deepEqual(failed(mustRun(h)), ["F7"]);
}
const h = host(t);
put(join(h.root, HOME, ".config", "mosaic-dev", "businesses", "mosaic-stack.json"), JSON.stringify({ id: "mosaic-stack" }));
const r = mustRun(h);
assert.deepEqual(failed(r), ["F7"]);
assert.match(r.out, /^F7 pm-live FAIL the business file has no launch\.by$/m);
});
test("row 195 accept: F8 fails on 0640, on another owner and on a symlink, and never prints the credential", (t) => {
for (const fault of [
(h) => put(join(h.hc, "stat-cred"), "mosaic 640 regular file\n"),
(h) => put(join(h.hc, "stat-cred"), "root 600 regular file\n"),
(h) => { const f = join(h.root, CRED); rmSync(f); put(`${f}.real`, `X=${CRED_VALUE}\n`, 0o600); symlinkSync(`${f}.real`, f); },
]) {
const h = host(t);
fault(h);
const r = mustRun(h);
assert.deepEqual(failed(r), ["F8"], r.out);
assert.ok(!(r.out + r.err).includes(CRED_VALUE));
}
});
test("row 195 accept: F9 fails on another owner, a group-writable mode and a number for the flag", (t) => {
for (const fault of [
(h) => put(join(h.hc, "stat-settings"), "root:root 664 regular file\n"),
(h) => put(join(h.hc, "stat-settings"), "mosaic:root 644 regular file\n"),
(h) => put(join(h.root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: 1 } })),
]) {
const h = host(t);
fault(h);
assert.deepEqual(failed(mustRun(h)), ["F9"]);
}
});
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"), "{}");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F11"]);
assert.match(r.out, /^F11 no-discord FAIL \/srv\/mosaic\/\.mosaic-dev\/discord holds bindings: chief\.json$/m);
h = host(t);
mkdirSync(join(h.root, HOME, ".mosaic-dev", "discord"), { recursive: true });
assert.deepEqual(failed(mustRun(h)), []);
});
// Each P check in its unnamed mark, with the line count each fault makes; none changes the exit.
const P_FAULTS = {
P1: [1, (h) => put(join(h.hc, "sudo-l-mosaic"), "User mosaic may run the following commands on dev-53:\n")],
P2: [1, (h) => put(join(h.root, HOME, ".ssh", "authorized_keys"), "ssh-ed25519 AAAA test\n")],
P5: [1, (h) => edit(join(h.root, "etc", "group"), (s) => s.replace("docker:x:999:", "docker:x:999:jarvis"))],
P6: [2, (h) => edit(join(h.root, "etc", "group"), (s) => s.replace("sudo:x:27:localadmin", "sudo:x:27:localadmin,bob").replace("lxd:x:998:", "lxd:x:998:carol"))],
P7: [1, (h) => put(join(h.root, "etc", "sudoers.d", "50-mosaic"), "mosaic ALL=(root) NOPASSWD: /usr/bin/systemctl\n")],
P8: [1, (h) => put(join(h.hc, "ss"), `${SS}LISTEN 0 511 0.0.0.0:7331 0.0.0.0:* users:(("node",pid=200,fd=23))\n`)],
P9: [3, (h) => { put(join(h.hc, "hostcheck-out"), HOSTCHECK_OK.replace("ok 2 ", "FAIL 2 ").replace("ok 5 ", "FAIL 5 ")); put(join(h.hc, "jarvis-bwrap-rc"), "0"); }],
P10: [1, (h) => put(join(h.root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "700\n")],
P11: [1, (h) => put(join(h.hc, "show-mosaic-hostcheck.service"), "[email protected] multi-user.target\n")],
};
test("row 195 accept: each P check reports unnamed on its fault, and the exit stays 0", (t) => {
for (const [id, [n, fault]] of Object.entries(P_FAULTS)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 0, `${id}: ${r.out}`);
assert.deepEqual(marksOf(r, id), Array(n).fill("unnamed"), `${id}: ${r.out}`);
}
});
test("row 195 accept: P2 is unnamed with a supplementary group, and P5 names docker", (t) => {
let h = host(t);
put(join(h.root, "etc", "group"), GROUP.replace("lxd:x:998:", "lxd:x:998:mosaic"));
let r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P2"), ["unnamed"]);
assert.match(r.out, /^P2 mosaic-groups-keys unnamed id -G is '997 998'; authorized_keys is absent$/m);
h = host(t);
P_FAULTS.P5[1](h);
r = run(h);
assert.match(r.out, /^P5 jarvis-reach unnamed jarvis is in docker$/m);
assert.match(r.out, /^P6 group-docker unnamed jarvis is a member of docker$/m);
});
test("row 195 accept: P5 is closed with no jarvis account, and P2 with an empty authorized_keys", (t) => {
const h = host(t);
put(join(h.root, "etc", "passwd"), PASSWD.replace(/^jarvis:.*\n/m, ""));
put(join(h.root, HOME, ".ssh", "authorized_keys"), "");
const r = run(h);
assert.deepEqual(marksOf(r, "P5"), ["closed"]);
assert.deepEqual(marksOf(r, "P2"), ["closed"]);
});
test("row 195 accept: P9 is unnamed once for each item missing", (t) => {
const items = {
hostcheck: (h) => put(join(h.hc, "hostcheck-out"), HOSTCHECK_OK.replace("ok 3 ", "FAIL 3 ")),
jarvis: (h) => { put(join(h.hc, "jarvis-bwrap-rc"), "0"); put(join(h.hc, "jarvis-bwrap"), ""); },
member: (h) => put(join(h.root, "etc", "group"), GROUP.replace("mosaic:x:997:", "mosaic:x:997:jarvis")),
gid: (h) => put(join(h.root, "etc", "passwd"), `${PASSWD}hermes:x:1002:997::/home/hermes:/bin/bash\n`),
};
for (const [name, fault] of Object.entries(items)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P9"), ["unnamed"], `${name}: ${r.out}`);
}
const h = host(t);
put(join(h.hc, "hostcheck-out"), "");
assert.deepEqual(marksOf(run(h), "P9"), Array(6).fill("unnamed"));
});
test("row253 accept: P10 fails its observation with another peer; P11 with the unit disabled", (t) => {
let h = host(t);
put(join(h.hc, "peer"), "555 997 997\n");
let r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P10"), ["unnamed"]);
assert.match(r.out, /P10 tester-socket-squat unnamed boundary=failed reason=peer/);
h = host(t);
put(join(h.hc, "enabled-mosaic-hostcheck.service"), "disabled\n");
r = run(h);
assert.deepEqual(marksOf(r, "P11"), ["unnamed"]);
});
test("row 195 accept: every P check unnamed together still exits 0", (t) => {
const h = host(t);
for (const [, fault] of Object.values(P_FAULTS)) fault(h);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
assert.deepEqual(failed(r), []);
for (const id of ["P1", "P2", "P5", "P6", "P7", "P8", "P9", "P10", "P11"]) assert.ok(marksOf(r, id).every((m) => m === "unnamed"), id);
assert.match(r.lines.at(-1), /^case1-accept: every F check passes/);
});
test("row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason", (t) => {
const bad = {
"group write": [(h) => chmodSync(h.args, 0o664), /dev\.args must be \S+'s with no group or other write \(it is uid \d+, mode 664\)$/],
symlink: [(h) => { put(`${h.args}.real`, ARGS.join("\n") + "\n", 0o644); rmSync(h.args); symlinkSync(`${h.args}.real`, h.args); }, /no regular file \/etc\/mosaic-stack\/dev\.args$/],
"unknown flag": [(h) => put(h.args, ["--send", ...ARGS].join("\n") + "\n", 0o644), /has --send before --, which case2-unit\.sh does not take$/],
"two copies": [(h) => put(h.args, ["--copy", COPY, ...ARGS].join("\n") + "\n", 0o644), /needs exactly one --copy before --, not 2$/],
"library profile": [(h) => put(h.args, ARGS.map((a) => (a === "factory" ? "library" : a)).join("\n") + "\n", 0o644), /has --profile 'library', not factory$/],
"another business": [(h) => put(h.args, ARGS.map((a, i) => (a === "mosaic-stack" && i < 14 ? "acme" : a)).join("\n") + "\n", 0o644), /is for business acme, not mosaic-stack$/],
"no tester socket": [(h) => put(h.args, ARGS.slice(0, -1).join("\n") + "\n", 0o644), /needs one bare --tester-socket after --$/],
"retired TCP": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\n')+'\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],
"socket value": [(h) => put(h.args, [...ARGS,'7333'].join('\n')+'\n',0o644), /unknown serve flag 7333$/],
"duplicate socket": [(h) => put(h.args, [...ARGS,'--tester-socket'].join('\n')+'\n',0o644), /needs one bare --tester-socket after --$/],
"short copy": [(h) => put(h.args, ARGS.map((a) => (a === COPY ? "abc123" : a)).join("\n") + "\n", 0o644), /lacks a plain --user, --home, --business, --node, --claude or --copy$/],
"no file": [(h) => rmSync(h.args), /no regular file \/etc\/mosaic-stack\/dev\.args$/],
};
for (const [name, [fault, why]] of Object.entries(bad)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 2, `${name}: ${r.out}${r.err}`);
assert.match(r.err.trimEnd(), /^case1-accept: refusing: /, name);
assert.match(r.err.trimEnd(), why, name);
assert.equal(r.out, "", name);
assert.ok(!existsSync(join(h.hc, "calls.log")), name);
}
});
test("row 195 accept: usage errors and a run without --root as a user exit 2", (t) => {
const h = host(t);
let r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--node-pin", NODE_PIN, "--root", h.root]);
assert.equal(r.status, 2);
assert.match(r.err, /^usage: /);
r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", "24.1.0", "--root", h.root]);
assert.equal(r.status, 2);
r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN]);
assert.equal(r.status, 2);
assert.equal(r.err, "case1-accept: refusing: run it as root\n");
assert.ok(!existsSync(join(h.hc, "calls.log")));
});
test("row 195 accept: every in-instance call goes through sudo -u mosaic -H env PATH=<node dir>:...", (t) => {
const h = host(t);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
const sudo = calls(h, "sudo");
const asMosaic = sudo.filter((a) => a.startsWith("-u mosaic"));
const isolated = asMosaic.filter(a=>a.startsWith('-u mosaic -H env -i '));
assert.equal(isolated.length, 2);
for(const a of isolated) assert.ok(a.startsWith(`-u mosaic -H env -i HOME=${HOME} PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin `),a);
for (const a of asMosaic.filter(a=>!isolated.includes(a))) assert.ok(a.startsWith(`${AS_MOSAIC} /`), a);
const cmds = asMosaic.filter(a=>!isolated.includes(a)).map((a) => a.slice(AS_MOSAIC.length + 1).split(" ")[0]);
assert.deepEqual([...new Set(cmds)].sort(), [CLI, PRECHECK, CLAUDE, NODE].sort());
assert.deepEqual(sudo.filter((a) => !a.startsWith("-u mosaic")).sort(),
["-l -U localadmin", "-l -U mosaic", "-u jarvis /usr/bin/bwrap --version"]);
const talk = calls(h, "mosaic").filter((a) => a.startsWith("talk "));
assert.equal(talk.length, 1);
assert.match(talk[0], /^talk chief case1-accept-[0-9a-f]{12}: .* --business mosaic-stack --tester --wait 300$/);
assert.deepEqual(calls(h, "precheck"), ["--business mosaic-stack --profile factory"]);
});
test("row 195 accept: the host check runs with --check and never --enforce", (t) => {
const h = host(t);
run(h);
assert.deepEqual(calls(h, "hostcheck"), ["--check"]);
});
@@ -0,0 +1,610 @@
#!/usr/bin/env bash
# Case 2 row C2-13a (design 9.3, 10 step 2c): render and install the two
# system units a hub runs as, [email protected] (enabled as
# mosaic-bus@<business>.service) and mosaic-webui.service, from the existing
# templates, which it doesn't change.
#
# case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH
# [--profile factory|library] [--pm] [--claude PATH]
# -- SERVE FLAGS
#
# Root runs it from a root-owned checkout of the reviewed commit outside
# <home>, never from the release, because <user> can write the release. It
# writes both units under /etc/systemd/system as root:root 0644, runs
# `systemctl daemon-reload`, enables both, then starts the bus and then the
# Console (Q189). Both come back after a boot on their own. It never links
# a unit file into place. Root only stats files under <home> and reads bounded
# stdout from a config/target query run as the stack user through sudo. Root
# never imports copy code. This adds sudo and GNU timeout to the installer
# dependencies. The inner query supervisor runs after the uid drop: 5 s inside,
# 7 s on root's wait. Fixed system Bash, setsid, sleep, timeout and head are
# resolved and root-validated. Only the config query uses this supervisor.
# It renders the default instance only.
#
# The library profile takes exactly these serve flags, each once:
# --port N (1..1023) --business ID (the unit's) --public-host NAME
# --auth-header HEADER --allow-user ID --send
# --proof-header NAME --proof-file PATH
# The factory profile takes --port N (1..1023) and --business ID, and an
# optional bare --tester-socket marker. Retired tester TCP refuses. Only factory takes
# --pm and --claude.
#
# On a re-install the installed units must equal the render in everything
# but the copy id, and both must name one copy, so a re-pin or rollback
# re-renders the pair together. A unit that was already running stays on its
# old copy, since `systemctl start` leaves it alone; the script says so and
# that the bus and Console must restart together, and restarts nothing.
#
# Test seams, for packages/cli/tests/case2-unit.test.mjs only:
# CASE2_UNIT_DIR the unit directory, default /etc/systemd/system;
# the root and root-owned checks apply only there
# CASE2_SYSTEMCTL the systemctl command, default systemctl
# CASE2_GETENT the getent command, default getent
# CASE2_BUS_TEMPLATE default packages/cli/systemd/mosaic-bus.service.in
# CASE2_WEBUI_TEMPLATE default packages/webui/systemd/mosaic-webui.service.in
#
# Exit 0 done, 1 refused, 4 usage.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEFAULT_UNIT_DIR=/etc/systemd/system
UNIT_DIR="${CASE2_UNIT_DIR:-$DEFAULT_UNIT_DIR}"
SYSTEMCTL="${CASE2_SYSTEMCTL:-systemctl}"
GETENT="${CASE2_GETENT:-getent}"
BUS_TEMPLATE="${CASE2_BUS_TEMPLATE:-$REPO/packages/cli/systemd/mosaic-bus.service.in}"
WEBUI_TEMPLATE="${CASE2_WEBUI_TEMPLATE:-$REPO/packages/webui/systemd/mosaic-webui.service.in}"
[email protected]
WEBUI_UNIT=mosaic-webui.service
usage() { echo 'usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS' >&2; exit 4; }
die() { echo "case2-unit: $*" >&2; exit 1; }
# A value the unit carries as itself: no space, %, $, quote, backslash, & or |.
plain() { case "$1" in *[!A-Za-z0-9/._+-]*) return 1 ;; esac; }
abs_plain() {
case "$1" in /*) ;; *) die "$2 must be an absolute path: $1" ;; esac
plain "$1" || die "$2 contains a character the unit cannot carry: $1"
case "$1/" in */./*|*/../*|?*//*) die "$2 must be a normalized path: $1" ;; esac
[ "$1" = / ] || [ "${1%/}" = "$1" ] || die "$2 must not end in /: $1"
}
PROFILE=library USER_NAME='' HOME_DIR='' BUSINESS='' COPY_ID='' NODE='' PM=0 CLAUDE='' CLAUDE_SET=0
while [ $# -gt 0 ]; do
case "$1" in
--profile|--user|--home|--business|--copy|--node|--claude)
[ $# -ge 2 ] || usage
case "$1" in
--profile) PROFILE="$2" ;; --user) USER_NAME="$2" ;; --home) HOME_DIR="$2" ;;
--business) BUSINESS="$2" ;; --copy) COPY_ID="$2" ;; --node) NODE="$2" ;;
--claude) CLAUDE="$2"; CLAUDE_SET=1 ;;
esac
shift 2 ;;
--pm) PM=1; shift ;;
--) shift; break ;;
*) usage ;;
esac
done
SERVE=("$@")
[ -n "$USER_NAME" ] && [ -n "$HOME_DIR" ] && [ -n "$BUSINESS" ] && [ -n "$COPY_ID" ] && [ -n "$NODE" ] || usage
case "$PROFILE" in factory|library) ;; *) die "--profile must be factory or library: $PROFILE" ;; esac
if [ "$PROFILE" = library ]; then
[ "$PM" = 0 ] || die "the library profile refuses --pm"
[ "$CLAUDE_SET" = 0 ] || die "the library profile refuses --claude"
fi
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
[ "$(id -u)" = 0 ] || die "writing $DEFAULT_UNIT_DIR needs root"
fi
# The user, its uid, primary group and home, from the passwd database.
[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "--user must be a user name: $USER_NAME"
PW="$("$GETENT" passwd "$USER_NAME")" || die "no user $USER_NAME in the passwd database"
IFS=: read -r _ _ USER_UID USER_GID _ PW_HOME _ <<<"$PW"
[[ "$USER_UID" =~ ^[0-9]+$ ]] && [ "$USER_UID" -ne 0 ] || die "--user must not be root: $USER_NAME"
abs_plain "$HOME_DIR" '--home'
[ "$HOME_DIR" = "$PW_HOME" ] || die "--home $HOME_DIR is not $USER_NAME's home in the passwd database ($PW_HOME)"
GR="$("$GETENT" group "$USER_GID")" || die "no group $USER_GID in the group database"
GROUP="${GR%%:*}"
[[ "$GROUP" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "the primary group of $USER_NAME is not a group name: $GROUP"
case "$BUSINESS" in [a-z0-9]*) ;; *) die "--business must be a business id: $BUSINESS" ;; esac
case "$BUSINESS" in *[!a-z0-9._-]*) die "--business must be a business id: $BUSINESS" ;; esac
[ "${#BUSINESS}" -le 64 ] || die "--business must be a business id: $BUSINESS"
# The copy: copy_root's rule (scripts/stack-copy.sh:8, :22), under <home>.
COPIES="$HOME_DIR/.local/share/mosaic-stack/copies"
[[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] || die "--copy must be a full 40-character lower-case commit id: $COPY_ID"
COPY="$COPIES/$COPY_ID"
[ -d "$COPY" ] && [ ! -L "$COPY" ] || die "no copy directory $COPY"
[ -f "$COPY/scripts/mosaic" ] || die "no copy $COPY_ID in $COPIES: $COPY/scripts/mosaic is missing"
# Owned by root and writable by no one else, the file and every directory above it.
root_only() {
local p="$1" what="$2" owner mode
while :; do
owner="$(stat -c '%u' -- "$p")" || die "cannot stat $p"
mode="$(stat -c '%a' -- "$p")" || die "cannot stat $p"
[ "$owner" = 0 ] || die "$what: $p is not owned by root"
(( (8#$mode & 8#022) == 0 )) || die "$what: $p is writable by its group or others"
[ "$p" = / ] && break
p="$(dirname -- "$p")"
done
}
# --node: an absolute path to a root-owned regular file outside <home>.
abs_plain "$NODE" '--node'
case "$NODE/" in "$HOME_DIR"/*) die "--node must be outside $HOME_DIR: $NODE" ;; esac
[ ! -L "$NODE" ] || die "--node must not be a symlink; pass its target: $NODE"
[ -f "$NODE" ] && [ -x "$NODE" ] || die "--node must be an executable regular file: $NODE"
root_only "$NODE" '--node'
# --claude follows the --node rule, so nothing running as <user> can replace
# the binary the bus runs.
if [ "$CLAUDE_SET" = 1 ]; then
abs_plain "$CLAUDE" '--claude'
case "$CLAUDE/" in "$HOME_DIR"/*) die "--claude must be outside $HOME_DIR: $CLAUDE" ;; esac
[ ! -L "$CLAUDE" ] || die "--claude must not be a symlink; pass its target: $CLAUDE"
[ -f "$CLAUDE" ] && [ -x "$CLAUDE" ] || die "--claude must be an executable regular file: $CLAUDE"
root_only "$CLAUDE" '--claude'
fi
# The checkout the templates come from, when installing for real.
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
case "$REPO/" in "$HOME_DIR"/*) die "run this from a root-owned checkout outside $HOME_DIR, not $REPO" ;; esac
root_only "$BUS_TEMPLATE" 'bus template'
root_only "$WEBUI_TEMPLATE" 'Console template'
fi
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
# row253-supervisor-begin
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
stat_fields() {
local pid="$1" raw rest
if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
[ -e "/proc/$pid/stat" ] && return 1
return 3
fi
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
[[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
}
clock_cs() {
local boot spare
read -r boot spare < /proc/uptime || return 1
[[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
}
clean_session() {
local entry pid rc live until
clock_cs || return 1
until=$((CLOCK_CS + 25))
while :; do
live=0
for entry in /proc/[0-9]*/stat; do
pid="${entry%/stat}"; pid="${pid##*/}"
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" != 3 ] || continue
[ "$rc" = 0 ] || return 1
[ "$ST_SESSION" = "$child" ] || continue
case "$ST_STATE" in Z|X) continue ;; esac
live=1
if ! kill -KILL "$pid" 2>/dev/null; then
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
fi
done
[ "$live" != 0 ] || return 0
clock_cs || return 1
[ "$CLOCK_CS" -lt "$until" ] || return 1
"$sleep_bin" 0.01
done
}
finish() {
local rc="$?"
trap - EXIT HUP INT TERM
if [ "$verified" = 1 ]; then
clean_session || rc=86
elif [ -n "$child" ]; then
kill -KILL "$child" 2>/dev/null || :
fi
[ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
"$setsid_bin" "$@" & child=$!
for ((attempt=0; attempt<100; attempt++)); do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 85
if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
tester_tools() {
local input target p st magic
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
p="$target"
while :; do
st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
[ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
[ "$p" != / ] || break
p="${p%/*}"; [ -n "$p" ] || p=/
done
magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
case "$input" in
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
/usr/bin/head) TS_HEAD="$target" ;;
esac
done
}
tester_capture_reason() {
case "$1" in
81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
*) echo inner-command ;;
esac
}
tester_capture() (
local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
tester_tools || exit 84
file="$(mktemp)" || exit 88
notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
trap 'exit 88' HUP INT TERM
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
set -o pipefail
maximum="$1"; head_bin="$2"; shift 2
"$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
# Only GNU timeout and the trusted head can write this root-side channel.
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
[ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
[ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# The serve flags, checked as scripts/webui-service.sh serve_flags checks
# them (:201-264), and kept in the order given. That script compares the
# proof file's owner to `id -u`, which is root here, so this copy has its own.
serve_flags() {
local -A seen=()
local flags='' name value
local port='' public_host='' auth_header='' allow_user='' proof_header='' proof_file='' tester_socket=0
local i=0 n=${#SERVE[@]}
while [ "$i" -lt "$n" ]; do
name="${SERVE[$i]}"
case "$name" in
--tester-port) die "case2-unit.sh retires tester TCP; use --tester-socket" ;;
--tester-socket) [ "$PROFILE" = factory ] || die "the library profile refuses --tester-socket" ;;
--checkout) die "refusing --checkout: Case 2 has no queue panel" ;;
--board) die "refusing --board: Case 2 runs no control board" ;;
--sso) die "refusing --sso: the Console signs people in through the proxy, with the preview set and the proof pair (Q175 phase 2 is a follow-up)" ;;
--port|--business) ;;
--public-host|--auth-header|--allow-user|--proof-header|--proof-file|--send)
[ "$PROFILE" = library ] || die "the factory profile refuses $name: its host has no proxy" ;;
*) die "unknown serve flag: $name" ;;
esac
[ -z "${seen[$name]:-}" ] || die "serve flag $name given twice"
seen[$name]=1
if [ "$name" = --tester-socket ]; then tester_socket=1; flags="$flags --tester-socket"; i=$((i + 1)); continue; fi
if [ "$name" = --send ]; then flags="$flags --send"; i=$((i + 1)); continue; fi
[ $((i + 1)) -lt "$n" ] || die "serve flag $name needs a value"
value="${SERVE[$((i + 1))]}"
case "$name" in
--port) port="$value"
[[ "$port" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$port" -le 1023 ] || die "--port must be 1..1023, so only the unit's capability can bind it: $port" ;;
--business) [ "$value" = "$BUSINESS" ] || die "serve --business $value is not the unit's business $BUSINESS" ;;
--public-host) public_host="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "public host must be a hostname, no port or scheme: $value" ;; esac
case "$value" in *[!A-Za-z0-9.-]*) die "public host must be a hostname, no port or scheme: $value" ;; esac ;;
--auth-header) auth_header="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "auth header must be a header name: $value" ;; esac
case "$value" in *[!A-Za-z0-9-]*) die "auth header must be a header name: $value" ;; esac ;;
--allow-user) allow_user="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "allowed user must start with a letter or digit: $value" ;; esac
case "$value" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $value" ;; esac ;;
--proof-header) proof_header="$value"
[[ "$value" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,63}$ ]] || die "invalid proxy proof header" ;;
--proof-file) proof_file="$value" ;;
esac
flags="$flags $name $value"
i=$((i + 2))
done
local required=(--port --business)
[ "$PROFILE" = factory ] || required+=(--public-host --auth-header --allow-user --send --proof-header --proof-file)
for name in "${required[@]}"; do
[ -n "${seen[$name]:-}" ] || die "serve flag $name is required"
done
if [ "$PROFILE" = factory ]; then
if [ "$tester_socket" = 1 ]; then
local query
query="$(tester_capture 5 7 4096 "$USER_NAME" "$HOME_DIR" "$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query $(tester_capture_reason "$?")"
tester_facts "$query" || die "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || die "the target does not support --tester-socket"
tester_location "$TS_DATA" "$HOME_DIR" "$USER_UID" "" || die "tester path refuses $TS_REASON"
fi
echo "${flags# }"; return 0
fi
# Lead decision 101: the uid header carries the Authentik uid.
if [ "${auth_header,,}" = x-authentik-uid ] && ! [[ "$allow_user" =~ ^[0-9a-f]{64}$ ]]; then
die "with --auth-header $auth_header the allowed user must be an Authentik uid, 64 lower-case hex characters (lead decision 101): $allow_user"
fi
case "${proof_header,,}" in host|origin|x-mosaic-tester|"${auth_header,,}") die "proxy proof header must differ from Host, Origin, tester and auth headers" ;; esac
# The proof file: metadata only, never opened (design 9.3).
local secrets="$HOME_DIR/.config/mosaic-dev/secrets" real owner mode
abs_plain "$proof_file" 'proxy proof file'
[ ! -L "$proof_file" ] || die "proxy proof file must not be a symlink: $proof_file"
[ -f "$proof_file" ] || die "proxy proof file must be a regular file: $proof_file"
real="$(realpath -e -- "$proof_file")" || die "cannot resolve proxy proof file: $proof_file"
[ "$real" = "$proof_file" ] || die "proxy proof file path must have no symlink in it: $proof_file"
case "$proof_file" in "$secrets"/*) ;; *) die "proxy proof file must be under $secrets, outside the data root: $proof_file" ;; esac
owner="$(stat -c '%u' -- "$proof_file")" || die "cannot stat proxy proof file"
mode="$(stat -c '%a' -- "$proof_file")" || die "cannot stat proxy proof file"
[ "$owner" = "$USER_UID" ] || die "proxy proof file must be owned by $USER_NAME (uid $USER_UID): $proof_file"
[ "$mode" = 600 ] || [ "$mode" = 400 ] || die "proxy proof file must be mode 0600 or 0400, not $mode: $proof_file"
echo "${flags# }"
}
# PATH: the node's directory first, then the default list without it. Unlike
# scripts/webui-service.sh:303-305 this always puts node's directory first,
# so the bus's `exec node` (scripts/mosaic) finds the same node.
NODE_DIR="$(dirname -- "$NODE")"
UNIT_PATH="$NODE_DIR"
for d in /usr/local/bin /usr/bin /bin; do [ "$d" = "$NODE_DIR" ] || UNIT_PATH="$UNIT_PATH:$d"; done
FLAGS="$(serve_flags)" || exit 1
HARDENING="ProtectSystem=strict
ReadWritePaths=$HOME_DIR
ProtectHome=yes
PrivateTmp=yes"
CAPS="AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE"
# The default instance, as scripts/instance.sh:85-107 renders it: no
# MOSAIC_CONFIG line, and the session env under %h, which becomes <home>.
SESSION_ENV='%h/.config/mosaic-dev/secrets/claude-session.env'
ENV_FILE="-$HOME_DIR/.config/mosaic-dev/secrets/claude-session.env"
PM_FLAGS=''
[ "$PM" = 0 ] || PM_FLAGS=' --pm'
CLAUDE_ENV=''
[ "$CLAUDE_SET" = 0 ] || CLAUDE_ENV="Environment=MOSAIC_CLAUDE_BIN=$CLAUDE"
# One unit from its template. kind is bus or webui.
render() {
local kind="$1" template="$2" text tokens
[ -f "$template" ] && [ ! -L "$template" ] || die "template missing or not a regular file: $template"
text="$(sed -e "s|@REPO@|$COPY|g" -e "s|@PATH@|$UNIT_PATH|g" -e "s|@NODE@|$NODE|g" \
-e "s|@PM_FLAGS@|$PM_FLAGS|g" -e "s|^@CLAUDE_ENV@\$|$CLAUDE_ENV|" \
-e '/^@CONFIG_ENV@$/d' -e "s|@SESSION_ENV@|$SESSION_ENV|g" \
-e "s|%h|$HOME_DIR|g" -- "$template")"
# Checked before @FLAGS@ goes in, since an allowed user may hold @.
tokens="$(printf '%s\n' "$text" | grep -oE '@[A-Z_]+@' || true)"
if [ "$kind" = webui ]; then
[ "$tokens" = @FLAGS@ ] || die "$template: a token other than one @FLAGS@ is left after the render: $(echo $tokens)"
text="${text//@FLAGS@/$FLAGS}"
else
[ -z "$tokens" ] || die "$template: a token is left after the render: $(echo $tokens)"
fi
if printf '%s\n' "$text" | grep -q '%h'; then die "$template: %h is left after the render"; fi
local add="User=$USER_NAME\nGroup=$GROUP\n${HARDENING//$'\n'/\\n}\n"
[ "$kind" = bus ] || add="$add${CAPS//$'\n'/\\n}\n"
# The added lines go at the end of [Service], before its trailing blank
# lines. In [Install], WantedBy=multi-user.target replaces
# WantedBy=default.target in both units, so both start at boot (Q189).
printf '%s\n' "$text" | awk -v add="$add" '
function blanks_out() { for (; blanks > 0; blanks--) print "" }
/^\[.*\]$/ {
if (sec == "[Service]") printf "%s", add
sec = $0
blanks_out(); print; next
}
$0 == "" { blanks++; next }
{ blanks_out() }
sec == "[Install]" && $0 == "WantedBy=default.target" { $0 = "WantedBy=multi-user.target" }
{ print }
END { if (sec == "[Service]") printf "%s", add; blanks_out() }'
}
# The unit's settings as section<TAB>key<TAB>value, one per line, keys
# trimmed as systemd trims them. A continued line refuses, so no setting
# hides in one.
settings() {
awk '
{ sub(/^[ \t]+/, ""); sub(/[ \t]+$/, "") }
$0 == "" || /^[#;]/ { next }
/\\$/ { print "CONTINUED"; exit }
/^\[.*\]$/ { sec = $0; next }
{
i = index($0, "="); if (i == 0) { print "NOKEY\t" $0; next }
k = substr($0, 1, i - 1); v = substr($0, i + 1)
sub(/[ \t]+$/, "", k); sub(/^[ \t]+/, "", v)
print sec "\t" k "\t" v
}'
}
# Refuses unless one unit's fixed lines hold. what names the unit in messages,
# copy is the copy directory it should run from.
check_unit() {
local kind="$1" text="$2" what="$3" copy="$4" s
s="$(printf '%s\n' "$text" | settings)"
! printf '%s\n' "$s" | grep -qx CONTINUED || die "$what: a line continues onto the next"
! printf '%s\n' "$s" | grep -q '^NOKEY' || die "$what: a line is neither a section, a comment nor a setting"
# %h in a system unit is root's home, not <user>'s.
! printf '%s\n' "$text" | grep -q '%h' || die "$what: %h is left; a system unit reads it as root's home"
local key
for key in RestrictNamespaces PrivateUsers SystemCallFilter PermissionsStartOnly; do
! printf '%s\n' "$s" | awk -F'\t' -v k="$key" '$2 == k { found = 1 } END { exit !found }' \
|| die "$what: refusing $key=, which the hub's units must not hold"
done
# An Exec*= value whose leading prefix characters hold + or ! runs with
# full privileges, whatever User= says.
local bad
bad="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^Exec/ { p = $3; sub(/[^-@:+!].*$/, "", p); if (p ~ /[+!]/) print $2 "=" $3 }')"
[ -z "$bad" ] || die "$what: refusing an Exec line that runs with full privileges: $bad"
# The fixed lines, each exactly once, in [Service].
one() {
local k="$1" v="$2" got
got="$(printf '%s\n' "$s" | awk -F'\t' -v k="$k" '$2 == k { print $1 "\t" $3 }')"
[ "$got" = "[Service] $v" ] || die "$what: $k= must be exactly once in [Service], as $k=$v"
}
none() {
! printf '%s\n' "$s" | awk -F'\t' -v k="$1" '$2 == k { found = 1 } END { exit !found }' || die "$what: refusing $1=, which this unit must not hold"
}
one User "$USER_NAME"
one Group "$GROUP"
one WorkingDirectory "$copy"
# Environment= lines: PATH, and MOSAIC_CLAUDE_BIN only with --claude.
local envs want_envs="[Service] PATH=$UNIT_PATH"
[ "$kind" = webui ] || [ -z "$CLAUDE_ENV" ] || want_envs="$want_envs
[Service] MOSAIC_CLAUDE_BIN=$CLAUDE"
envs="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "Environment" { print $1 "\t" $3 }')"
[ "$envs" = "$want_envs" ] || die "$what: the Environment= lines must be exactly: $(printf '%s' "$want_envs" | cut -f2 | tr '\n' ' ')"
one NoNewPrivileges yes
one ProtectSystem strict
one ReadWritePaths "$HOME_DIR"
one ProtectHome yes
one PrivateTmp yes
local order
order="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)$/ { printf "%s ", $2 }')"
[ "$order" = "ProtectSystem ReadWritePaths ProtectHome PrivateTmp " ] || die "$what: the hardening lines must be in the order ProtectSystem, ReadWritePaths, ProtectHome, PrivateTmp"
local wanted
wanted="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "WantedBy" { print $1 "\t" $3 }')"
[ "$wanted" = "[Install] multi-user.target" ] || die "$what: WantedBy= must be exactly once, as WantedBy=multi-user.target"
if [ "$kind" = bus ]; then
one ExecStart "$copy/scripts/mosaic bus start %i$PM_FLAGS"
one EnvironmentFile "$ENV_FILE"
none AmbientCapabilities
none CapabilityBoundingSet
else
one ExecStart "$NODE $copy/packages/webui/src/cli.mjs serve $FLAGS"
none EnvironmentFile
one AmbientCapabilities CAP_NET_BIND_SERVICE
one CapabilityBoundingSet CAP_NET_BIND_SERVICE
fi
}
BUS_TEXT="$(render bus "$BUS_TEMPLATE")" || exit 1
WEBUI_TEXT="$(render webui "$WEBUI_TEMPLATE")" || exit 1
check_unit bus "$BUS_TEXT" "rendered $BUS_UNIT" "$COPY"
check_unit webui "$WEBUI_TEXT" "rendered $WEBUI_UNIT" "$COPY"
# A re-install: each installed unit passes the same checks, both name one
# copy, and each equals the render once its copy id is the new one.
[ -d "$UNIT_DIR" ] && [ ! -L "$UNIT_DIR" ] || die "no unit directory $UNIT_DIR"
installed_ids=''
for pair in "bus:$BUS_UNIT:BUS_TEXT" "webui:$WEBUI_UNIT:WEBUI_TEXT"; do
IFS=: read -r kind name var <<<"$pair"
path="$UNIT_DIR/$name"
[ ! -L "$path" ] || die "refusing: $path is a symlink"
[ -e "$path" ] || continue
[ -f "$path" ] || die "refusing: $path is not a regular file"
text="$(cat -- "$path")"
old="$(printf '%s\n' "$text" | settings | awk -F'\t' -v c="$COPIES/" '$2 == "WorkingDirectory" && index($3, c) == 1 { print substr($3, length(c) + 1) }')"
[[ "$old" =~ ^[0-9a-f]{40}$ ]] || die "installed $path names no copy under $COPIES"
check_unit "$kind" "$text" "installed $path" "$COPIES/$old"
[ "$text" = "$(printf '%s\n' "${!var}" | sed "s|$COPY|$COPIES/$old|g")" ] \
|| die "installed $path differs from the render in more than the copy id; the node path, serve flags, --pm and --claude must equal the installed ones"
installed_ids="$installed_ids $old"
done
read -r -a ids <<<"$installed_ids"
[ "${#ids[@]}" -lt 2 ] || [ "${ids[0]}" = "${ids[1]}" ] || die "the installed units name different copies, ${ids[0]} and ${ids[1]}"
# `systemctl start` leaves a running unit on the copy it started from, so
# note which units run before the write (row 195 owns the restart).
BUS_NAME="mosaic-bus@$BUSINESS.service"
running=''
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
if "$SYSTEMCTL" is-active --quiet "$name"; then running="$running $name"; fi
done
# Write each unit to a temp file in the unit directory, then rename.
write_unit() {
local name="$1" text="$2" tmp
tmp="$(mktemp "$UNIT_DIR/.case2-unit.XXXXXX")"
printf '%s\n' "$text" >"$tmp"
chmod 0644 "$tmp"
[ "$(id -u)" != 0 ] || chown root:root "$tmp"
mv -fT -- "$tmp" "$UNIT_DIR/$name"
}
write_unit "$BUS_UNIT" "$BUS_TEXT"
write_unit "$WEBUI_UNIT" "$WEBUI_TEXT"
# Enable both, then start the bus, then the Console (design 10 step 2c).
# serve calls the bus per request, so the units carry no After= or Wants=.
"$SYSTEMCTL" daemon-reload || die "systemctl daemon-reload failed; the units are written, nothing is enabled or started"
"$SYSTEMCTL" enable "$BUS_NAME" "$WEBUI_UNIT" || die "systemctl enable $BUS_NAME $WEBUI_UNIT failed; nothing is started"
"$SYSTEMCTL" start "$BUS_NAME" || die "systemctl start $BUS_NAME failed; the Console is not started"
"$SYSTEMCTL" start "$WEBUI_UNIT" || die "systemctl start $WEBUI_UNIT failed; the bus is running"
if [ -z "$running" ]; then
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID; enabled and started $BUS_NAME, then $WEBUI_UNIT"
exit 0
fi
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID and enabled both"
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
case " $running " in
*" $name "*) echo "case2-unit: $name was already running, so it is still on ${ids[0]:+copy }${ids[0]:-the copy it started from}, not copy $COPY_ID" ;;
*) echo "case2-unit: started $name on copy $COPY_ID" ;;
esac
done
echo "case2-unit: restart $BUS_NAME and $WEBUI_UNIT together to put both on copy $COPY_ID; until then they may run different copies"
@@ -0,0 +1,484 @@
#!/usr/bin/env bash
# Case 2 row C2-13a (design 9.3, 10 step 2c): render and install the two
# system units a hub runs as, [email protected] (enabled as
# mosaic-bus@<business>.service) and mosaic-webui.service, from the existing
# templates, which it doesn't change.
#
# case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH
# [--profile factory|library] [--pm] [--claude PATH]
# -- SERVE FLAGS
#
# Root runs it from a root-owned checkout of the reviewed commit outside
# <home>, never from the release, because <user> can write the release. It
# writes both units under /etc/systemd/system as root:root 0644, runs
# `systemctl daemon-reload`, enables both, then starts the bus and then the
# Console (Q189). Both come back after a boot on their own. It never links
# a unit file into place. Root only stats files under <home> and reads bounded
# stdout from a config/target query run as the stack user through sudo. Root
# never imports copy code. This adds sudo and GNU timeout to the installer
# dependencies. It renders the default instance only.
#
# The library profile takes exactly these serve flags, each once:
# --port N (1..1023) --business ID (the unit's) --public-host NAME
# --auth-header HEADER --allow-user ID --send
# --proof-header NAME --proof-file PATH
# The factory profile takes --port N (1..1023) and --business ID, and an
# optional bare --tester-socket marker. Retired tester TCP refuses. Only factory takes
# --pm and --claude.
#
# On a re-install the installed units must equal the render in everything
# but the copy id, and both must name one copy, so a re-pin or rollback
# re-renders the pair together. A unit that was already running stays on its
# old copy, since `systemctl start` leaves it alone; the script says so and
# that the bus and Console must restart together, and restarts nothing.
#
# Test seams, for packages/cli/tests/case2-unit.test.mjs only:
# CASE2_UNIT_DIR the unit directory, default /etc/systemd/system;
# the root and root-owned checks apply only there
# CASE2_SYSTEMCTL the systemctl command, default systemctl
# CASE2_GETENT the getent command, default getent
# CASE2_BUS_TEMPLATE default packages/cli/systemd/mosaic-bus.service.in
# CASE2_WEBUI_TEMPLATE default packages/webui/systemd/mosaic-webui.service.in
#
# Exit 0 done, 1 refused, 4 usage.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEFAULT_UNIT_DIR=/etc/systemd/system
UNIT_DIR="${CASE2_UNIT_DIR:-$DEFAULT_UNIT_DIR}"
SYSTEMCTL="${CASE2_SYSTEMCTL:-systemctl}"
GETENT="${CASE2_GETENT:-getent}"
BUS_TEMPLATE="${CASE2_BUS_TEMPLATE:-$REPO/packages/cli/systemd/mosaic-bus.service.in}"
WEBUI_TEMPLATE="${CASE2_WEBUI_TEMPLATE:-$REPO/packages/webui/systemd/mosaic-webui.service.in}"
[email protected]
WEBUI_UNIT=mosaic-webui.service
usage() { echo 'usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS' >&2; exit 4; }
die() { echo "case2-unit: $*" >&2; exit 1; }
# A value the unit carries as itself: no space, %, $, quote, backslash, & or |.
plain() { case "$1" in *[!A-Za-z0-9/._+-]*) return 1 ;; esac; }
abs_plain() {
case "$1" in /*) ;; *) die "$2 must be an absolute path: $1" ;; esac
plain "$1" || die "$2 contains a character the unit cannot carry: $1"
case "$1/" in */./*|*/../*|?*//*) die "$2 must be a normalized path: $1" ;; esac
[ "$1" = / ] || [ "${1%/}" = "$1" ] || die "$2 must not end in /: $1"
}
PROFILE=library USER_NAME='' HOME_DIR='' BUSINESS='' COPY_ID='' NODE='' PM=0 CLAUDE='' CLAUDE_SET=0
while [ $# -gt 0 ]; do
case "$1" in
--profile|--user|--home|--business|--copy|--node|--claude)
[ $# -ge 2 ] || usage
case "$1" in
--profile) PROFILE="$2" ;; --user) USER_NAME="$2" ;; --home) HOME_DIR="$2" ;;
--business) BUSINESS="$2" ;; --copy) COPY_ID="$2" ;; --node) NODE="$2" ;;
--claude) CLAUDE="$2"; CLAUDE_SET=1 ;;
esac
shift 2 ;;
--pm) PM=1; shift ;;
--) shift; break ;;
*) usage ;;
esac
done
SERVE=("$@")
[ -n "$USER_NAME" ] && [ -n "$HOME_DIR" ] && [ -n "$BUSINESS" ] && [ -n "$COPY_ID" ] && [ -n "$NODE" ] || usage
case "$PROFILE" in factory|library) ;; *) die "--profile must be factory or library: $PROFILE" ;; esac
if [ "$PROFILE" = library ]; then
[ "$PM" = 0 ] || die "the library profile refuses --pm"
[ "$CLAUDE_SET" = 0 ] || die "the library profile refuses --claude"
fi
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
[ "$(id -u)" = 0 ] || die "writing $DEFAULT_UNIT_DIR needs root"
fi
# The user, its uid, primary group and home, from the passwd database.
[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "--user must be a user name: $USER_NAME"
PW="$("$GETENT" passwd "$USER_NAME")" || die "no user $USER_NAME in the passwd database"
IFS=: read -r _ _ USER_UID USER_GID _ PW_HOME _ <<<"$PW"
[[ "$USER_UID" =~ ^[0-9]+$ ]] && [ "$USER_UID" -ne 0 ] || die "--user must not be root: $USER_NAME"
abs_plain "$HOME_DIR" '--home'
[ "$HOME_DIR" = "$PW_HOME" ] || die "--home $HOME_DIR is not $USER_NAME's home in the passwd database ($PW_HOME)"
GR="$("$GETENT" group "$USER_GID")" || die "no group $USER_GID in the group database"
GROUP="${GR%%:*}"
[[ "$GROUP" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "the primary group of $USER_NAME is not a group name: $GROUP"
case "$BUSINESS" in [a-z0-9]*) ;; *) die "--business must be a business id: $BUSINESS" ;; esac
case "$BUSINESS" in *[!a-z0-9._-]*) die "--business must be a business id: $BUSINESS" ;; esac
[ "${#BUSINESS}" -le 64 ] || die "--business must be a business id: $BUSINESS"
# The copy: copy_root's rule (scripts/stack-copy.sh:8, :22), under <home>.
COPIES="$HOME_DIR/.local/share/mosaic-stack/copies"
[[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] || die "--copy must be a full 40-character lower-case commit id: $COPY_ID"
COPY="$COPIES/$COPY_ID"
[ -d "$COPY" ] && [ ! -L "$COPY" ] || die "no copy directory $COPY"
[ -f "$COPY/scripts/mosaic" ] || die "no copy $COPY_ID in $COPIES: $COPY/scripts/mosaic is missing"
# Owned by root and writable by no one else, the file and every directory above it.
root_only() {
local p="$1" what="$2" owner mode
while :; do
owner="$(stat -c '%u' -- "$p")" || die "cannot stat $p"
mode="$(stat -c '%a' -- "$p")" || die "cannot stat $p"
[ "$owner" = 0 ] || die "$what: $p is not owned by root"
(( (8#$mode & 8#022) == 0 )) || die "$what: $p is writable by its group or others"
[ "$p" = / ] && break
p="$(dirname -- "$p")"
done
}
# --node: an absolute path to a root-owned regular file outside <home>.
abs_plain "$NODE" '--node'
case "$NODE/" in "$HOME_DIR"/*) die "--node must be outside $HOME_DIR: $NODE" ;; esac
[ ! -L "$NODE" ] || die "--node must not be a symlink; pass its target: $NODE"
[ -f "$NODE" ] && [ -x "$NODE" ] || die "--node must be an executable regular file: $NODE"
root_only "$NODE" '--node'
# --claude follows the --node rule, so nothing running as <user> can replace
# the binary the bus runs.
if [ "$CLAUDE_SET" = 1 ]; then
abs_plain "$CLAUDE" '--claude'
case "$CLAUDE/" in "$HOME_DIR"/*) die "--claude must be outside $HOME_DIR: $CLAUDE" ;; esac
[ ! -L "$CLAUDE" ] || die "--claude must not be a symlink; pass its target: $CLAUDE"
[ -f "$CLAUDE" ] && [ -x "$CLAUDE" ] || die "--claude must be an executable regular file: $CLAUDE"
root_only "$CLAUDE" '--claude'
fi
# The checkout the templates come from, when installing for real.
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
case "$REPO/" in "$HOME_DIR"/*) die "run this from a root-owned checkout outside $HOME_DIR, not $REPO" ;; esac
root_only "$BUS_TEMPLATE" 'bus template'
root_only "$WEBUI_TEMPLATE" 'Console template'
fi
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
tester_capture() (
local seconds="$1" maximum="$2" file child="" rc=0; shift 2
file="$(mktemp)" || exit 1
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
trap 'exit 1' HUP INT TERM
timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
[ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# The serve flags, checked as scripts/webui-service.sh serve_flags checks
# them (:201-264), and kept in the order given. That script compares the
# proof file's owner to `id -u`, which is root here, so this copy has its own.
serve_flags() {
local -A seen=()
local flags='' name value
local port='' public_host='' auth_header='' allow_user='' proof_header='' proof_file='' tester_socket=0
local i=0 n=${#SERVE[@]}
while [ "$i" -lt "$n" ]; do
name="${SERVE[$i]}"
case "$name" in
--tester-port) die "case2-unit.sh retires tester TCP; use --tester-socket" ;;
--tester-socket) [ "$PROFILE" = factory ] || die "the library profile refuses --tester-socket" ;;
--checkout) die "refusing --checkout: Case 2 has no queue panel" ;;
--board) die "refusing --board: Case 2 runs no control board" ;;
--sso) die "refusing --sso: the Console signs people in through the proxy, with the preview set and the proof pair (Q175 phase 2 is a follow-up)" ;;
--port|--business) ;;
--public-host|--auth-header|--allow-user|--proof-header|--proof-file|--send)
[ "$PROFILE" = library ] || die "the factory profile refuses $name: its host has no proxy" ;;
*) die "unknown serve flag: $name" ;;
esac
[ -z "${seen[$name]:-}" ] || die "serve flag $name given twice"
seen[$name]=1
if [ "$name" = --tester-socket ]; then tester_socket=1; flags="$flags --tester-socket"; i=$((i + 1)); continue; fi
if [ "$name" = --send ]; then flags="$flags --send"; i=$((i + 1)); continue; fi
[ $((i + 1)) -lt "$n" ] || die "serve flag $name needs a value"
value="${SERVE[$((i + 1))]}"
case "$name" in
--port) port="$value"
[[ "$port" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$port" -le 1023 ] || die "--port must be 1..1023, so only the unit's capability can bind it: $port" ;;
--business) [ "$value" = "$BUSINESS" ] || die "serve --business $value is not the unit's business $BUSINESS" ;;
--public-host) public_host="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "public host must be a hostname, no port or scheme: $value" ;; esac
case "$value" in *[!A-Za-z0-9.-]*) die "public host must be a hostname, no port or scheme: $value" ;; esac ;;
--auth-header) auth_header="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "auth header must be a header name: $value" ;; esac
case "$value" in *[!A-Za-z0-9-]*) die "auth header must be a header name: $value" ;; esac ;;
--allow-user) allow_user="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "allowed user must start with a letter or digit: $value" ;; esac
case "$value" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $value" ;; esac ;;
--proof-header) proof_header="$value"
[[ "$value" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,63}$ ]] || die "invalid proxy proof header" ;;
--proof-file) proof_file="$value" ;;
esac
flags="$flags $name $value"
i=$((i + 2))
done
local required=(--port --business)
[ "$PROFILE" = factory ] || required+=(--public-host --auth-header --allow-user --send --proof-header --proof-file)
for name in "${required[@]}"; do
[ -n "${seen[$name]:-}" ] || die "serve flag $name is required"
done
if [ "$PROFILE" = factory ]; then
if [ "$tester_socket" = 1 ]; then
local query
query="$(tester_capture 5 4096 sudo -u "$USER_NAME" -H env -i "HOME=$HOME_DIR" "PATH=$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query failed"
tester_facts "$query" || die "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || die "the target does not support --tester-socket"
tester_location "$TS_DATA" "$HOME_DIR" "$USER_UID" "" || die "tester path refuses $TS_REASON"
fi
echo "${flags# }"; return 0
fi
# Lead decision 101: the uid header carries the Authentik uid.
if [ "${auth_header,,}" = x-authentik-uid ] && ! [[ "$allow_user" =~ ^[0-9a-f]{64}$ ]]; then
die "with --auth-header $auth_header the allowed user must be an Authentik uid, 64 lower-case hex characters (lead decision 101): $allow_user"
fi
case "${proof_header,,}" in host|origin|x-mosaic-tester|"${auth_header,,}") die "proxy proof header must differ from Host, Origin, tester and auth headers" ;; esac
# The proof file: metadata only, never opened (design 9.3).
local secrets="$HOME_DIR/.config/mosaic-dev/secrets" real owner mode
abs_plain "$proof_file" 'proxy proof file'
[ ! -L "$proof_file" ] || die "proxy proof file must not be a symlink: $proof_file"
[ -f "$proof_file" ] || die "proxy proof file must be a regular file: $proof_file"
real="$(realpath -e -- "$proof_file")" || die "cannot resolve proxy proof file: $proof_file"
[ "$real" = "$proof_file" ] || die "proxy proof file path must have no symlink in it: $proof_file"
case "$proof_file" in "$secrets"/*) ;; *) die "proxy proof file must be under $secrets, outside the data root: $proof_file" ;; esac
owner="$(stat -c '%u' -- "$proof_file")" || die "cannot stat proxy proof file"
mode="$(stat -c '%a' -- "$proof_file")" || die "cannot stat proxy proof file"
[ "$owner" = "$USER_UID" ] || die "proxy proof file must be owned by $USER_NAME (uid $USER_UID): $proof_file"
[ "$mode" = 600 ] || [ "$mode" = 400 ] || die "proxy proof file must be mode 0600 or 0400, not $mode: $proof_file"
echo "${flags# }"
}
# PATH: the node's directory first, then the default list without it. Unlike
# scripts/webui-service.sh:303-305 this always puts node's directory first,
# so the bus's `exec node` (scripts/mosaic) finds the same node.
NODE_DIR="$(dirname -- "$NODE")"
UNIT_PATH="$NODE_DIR"
for d in /usr/local/bin /usr/bin /bin; do [ "$d" = "$NODE_DIR" ] || UNIT_PATH="$UNIT_PATH:$d"; done
FLAGS="$(serve_flags)" || exit 1
HARDENING="ProtectSystem=strict
ReadWritePaths=$HOME_DIR
ProtectHome=yes
PrivateTmp=yes"
CAPS="AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE"
# The default instance, as scripts/instance.sh:85-107 renders it: no
# MOSAIC_CONFIG line, and the session env under %h, which becomes <home>.
SESSION_ENV='%h/.config/mosaic-dev/secrets/claude-session.env'
ENV_FILE="-$HOME_DIR/.config/mosaic-dev/secrets/claude-session.env"
PM_FLAGS=''
[ "$PM" = 0 ] || PM_FLAGS=' --pm'
CLAUDE_ENV=''
[ "$CLAUDE_SET" = 0 ] || CLAUDE_ENV="Environment=MOSAIC_CLAUDE_BIN=$CLAUDE"
# One unit from its template. kind is bus or webui.
render() {
local kind="$1" template="$2" text tokens
[ -f "$template" ] && [ ! -L "$template" ] || die "template missing or not a regular file: $template"
text="$(sed -e "s|@REPO@|$COPY|g" -e "s|@PATH@|$UNIT_PATH|g" -e "s|@NODE@|$NODE|g" \
-e "s|@PM_FLAGS@|$PM_FLAGS|g" -e "s|^@CLAUDE_ENV@\$|$CLAUDE_ENV|" \
-e '/^@CONFIG_ENV@$/d' -e "s|@SESSION_ENV@|$SESSION_ENV|g" \
-e "s|%h|$HOME_DIR|g" -- "$template")"
# Checked before @FLAGS@ goes in, since an allowed user may hold @.
tokens="$(printf '%s\n' "$text" | grep -oE '@[A-Z_]+@' || true)"
if [ "$kind" = webui ]; then
[ "$tokens" = @FLAGS@ ] || die "$template: a token other than one @FLAGS@ is left after the render: $(echo $tokens)"
text="${text//@FLAGS@/$FLAGS}"
else
[ -z "$tokens" ] || die "$template: a token is left after the render: $(echo $tokens)"
fi
if printf '%s\n' "$text" | grep -q '%h'; then die "$template: %h is left after the render"; fi
local add="User=$USER_NAME\nGroup=$GROUP\n${HARDENING//$'\n'/\\n}\n"
[ "$kind" = bus ] || add="$add${CAPS//$'\n'/\\n}\n"
# The added lines go at the end of [Service], before its trailing blank
# lines. In [Install], WantedBy=multi-user.target replaces
# WantedBy=default.target in both units, so both start at boot (Q189).
printf '%s\n' "$text" | awk -v add="$add" '
function blanks_out() { for (; blanks > 0; blanks--) print "" }
/^\[.*\]$/ {
if (sec == "[Service]") printf "%s", add
sec = $0
blanks_out(); print; next
}
$0 == "" { blanks++; next }
{ blanks_out() }
sec == "[Install]" && $0 == "WantedBy=default.target" { $0 = "WantedBy=multi-user.target" }
{ print }
END { if (sec == "[Service]") printf "%s", add; blanks_out() }'
}
# The unit's settings as section<TAB>key<TAB>value, one per line, keys
# trimmed as systemd trims them. A continued line refuses, so no setting
# hides in one.
settings() {
awk '
{ sub(/^[ \t]+/, ""); sub(/[ \t]+$/, "") }
$0 == "" || /^[#;]/ { next }
/\\$/ { print "CONTINUED"; exit }
/^\[.*\]$/ { sec = $0; next }
{
i = index($0, "="); if (i == 0) { print "NOKEY\t" $0; next }
k = substr($0, 1, i - 1); v = substr($0, i + 1)
sub(/[ \t]+$/, "", k); sub(/^[ \t]+/, "", v)
print sec "\t" k "\t" v
}'
}
# Refuses unless one unit's fixed lines hold. what names the unit in messages,
# copy is the copy directory it should run from.
check_unit() {
local kind="$1" text="$2" what="$3" copy="$4" s
s="$(printf '%s\n' "$text" | settings)"
! printf '%s\n' "$s" | grep -qx CONTINUED || die "$what: a line continues onto the next"
! printf '%s\n' "$s" | grep -q '^NOKEY' || die "$what: a line is neither a section, a comment nor a setting"
# %h in a system unit is root's home, not <user>'s.
! printf '%s\n' "$text" | grep -q '%h' || die "$what: %h is left; a system unit reads it as root's home"
local key
for key in RestrictNamespaces PrivateUsers SystemCallFilter PermissionsStartOnly; do
! printf '%s\n' "$s" | awk -F'\t' -v k="$key" '$2 == k { found = 1 } END { exit !found }' \
|| die "$what: refusing $key=, which the hub's units must not hold"
done
# An Exec*= value whose leading prefix characters hold + or ! runs with
# full privileges, whatever User= says.
local bad
bad="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^Exec/ { p = $3; sub(/[^-@:+!].*$/, "", p); if (p ~ /[+!]/) print $2 "=" $3 }')"
[ -z "$bad" ] || die "$what: refusing an Exec line that runs with full privileges: $bad"
# The fixed lines, each exactly once, in [Service].
one() {
local k="$1" v="$2" got
got="$(printf '%s\n' "$s" | awk -F'\t' -v k="$k" '$2 == k { print $1 "\t" $3 }')"
[ "$got" = "[Service] $v" ] || die "$what: $k= must be exactly once in [Service], as $k=$v"
}
none() {
! printf '%s\n' "$s" | awk -F'\t' -v k="$1" '$2 == k { found = 1 } END { exit !found }' || die "$what: refusing $1=, which this unit must not hold"
}
one User "$USER_NAME"
one Group "$GROUP"
one WorkingDirectory "$copy"
# Environment= lines: PATH, and MOSAIC_CLAUDE_BIN only with --claude.
local envs want_envs="[Service] PATH=$UNIT_PATH"
[ "$kind" = webui ] || [ -z "$CLAUDE_ENV" ] || want_envs="$want_envs
[Service] MOSAIC_CLAUDE_BIN=$CLAUDE"
envs="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "Environment" { print $1 "\t" $3 }')"
[ "$envs" = "$want_envs" ] || die "$what: the Environment= lines must be exactly: $(printf '%s' "$want_envs" | cut -f2 | tr '\n' ' ')"
one NoNewPrivileges yes
one ProtectSystem strict
one ReadWritePaths "$HOME_DIR"
one ProtectHome yes
one PrivateTmp yes
local order
order="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)$/ { printf "%s ", $2 }')"
[ "$order" = "ProtectSystem ReadWritePaths ProtectHome PrivateTmp " ] || die "$what: the hardening lines must be in the order ProtectSystem, ReadWritePaths, ProtectHome, PrivateTmp"
local wanted
wanted="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "WantedBy" { print $1 "\t" $3 }')"
[ "$wanted" = "[Install] multi-user.target" ] || die "$what: WantedBy= must be exactly once, as WantedBy=multi-user.target"
if [ "$kind" = bus ]; then
one ExecStart "$copy/scripts/mosaic bus start %i$PM_FLAGS"
one EnvironmentFile "$ENV_FILE"
none AmbientCapabilities
none CapabilityBoundingSet
else
one ExecStart "$NODE $copy/packages/webui/src/cli.mjs serve $FLAGS"
none EnvironmentFile
one AmbientCapabilities CAP_NET_BIND_SERVICE
one CapabilityBoundingSet CAP_NET_BIND_SERVICE
fi
}
BUS_TEXT="$(render bus "$BUS_TEMPLATE")" || exit 1
WEBUI_TEXT="$(render webui "$WEBUI_TEMPLATE")" || exit 1
check_unit bus "$BUS_TEXT" "rendered $BUS_UNIT" "$COPY"
check_unit webui "$WEBUI_TEXT" "rendered $WEBUI_UNIT" "$COPY"
# A re-install: each installed unit passes the same checks, both name one
# copy, and each equals the render once its copy id is the new one.
[ -d "$UNIT_DIR" ] && [ ! -L "$UNIT_DIR" ] || die "no unit directory $UNIT_DIR"
installed_ids=''
for pair in "bus:$BUS_UNIT:BUS_TEXT" "webui:$WEBUI_UNIT:WEBUI_TEXT"; do
IFS=: read -r kind name var <<<"$pair"
path="$UNIT_DIR/$name"
[ ! -L "$path" ] || die "refusing: $path is a symlink"
[ -e "$path" ] || continue
[ -f "$path" ] || die "refusing: $path is not a regular file"
text="$(cat -- "$path")"
old="$(printf '%s\n' "$text" | settings | awk -F'\t' -v c="$COPIES/" '$2 == "WorkingDirectory" && index($3, c) == 1 { print substr($3, length(c) + 1) }')"
[[ "$old" =~ ^[0-9a-f]{40}$ ]] || die "installed $path names no copy under $COPIES"
check_unit "$kind" "$text" "installed $path" "$COPIES/$old"
[ "$text" = "$(printf '%s\n' "${!var}" | sed "s|$COPY|$COPIES/$old|g")" ] \
|| die "installed $path differs from the render in more than the copy id; the node path, serve flags, --pm and --claude must equal the installed ones"
installed_ids="$installed_ids $old"
done
read -r -a ids <<<"$installed_ids"
[ "${#ids[@]}" -lt 2 ] || [ "${ids[0]}" = "${ids[1]}" ] || die "the installed units name different copies, ${ids[0]} and ${ids[1]}"
# `systemctl start` leaves a running unit on the copy it started from, so
# note which units run before the write (row 195 owns the restart).
BUS_NAME="mosaic-bus@$BUSINESS.service"
running=''
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
if "$SYSTEMCTL" is-active --quiet "$name"; then running="$running $name"; fi
done
# Write each unit to a temp file in the unit directory, then rename.
write_unit() {
local name="$1" text="$2" tmp
tmp="$(mktemp "$UNIT_DIR/.case2-unit.XXXXXX")"
printf '%s\n' "$text" >"$tmp"
chmod 0644 "$tmp"
[ "$(id -u)" != 0 ] || chown root:root "$tmp"
mv -fT -- "$tmp" "$UNIT_DIR/$name"
}
write_unit "$BUS_UNIT" "$BUS_TEXT"
write_unit "$WEBUI_UNIT" "$WEBUI_TEXT"
# Enable both, then start the bus, then the Console (design 10 step 2c).
# serve calls the bus per request, so the units carry no After= or Wants=.
"$SYSTEMCTL" daemon-reload || die "systemctl daemon-reload failed; the units are written, nothing is enabled or started"
"$SYSTEMCTL" enable "$BUS_NAME" "$WEBUI_UNIT" || die "systemctl enable $BUS_NAME $WEBUI_UNIT failed; nothing is started"
"$SYSTEMCTL" start "$BUS_NAME" || die "systemctl start $BUS_NAME failed; the Console is not started"
"$SYSTEMCTL" start "$WEBUI_UNIT" || die "systemctl start $WEBUI_UNIT failed; the bus is running"
if [ -z "$running" ]; then
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID; enabled and started $BUS_NAME, then $WEBUI_UNIT"
exit 0
fi
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID and enabled both"
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
case " $running " in
*" $name "*) echo "case2-unit: $name was already running, so it is still on ${ids[0]:+copy }${ids[0]:-the copy it started from}, not copy $COPY_ID" ;;
*) echo "case2-unit: started $name on copy $COPY_ID" ;;
esac
done
echo "case2-unit: restart $BUS_NAME and $WEBUI_UNIT together to put both on copy $COPY_ID; until then they may run different copies"
@@ -0,0 +1,850 @@
// Case 2 row C2-13a (#1732): scripts/case2-unit.sh renders and installs the hub's two system units,
// and scripts/case2-precheck.sh checks the host before they start. Brief: docs/plans/2026-10-20_case2-rows.md
// "C2-13a". Every case runs against a scratch unit directory, a scratch home and fake systemctl,
// getent, sudo, id, node, bwrap and claude. Nothing here reads or writes /etc/systemd,
// ~/.mosaic-dev or ~/.config/mosaic-dev, and no fixture value is a real credential.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createServer } from "node:net";
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { businessDoc, writeJson } from "../../business/tests/helpers.mjs";
import { binds, prepareRun, sandboxArgv, sandboxProblem } from "../../seat/src/sandbox.mjs";
import { tmp } from "./helpers.mjs";
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const UNIT = join(REPO, "scripts", "case2-unit.sh");
const PRECHECK = join(REPO, "scripts", "case2-precheck.sh");
const BUS_TEMPLATE = join(REPO, "packages", "cli", "systemd", "mosaic-bus.service.in");
const WEBUI_TEMPLATE = join(REPO, "packages", "webui", "systemd", "mosaic-webui.service.in");
const COPY_A = "a".repeat(40);
const COPY_B = "b".repeat(40);
const UID = process.getuid();
const GID = process.getgid();
// A made-up key value; no output may hold it.
const FIXTURE_KEY = "fixture-zai-key-3c9e51";
// A root-owned executable outside /usr/local/bin, /usr/bin and /bin, for the PATH case.
function outsideNode() {
const dir = "/usr/lib/systemd";
for (const name of readdirSync(dir).sort()) {
const p = join(dir, name);
const s = statSync(p, { throwIfNoEntry: false });
if (s?.isFile() && s.uid === 0 && (s.mode & 0o111) && (s.mode & 0o022) === 0 && realpathSync(p) === p) return p;
}
throw new Error(`no root-owned executable in ${dir}`);
}
const USR_NODE = realpathSync("/usr/bin/node");
const TRUE_BIN = realpathSync("/usr/bin/true");
function script(file, text) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, `#!/usr/bin/env bash\n${text}\n`);
chmodSync(file, 0o755);
return file;
}
// A scratch hub: a home with two copies and a proof file, a unit directory, and fakes.
function hub(t, { uid = UID } = {}) {
const root = tmp(t, "case2-unit-");
const home = join(root, "home", "mosaic");
for (const id of [COPY_A, COPY_B]) {
mkdirSync(join(home, ".local", "share", "mosaic-stack", "copies", id, "scripts"), { recursive: true });
writeFileSync(join(home, ".local", "share", "mosaic-stack", "copies", id, "scripts", "mosaic"), "");
}
const secrets = join(home, ".config", "mosaic-dev", "secrets");
mkdirSync(secrets, { recursive: true, mode: 0o700 });
const proof = join(secrets, "proxy-proof");
writeFileSync(proof, "made-up-proof\n");
chmodSync(proof, 0o600);
const units = join(root, "units");
mkdirSync(units);
const bin = join(root, "bin");
const log = join(root, "systemctl.log");
script(join(bin, "getent"), `case "$1" in
passwd) [ "$2" = mosaic ] && echo "mosaic:x:${uid}:${GID}::${home}:/bin/bash" ;;
group) [ "$2" = "${GID}" ] && echo "mosaic:x:${GID}:" ;;
*) exit 2 ;;
esac`);
// is-active answers yes for each unit named in CASE2_TEST_ACTIVE.
script(join(bin, "systemctl"), `echo "$*" >>'${log}'
if [ "$1" = is-active ]; then case " \${CASE2_TEST_ACTIVE:-} " in *" $3 "*) exit 0 ;; *) exit 3 ;; esac; fi
[ "$*" != "\${CASE2_TEST_FAIL:-}" ]`);
const dataRoot = join(home, '.mosaic-dev'), query = join(root, 'query.json'), sudoLog = join(root, 'sudo.log');
mkdirSync(dataRoot, { recursive: true });
writeJson(query, { dataRoot });
for (const copy of [COPY_A, COPY_B]) {
const src = join(home, '.local/share/mosaic-stack/copies', copy, 'packages');
mkdirSync(join(src, 'cli/src'), { recursive: true }); mkdirSync(join(src, 'webui/src'), { recursive: true });
writeFileSync(join(src, 'cli/src/config.mjs'), `import fs from 'node:fs';export const loadSystem=()=>JSON.parse(fs.readFileSync(${JSON.stringify(query)},'utf8'));`);
writeFileSync(join(src, 'webui/src/cli.mjs'), '// MOSAIC_TESTER_SOCKET_V1\n');
}
script(join(bin, 'sudo'), `${CASE_SUDO_SESSION}
echo "$*" >>'${sudoLog}'
[ "$1" = -u ] && [ "$2" = mosaic ] && [ "$3" = -H ] || exit 97
shift 3
[ "$1" = env ] && [ "$2" = -i ] && [ "$3" = 'HOME=${home}' ] || exit 98
if [ -f '${root}/query-hang' ]; then
trap '' TERM
(trap '' TERM; echo "$BASHPID" >>'${root}/query-pids'; exec sleep 60) &
echo "$BASHPID" >>'${root}/query-pids'; wait; exit
fi
if [ -f '${root}/query-orphan' ]; then
(trap '' TERM; here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here"); printf '%s %s\\n' "$here" "$sid" >'${root}/query-owned'; exec sleep 60) </dev/null >/dev/null 2>&1 &
echo "$!" >'${root}/query-pids'
cat '${root}/query-output'; exit 0
fi
if [ -f '${root}/query-output' ]; then cat '${root}/query-output'; exit "$(cat '${root}/query-status')"; fi
case_sudo_run '${root}' "$@"`);
return { root, home, secrets, proof, units, bin, log, dataRoot, query, sudoLog };
}
const libraryFlags = (h) => ["--port", "443", "--business", "acme", "--public-host", "books.example", "--auth-header", "X-Auth-User",
"--allow-user", "melanie", "--send", "--proof-header", "X-Proxy-Proof", "--proof-file", h.proof];
const factoryFlags = () => ["--port", "443", "--business", "acme", "--tester-socket"];
function unit(h, { profile = "library", node = USR_NODE, copy = COPY_A, extra = [], flags, env = {} } = {}) {
const serve = flags ?? (profile === "factory" ? factoryFlags() : libraryFlags(h));
const args = ["--user", "mosaic", "--home", h.home, "--business", "acme", "--copy", copy, "--node", node, "--profile", profile, ...extra, "--", ...serve];
const r = spawnSync(UNIT, args, {
encoding: "utf8",
timeout: 30000,
env: { PATH: `${h.bin}:${process.env.PATH}`, CASE2_UNIT_DIR: h.units, CASE2_SYSTEMCTL: join(h.bin, "systemctl"), CASE2_GETENT: join(h.bin, "getent"), ...env },
});
return { code: r.status, out: r.stdout, err: r.stderr };
}
const busFile = (h) => join(h.units, "[email protected]");
const webuiFile = (h) => join(h.units, "mosaic-webui.service");
const read = (f) => readFileSync(f, "utf8");
const lines = (text, key) => text.split("\n").filter((l) => l.startsWith(`${key}=`)).map((l) => l.slice(key.length + 1));
const copyDir = (h, id = COPY_A) => join(h.home, ".local", "share", "mosaic-stack", "copies", id);
const HARDENING = (h) => ["ProtectSystem=strict", `ReadWritePaths=${h.home}`, "ProtectHome=yes", "PrivateTmp=yes"];
test('row253 factory socket uses actual config and target-uid query without widening hardening', t=>{
const h=hub(t),custom=join(h.home,'custom');mkdirSync(custom);writeJson(h.query,{dataRoot:custom});
const r=unit(h,{profile:'factory'});assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(lines(read(webuiFile(h)),'ExecStart'),[`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${factoryFlags().join(' ')}`]);
assert.deepEqual(read(webuiFile(h)).split('\n').filter(l=>/^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)=/.test(l)),HARDENING(h));
assert.match(read(h.sudoLog),new RegExp(`^-u mosaic -H env -i HOME=${h.home} PATH=`, 'm'));
assert.equal(existsSync(join(custom,'console')),false,'installer never creates the socket parent');
});
test('row253 installer refuses unsafe location and query facts before unit writes',t=>{
const cases=[
['sibling prefix',h=>writeJson(h.query,{dataRoot:h.home+'2/data'}),/outside.*home/],
['dot',h=>writeJson(h.query,{dataRoot:h.home+'/./data'}),/normalized/],
['dotdot',h=>writeJson(h.query,{dataRoot:h.home+'/x/../data'}),/normalized/],
['double slash',h=>writeJson(h.query,{dataRoot:h.home+'//data'}),/normalized/],
['trailing slash',h=>writeJson(h.query,{dataRoot:h.home+'/data/'}),/normalized/],
['newline',h=>writeJson(h.query,{dataRoot:h.home+'/x\nP10 forged closed'}),/query/],
['non ASCII',h=>writeJson(h.query,{dataRoot:h.home+'/é'}),/query/],
['middle symlink',h=>{symlinkSync(h.dataRoot,join(h.home,'link'));writeJson(h.query,{dataRoot:join(h.home,'link','nested')});},/symlink/],
['long path',h=>writeJson(h.query,{dataRoot:join(h.home,'a'.repeat(108))}),/too.long/],
['parent file',h=>writeFileSync(join(h.dataRoot,'console'),'x',{mode:0o600}),/parent-type/],
['parent symlink',h=>symlinkSync(h.home,join(h.dataRoot,'console')),/symlink/],
['parent mode',h=>{mkdirSync(join(h.dataRoot,'console'));chmodSync(join(h.dataRoot,'console'),0o755);},/parent/],
['foreign parent',h=>{mkdirSync(join(h.dataRoot,'console'),{mode:0o700});},/parent/],
['unsupported',h=>writeFileSync(join(copyDir(h),'packages/webui/src/cli.mjs'),'// old copy'),/support/],
['schema',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true,extra:true}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['wrong type',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:'true'}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['noisy query',h=>{writeFileSync(join(h.root,'query-output'),'noise\n'+JSON.stringify({dataRoot:h.dataRoot,socket:true}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['failed valid query',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));writeFileSync(join(h.root,'query-status'),'1');},/query/],
];
for(const [name,change,why] of cases){
const h=hub(t,{uid:name==='foreign parent'?UID+4242:UID});change(h);
const r=unit(h,{profile:'factory'});refuses(r,why);
assert.equal(existsSync(busFile(h)),false,name);assert.equal(existsSync(webuiFile(h)),false,name);assert.equal(existsSync(h.log),false,name);
assert.equal((r.out+r.err).includes('P10 forged closed'),false,name);
}
});
test('row253 query timeout kills its process group and leaves no running child',async t=>{
const h=hub(t);writeFileSync(join(h.root,'query-hang'),'');
const r=unit(h,{profile:'factory'});refuses(r,/query/);
const pids=read(join(h.root,'query-pids')).trim().split('\n').map(Number);assert.ok(pids.length>=2);
for(const pid of pids){
let state='gone';
for(let n=0;n<50;n++){
try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`query descendant ${pid} still ${state}`);
}
assert.equal(existsSync(h.log),false);assert.equal(existsSync(busFile(h)),false);
});
test('row253 protected homes refuse before any filesystem access',()=>{
const source=read(UNIT), body=source.slice(source.indexOf('tester_location() {'),source.indexOf('# tester-location-end'));
for(const home of ['/home/mosaic','/root/mosaic','/run/user/997','/tmp/mosaic','/var/tmp/mosaic']){
const r=spawnSync('/bin/bash',['-c',body+'\nif tester_location "$1/data" "$1" 997 ""; then exit 99; fi; printf "%s" "$TS_REASON"','--',home],{encoding:'utf8'});
assert.equal(r.status,0);assert.equal(r.stdout,'protected-private-home');
}
});
// C4 cases run through both sudo process models. Exec shares the runner's
// session, so its session-wide check would be vacuous; exact recorded PIDs
// are checked in both modes. Hop additionally isolates the fake session.
for(const kind of ['sleep','signals','member']) test(`row253 C4 ${kind} query has exact inner reason and cleans all members`,async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),sessions=join(h.root,'session-pids'),queries=join(h.root,'query-pids'),files=[sessions,queries];
writeFileSync(join(h.root,'session-mode'),mode);
const member=join(h.root,'q ) '+String.fromCharCode(10)+'mem'),proof=join(h.root,'comm-proof');
if(kind==='member')symlinkSync('/usr/bin/sleep',member);
writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),blockingQuerySource(queries,kind,member,proof));
const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
refuses(r,/^case2-unit: tester config query inner-timeout\n$/);
assert.ok(seconds<7,`${kind}/${mode}: ${seconds}s exceeds outer bound`);
assert.deepEqual(await pollFixtureGone(files),[],mode);
if(kind==='member')assert.ok(read(proof).includes('('+member.split('/').at(-1)+') '),'member comm with newline and closing parenthesis was observed');
assert.equal(existsSync(h.log),false,mode);await stopGuard();
}
});
test('row253 C4 overflow is output-limit in both sudo modes',async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),files=[join(h.root,'session-pids')];writeFileSync(join(h.root,'session-mode'),mode);const capture=cleanupFixture(t,files);
writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:${JSON.stringify(h.dataRoot)}});`);
const r=unit(h,{profile:'factory'});capture();refuses(r,/^case2-unit: tester config query output-limit\n$/);
assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
assert.deepEqual(await pollFixtureGone(files),[]);
}
});
test('row253 C4 stalled supervisor uses the outer reason and explicit residual cleanup',async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),file=join(h.root,'session-pids'),files=[file];writeFileSync(join(h.root,'session-mode'),mode);
writeFileSync(join(h.root,'stall-config'),'');writeFileSync(join(h.root,'stall-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
refuses(r,/^case2-unit: tester config query outer-timeout\n$/);
assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s slack includes root preflight`);
assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
if(mode==='hop')assert.ok(recordedLivePids(file).length>0,'R3 residual exists before fixture cleanup');
killRecordedPids(file);assert.deepEqual(await pollFixtureGone(files),[], 'explicit fixture cleanup of R3 residual');await stopGuard();
}
});
test('row253 C4 inline supervisor is byte-identical in both host scripts',()=>{
const extract=s=>s.split('# row253-supervisor-begin\n')[1]?.split('# row253-supervisor-end')[0];
const a=extract(read(UNIT)),b=extract(read(join(REPO,'scripts/case1-accept.sh')));
assert.ok(a?.length>100);assert.equal(a,b);
});
test('row253 query reaps its group even when a child closes stdout before the query returns',async t=>{
const h=hub(t);writeFileSync(join(h.root,'query-orphan'),'');
writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
let pid;
const capture=cleanupFixture(t,[join(h.root,'query-owned')]);
const r=unit(h,{profile:'factory'});capture();
pid=Number(read(join(h.root,'query-pids')).trim());
assert.equal(r.code,0,r.out+r.err);
let state='gone';
for(let n=0;n<50;n++){
try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`query descendant ${pid} still ${state}`);
});
test('row253 config parser is strict',()=>{
const source=read(UNIT), body=source.slice(source.indexOf('tester_facts() {'),source.indexOf('tester_location() {'));
const valid=spawnSync('/bin/bash',['-c',body+'\ntester_facts "$1" && printf "%s %s" "$TS_DATA" "$TS_MARKED"','--','{"dataRoot":"/srv/mosaic/data","socket":true}'],{encoding:'utf8'});
assert.equal(valid.status,0);assert.equal(valid.stdout,'/srv/mosaic/data true');
const facts=['{"dataRoot":"/srv/mosaic/data","socket":true,"extra":1}','{"dataRoot":4,"socket":true}','{"dataRoot":"/srv/mosaic/data","socket":"true"}','{"dataRoot":"/srv/mosaic/data","socket":true}\nP10 forged closed'];
for(const fact of facts){
const r=spawnSync('/bin/bash',['-c',body+'\ntester_facts "$1"','--',fact]);
assert.notEqual(r.status,0,fact);
}
});
function refuses(r, pattern) {
assert.equal(r.code, 1, `expected a refusal, got ${r.code}: ${r.out}${r.err}`);
assert.match(r.err, pattern);
}
// A template copy with one edit, for the template cases.
function template(h, from, name, edit) {
const file = join(h.root, name);
writeFileSync(file, edit(read(from)));
return file;
}
test("a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console", (t) => {
const h = hub(t);
const r = unit(h);
assert.equal(r.code, 0, r.err);
for (const f of [busFile(h), webuiFile(h)]) assert.equal(statSync(f).mode & 0o777, 0o644);
const bus = read(busFile(h));
const webui = read(webuiFile(h));
assert.deepEqual(lines(bus, "Environment"), ["PATH=/usr/bin:/usr/local/bin:/bin"]);
assert.deepEqual(lines(bus, "ExecStart"), [`${copyDir(h)}/scripts/mosaic bus start %i`]);
assert.deepEqual(lines(bus, "EnvironmentFile"), [`-${h.home}/.config/mosaic-dev/secrets/claude-session.env`]);
assert.deepEqual(lines(webui, "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${libraryFlags(h).join(" ")}`]);
assert.deepEqual(lines(webui, "EnvironmentFile"), []);
for (const text of [bus, webui]) {
assert.deepEqual(lines(text, "WantedBy"), ["multi-user.target"]);
assert.deepEqual(lines(text, "User"), ["mosaic"]);
assert.deepEqual(lines(text, "Group"), ["mosaic"]);
assert.deepEqual(lines(text, "NoNewPrivileges"), ["yes"]);
const hardening = text.split("\n").filter((l) => /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)=/.test(l));
assert.deepEqual(hardening, HARDENING(h));
assert.doesNotMatch(text, /@[A-Z_]+@|%h|MOSAIC_CONFIG=|MOSAIC_CLAUDE_BIN|After=|Wants=/);
}
assert.deepEqual(lines(bus, "AmbientCapabilities"), []);
assert.deepEqual(lines(bus, "CapabilityBoundingSet"), []);
assert.deepEqual(lines(webui, "AmbientCapabilities"), ["CAP_NET_BIND_SERVICE"]);
assert.deepEqual(lines(webui, "CapabilityBoundingSet"), ["CAP_NET_BIND_SERVICE"]);
assert.match(r.out, /enabled and started mosaic-bus@acme\.service, then mosaic-webui\.service\n$/);
assert.deepEqual(read(h.log).trim().split("\n"), [
"is-active --quiet [email protected]",
"is-active --quiet mosaic-webui.service",
"daemon-reload",
"enable [email protected] mosaic-webui.service",
"start [email protected]",
"start mosaic-webui.service",
]);
});
test("PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node", (t) => {
const h = hub(t);
const node = outsideNode();
assert.equal(unit(h, { node }).code, 0);
for (const f of [busFile(h), webuiFile(h)]) assert.deepEqual(lines(read(f), "Environment"), [`PATH=${dirname(node)}:/usr/local/bin:/usr/bin:/bin`]);
assert.match(lines(read(webuiFile(h)), "ExecStart")[0], new RegExp(`^${node} `));
});
test("a failed bus start refuses with the Console not started", (t) => {
const h = hub(t);
const r = unit(h, { env: { CASE2_TEST_FAIL: "start [email protected]" } });
refuses(r, /start mosaic-bus@acme\.service failed; the Console is not started/);
assert.deepEqual(read(h.log).trim().split("\n").slice(2), ["daemon-reload", "enable [email protected] mosaic-webui.service", "start [email protected]"]);
});
test("the script never links a unit file", () => {
assert.doesNotMatch(read(UNIT), /systemctl link/);
});
test("factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty", (t) => {
const h = hub(t);
assert.equal(unit(h, { profile: "factory", extra: ["--pm", "--claude", TRUE_BIN] }).code, 0);
const bus = read(busFile(h));
const exec = lines(bus, "ExecStart");
assert.equal(exec.length, 1);
assert.match(exec[0], / bus start %i --pm$/);
assert.equal(exec[0].split("--pm").length, 2);
assert.deepEqual(lines(bus, "Environment"), ["PATH=/usr/bin:/usr/local/bin:/bin", `MOSAIC_CLAUDE_BIN=${TRUE_BIN}`]);
assert.deepEqual(lines(read(webuiFile(h)), "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${factoryFlags().join(" ")}`]);
const plain = hub(t);
assert.equal(unit(plain, { profile: "factory" }).code, 0);
const text = read(busFile(plain));
assert.deepEqual(lines(text, "ExecStart"), [`${copyDir(plain)}/scripts/mosaic bus start %i`]);
assert.doesNotMatch(text, /MOSAIC_CLAUDE_BIN|@PM_FLAGS@|@CLAUDE_ENV@/);
});
test("a re-install over running units says they are still on the old copy and must restart together, never that it started them", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
const both = unit(h, { copy: COPY_B, env: { CASE2_TEST_ACTIVE: "[email protected] mosaic-webui.service" } });
assert.equal(both.code, 0, both.err);
assert.doesNotMatch(both.out, /started/);
assert.match(both.out, new RegExp(`mosaic-bus@acme\\.service was already running, so it is still on copy ${COPY_A}, not copy ${COPY_B}`));
assert.match(both.out, new RegExp(`mosaic-webui\\.service was already running, so it is still on copy ${COPY_A}, not copy ${COPY_B}`));
assert.match(both.out, new RegExp(`restart mosaic-bus@acme\\.service and mosaic-webui\\.service together to put both on copy ${COPY_B}`));
assert.deepEqual(read(h.log).trim().split("\n").slice(-2), ["start [email protected]", "start mosaic-webui.service"]);
// Only the Console ran (the bus had stopped): the bus starts on the new copy, the Console stays.
const one = unit(h, { copy: COPY_A, env: { CASE2_TEST_ACTIVE: "mosaic-webui.service" } });
assert.equal(one.code, 0, one.err);
assert.match(one.out, new RegExp(`started mosaic-bus@acme\\.service on copy ${COPY_A}`));
assert.match(one.out, new RegExp(`mosaic-webui\\.service was already running, so it is still on copy ${COPY_B}, not copy ${COPY_A}`));
assert.doesNotMatch(one.out, /started mosaic-webui/);
assert.match(one.out, /restart mosaic-bus@acme\.service and mosaic-webui\.service together/);
});
test("a re-install to another copy re-renders both units, and an edited installed unit refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
assert.equal(unit(h, { copy: COPY_B }).code, 0);
assert.match(read(busFile(h)), new RegExp(`copies/${COPY_B}/scripts/mosaic`));
assert.doesNotMatch(read(busFile(h)) + read(webuiFile(h)), new RegExp(COPY_A));
const edits = [
[busFile, (s) => s.replace("User=mosaic", "User=root"), /User= must be exactly once in \[Service\], as User=mosaic/],
[webuiFile, (s) => s.replace(/^ExecStart=(.*)$/m, "ExecStart=$1 --board http://127.0.0.1:1"), /ExecStart= must be exactly once/],
[busFile, (s) => s.replace("ProtectHome=yes\n", ""), /ProtectHome= must be exactly once/],
[webuiFile, (s) => s.replace("PrivateTmp=yes\n", ""), /PrivateTmp= must be exactly once/],
[busFile, (s) => s.replace("Type=simple", "Type=simple\nX-Note=%h"), /%h is left/],
];
for (const [file, edit, pattern] of edits) {
const before = read(file(h));
writeFileSync(file(h), edit(before));
refuses(unit(h, { copy: COPY_B }), pattern);
writeFileSync(file(h), before);
}
assert.equal(unit(h, { copy: COPY_B }).code, 0);
});
test("a template with a namespace, user or system call restriction refuses", (t) => {
const h = hub(t);
for (const line of ["RestrictNamespaces=yes", "PrivateUsers=yes", "SystemCallFilter=@system-service"]) {
const key = line.split("=")[0];
for (const [from, env] of [[BUS_TEMPLATE, "CASE2_BUS_TEMPLATE"], [WEBUI_TEMPLATE, "CASE2_WEBUI_TEMPLATE"]]) {
const file = template(h, from, `t-${key}.in`, (s) => s.replace("NoNewPrivileges=yes", `NoNewPrivileges=yes\n${line}`));
refuses(unit(h, { env: { [env]: file } }), new RegExp(`refusing ${key}=`));
}
}
assert.equal(existsSync(busFile(h)), false);
});
test("the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none", (t) => {
const h = hub(t);
const gone = template(h, BUS_TEMPLATE, "no-env.in", (s) => s.replace(/^EnvironmentFile=.*\n/m, ""));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: gone } }), /EnvironmentFile= must be exactly once/);
const elsewhere = template(h, BUS_TEMPLATE, "env-elsewhere.in", (s) => s.replace("EnvironmentFile=-@SESSION_ENV@", "EnvironmentFile=-/etc/other.env"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: elsewhere } }), /EnvironmentFile= must be exactly once/);
const added = template(h, WEBUI_TEMPLATE, "webui-env.in", (s) => s.replace("NoNewPrivileges=yes", "NoNewPrivileges=yes\nEnvironmentFile=-/etc/other.env"));
refuses(unit(h, { env: { CASE2_WEBUI_TEMPLATE: added } }), /refusing EnvironmentFile=/);
});
test("a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses", (t) => {
const h = hub(t);
const token = template(h, BUS_TEMPLATE, "token.in", (s) => s.replace("Type=simple", "Type=simple\nX-Note=@SECRET@"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: token } }), /a token is left after the render: @SECRET@/);
const webuiToken = template(h, WEBUI_TEMPLATE, "wtoken.in", (s) => s.replace("Type=simple", "Type=simple\nX-Note=@SECRET@"));
refuses(unit(h, { env: { CASE2_WEBUI_TEMPLATE: webuiToken } }), /a token other than one @FLAGS@/);
const pso = template(h, BUS_TEMPLATE, "pso.in", (s) => s.replace("Type=simple", "Type=simple\nPermissionsStartOnly=yes"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: pso } }), /refusing PermissionsStartOnly=/);
for (const pre of ["+/srv/mosaic/x", "-+/srv/mosaic/x"]) {
const file = template(h, BUS_TEMPLATE, "pre.in", (s) => s.replace("Type=simple", `Type=simple\nExecStartPre=${pre}`));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: file } }), /an Exec line that runs with full privileges: ExecStartPre=/);
}
});
test("a symlink at either unit path refuses", (t) => {
for (const name of ["[email protected]", "mosaic-webui.service"]) {
const h = hub(t);
const target = join(h.root, "elsewhere");
writeFileSync(target, "");
symlinkSync(target, join(h.units, name));
refuses(unit(h), new RegExp(`${name.replace(".", "\\.")} is a symlink`));
}
});
test("a --node that is relative, not root-owned or under <home> refuses", (t) => {
const h = hub(t);
refuses(unit(h, { node: "bin/node" }), /--node must be an absolute path/);
const mine = script(join(h.root, "opt", "node"), "exit 0");
refuses(unit(h, { node: mine }), /--node: .* is not owned by root/);
const inside = script(join(h.home, "bin", "node"), "exit 0");
refuses(unit(h, { node: inside }), /--node must be outside/);
});
test("a re-install with a different --node or different serve flags refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
// A new node moves PATH too, so the installed unit fails its own Environment= check first.
refuses(unit(h, { node: outsideNode() }), /installed .*: the Environment= lines must be exactly/);
const flags = libraryFlags(h).map((f) => (f === "melanie" ? "someone" : f));
refuses(unit(h, { flags }), /installed .*: ExecStart= must be exactly once/);
// A line check_unit doesn't fix still has to equal the render.
writeFileSync(busFile(h), read(busFile(h)).replace(/^Description=.*$/m, "Description=edited"));
refuses(unit(h), /differs from the render in more than the copy id/);
});
test("an installed pair whose units name different copies refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
writeFileSync(webuiFile(h), read(webuiFile(h)).replaceAll(COPY_A, COPY_B));
refuses(unit(h), new RegExp(`the installed units name different copies, ${COPY_A} and ${COPY_B}`));
});
test("a copy id that is short, uppercase, holds / or .., or names no directory refuses", (t) => {
const h = hub(t);
for (const copy of ["a".repeat(39), "A".repeat(40), `${"a".repeat(19)}/${"a".repeat(20)}`, `..${"a".repeat(38)}`]) {
refuses(unit(h, { copy }), /--copy must be a full 40-character lower-case commit id/);
}
refuses(unit(h, { copy: "c".repeat(40) }), /no copy directory/);
});
test("library refuses a privileged-floor miss, a bad proof pair and every flag outside its set", (t) => {
const h = hub(t);
const base = libraryFlags(h);
const without = (...names) => {
const out = [];
for (let i = 0; i < base.length; i++) {
if (names.includes(base[i])) { if (base[i] !== "--send") i++; continue; }
out.push(base[i]);
}
return out;
};
refuses(unit(h, { flags: base.map((f) => (f === "443" ? "7332" : f)) }), /--port must be 1\.\.1023/);
refuses(unit(h, { flags: without("--proof-header", "--proof-file") }), /serve flag --proof-header is required/);
refuses(unit(h, { flags: base.map((f) => (f === "X-Auth-User" ? "X-Authentik-Uid" : f)) }), /must be an Authentik uid/);
assert.equal(unit(h, { flags: base.map((f) => (f === "X-Auth-User" ? "X-Authentik-Uid" : f === "melanie" ? "0".repeat(64) : f)) }).code, 0);
rmSync(busFile(h));
rmSync(webuiFile(h));
for (const [flag, value, pattern] of [
["--tester-port", "7333", /case2-unit.sh retires tester TCP; use --tester-socket/],
["--tester-socket", null, /the library profile refuses --tester-socket/],
["--checkout", "/srv/x", /refusing --checkout/],
["--board", "http://127.0.0.1:7331", /refusing --board/],
["--sso", null, /refusing --sso/],
["--nonsense", "1", /unknown serve flag: --nonsense/],
]) {
refuses(unit(h, { flags: [...base, flag, ...(value === null ? [] : [value])] }), pattern);
}
refuses(unit(h, { flags: base.map((f) => (f === "acme" ? "other" : f)) }), /serve --business other is not the unit's business acme/);
refuses(unit(h, { extra: ["--pm"] }), /the library profile refuses --pm/);
refuses(unit(h, { extra: ["--claude", TRUE_BIN] }), /the library profile refuses --claude/);
chmodSync(h.proof, 0o644);
refuses(unit(h), /mode 0600 or 0400, not 644/);
chmodSync(h.proof, 0o600);
const link = join(h.secrets, "proof-link");
symlinkSync(h.proof, link);
refuses(unit(h, { flags: base.map((f) => (f === h.proof ? link : f)) }), /must not be a symlink/);
const dataRoot = join(h.home, ".mosaic-dev");
mkdirSync(dataRoot, { recursive: true });
const outside = join(dataRoot, "proof");
writeFileSync(outside, "made-up-proof\n");
chmodSync(outside, 0o600);
refuses(unit(h, { flags: base.map((f) => (f === h.proof ? outside : f)) }), /must be under .*secrets, outside the data root/);
assert.equal(existsSync(busFile(h)), false);
});
// A non-root test can't make a root-owned file under the scratch home, so the
// owner case names another uid in the passwd database instead.
test("library refuses a proof file not owned by <user>'s uid", (t) => {
const h = hub(t, { uid: UID + 4242 });
refuses(unit(h), new RegExp(`proxy proof file must be owned by mosaic \\(uid ${UID + 4242}\\)`));
});
test("factory refuses duplicate socket, a missing --port or --business, and each proxy flag", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), '--tester-socket'] }), /given twice/);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), '--port', '443'] }), /given twice/);
refuses(unit(h, { profile: "factory", flags: ["--business", "acme"] }), /serve flag --port is required/);
refuses(unit(h, { profile: "factory", flags: ["--port", "443"] }), /serve flag --business is required/);
for (const [flag, value] of [["--public-host", "x.example"], ["--auth-header", "X-A"], ["--allow-user", "u"], ["--proof-header", "X-P"], ["--proof-file", h.proof], ["--send", null]]) {
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), new RegExp(`the factory profile refuses ${flag}`));
}
for (const [flag, value, pattern] of [["--checkout", "/srv/x", /refusing --checkout/], ["--board", "http://127.0.0.1:7331", /refusing --board/], ["--sso", null, /refusing --sso/], ["--nonsense", "1", /unknown serve flag/]]) {
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), pattern);
}
});
test("row253 factory refuses retired TCP and a value after the socket marker", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);
refuses(unit(h, { profile: 'factory', flags: [...factoryFlags(), '7333'] }), /unknown serve flag: 7333/);
});
test("factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", extra: ["--claude", "bin/claude"] }), /--claude must be an absolute path/);
const mine = script(join(h.root, "opt", "claude"), "exit 0");
refuses(unit(h, { profile: "factory", extra: ["--claude", mine] }), /--claude: .* is not owned by root/);
const inside = script(join(h.home, "bin", "claude"), "exit 0");
refuses(unit(h, { profile: "factory", extra: ["--claude", inside] }), /--claude must be outside/);
assert.equal(unit(h, { profile: "factory", extra: ["--pm", "--claude", TRUE_BIN] }).code, 0);
refuses(unit(h, { profile: "factory", extra: ["--claude", TRUE_BIN] }), /--pm and --claude must equal the installed ones|ExecStart= must be exactly once/);
refuses(unit(h, { profile: "factory", extra: ["--pm"] }), /Environment= lines must be exactly|differs from the render/);
refuses(unit(h, { profile: "factory", extra: ["--pm", "--claude", USR_NODE] }), /Environment= lines must be exactly|differs from the render/);
});
// ---- case2-precheck.sh ----
// A scratch user for the precheck: a config, a business file, an installed Console unit,
// fakes on PATH and a fake /proc/sys.
function host(t, { roles = {}, vars = {}, keyFile = `ZAI_API_KEY=${FIXTURE_KEY}\n`, port = "443", busClaude = null } = {}) {
const root = tmp(t, "case2-pre-");
const home = join(root, "home");
const configDir = join(home, ".config", "mosaic-dev");
const config = writeJson(join(configDir, "config.json"), {
configVersion: 1,
environment: "production",
dataRoot: join(home, ".mosaic-dev"),
execution: { backend: "docker", provider: "zai", model: "glm-5.3-flash", adapter: "pi" },
});
const doc = businessDoc(root);
doc.roles.cto.vars = { harness: "pi" };
for (const [name, v] of Object.entries(roles)) doc.roles[name].vars = v;
Object.assign(doc.vars, vars);
writeJson(join(configDir, "businesses", "acme.json"), doc);
const secrets = join(configDir, "secrets");
mkdirSync(secrets, { recursive: true, mode: 0o700 });
if (keyFile !== null) {
writeFileSync(join(secrets, "claude-session.env"), keyFile);
chmodSync(join(secrets, "claude-session.env"), 0o600);
}
const units = join(root, "units");
mkdirSync(units);
writeFileSync(join(units, "mosaic-webui.service"), `[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --port ${port} --business acme\n`);
writeFileSync(join(units, "[email protected]"), `[Service]\nEnvironment=PATH=/usr/bin\n${busClaude ? `Environment=MOSAIC_CLAUDE_BIN=${busClaude}\n` : ""}`);
const sys = join(root, "sys");
mkdirSync(join(sys, "net", "ipv4"), { recursive: true });
mkdirSync(join(sys, "kernel"), { recursive: true });
writeFileSync(join(sys, "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
writeFileSync(join(sys, "kernel", "apparmor_restrict_unprivileged_userns"), "1\n");
const bin = join(root, "bin");
const me = spawnSync("id", ["-un"], { encoding: "utf8" }).stdout.trim();
script(join(bin, "getent"), `[ "$1" = passwd ] && [ "$2" = '${me}' ] && echo '${me}:x:${UID}:${GID}::${home}:/bin/bash'`);
script(join(bin, "sudo"), "exit 1");
script(join(bin, "id"), `if [ "$1" = -G ]; then echo "\${CASE2_TEST_GROUPS:-${GID}}"; else exec /usr/bin/id "$@"; fi`);
script(join(bin, "node"), `if [ "$1" = --version ]; then echo "\${CASE2_TEST_NODE:-v24.3.0}"; exit 0; fi\nexec '${process.execPath}' "$@"`);
script(join(bin, "bwrap"), `exit "\${CASE2_TEST_BWRAP:-0}"`);
return { root, home, configDir, config, secrets, units, sys, bin, dataRoot: join(home, ".mosaic-dev") };
}
// A fake claude: replace answers with the marker and a small prompt; append answers with a large one;
// nomarker answers with a small prompt and no marker; text answers with plain text, not JSON.
function fakeClaude(dir, kind) {
return script(join(dir, "claude"), `[ "$1" = --version ] && { echo "9.9.9 (Claude Code, ${kind} fake)"; exit 0; }
file=''
while [ $# -gt 0 ]; do [ "$1" = --system-prompt-file ] && file="$2"; shift; done
marker="$(awk '{ print $NF }' "$file")"
${kind === "nomarker" ? "marker=something-else" : ""}
${kind === "text" ? 'echo "$marker"; exit 0' : ""}
tokens=${kind === "append" ? 9000 : 40}
printf '{"result":"%s","usage":{"input_tokens":%s,"cache_read_input_tokens":0}}\\n' "$marker" "$tokens"`);
}
// This host may have a claude in /usr/bin, so this PATH links every /usr/bin entry but that one.
function pathWithoutClaude(x) {
const usr = join(x.root, "usr-bin");
mkdirSync(usr);
for (const name of readdirSync("/usr/bin")) if (name !== "claude") symlinkSync(join("/usr/bin", name), join(usr, name));
return `${x.bin}:${usr}`;
}
function precheck(x, { profile = "library", env = {}, path } = {}) {
const r = spawnSync(PRECHECK, ["--business", "acme", "--profile", profile], {
encoding: "utf8",
timeout: 60000,
env: {
PATH: path ?? `${x.bin}:/usr/bin:/bin`,
HOME: x.home,
MOSAIC_CONFIG: x.config,
CASE2_UNIT_DIR: x.units,
CASE2_PROC_SYS: x.sys,
CASE2_GETENT: join(x.bin, "getent"),
...env,
},
});
const out = r.stdout + r.stderr;
assert.doesNotMatch(out, new RegExp(FIXTURE_KEY), "the precheck printed the fixture key");
return { code: r.status, out };
}
function blocked(r, pattern) {
assert.equal(r.code, 1, r.out);
assert.match(r.out, pattern);
}
test("the precheck passes on a good host and prints free memory, without the key", (t) => {
const x = host(t);
const r = precheck(x);
assert.equal(r.code, 0, r.out);
assert.match(r.out, /ok {6}config validates/);
assert.match(r.out, /ok {6}bus\.tester is off/);
assert.match(r.out, /ok {6}net\.ipv4\.ip_unprivileged_port_start 1024 is above the Console's port 443/);
assert.match(r.out, /claude-session\.env sets ZAI_API_KEY/);
assert.match(r.out, /free memory: \d+ MiB/);
assert.doesNotMatch(r.out, /claude .*fake|marker/);
});
test("the precheck refuses each failed host check", (t) => {
blocked(precheck(host(t), { env: { CASE2_TEST_NODE: "v22.22.2" } }), /REFUSED node v22\.22\.2 is older than 24/);
blocked(precheck(host(t), { env: { CASE2_TEST_GROUPS: `${GID} 998` } }), /REFUSED id -G prints more than one group/);
const sudo = host(t);
script(join(sudo.bin, "sudo"), "exit 0");
blocked(precheck(sudo), /REFUSED sudo -n true succeeds/);
blocked(precheck(host(t), { env: { XDG_DATA_HOME: "/elsewhere" } }), /REFUSED XDG_DATA_HOME is \/elsewhere/);
const home = host(t);
assert.equal(precheck(home, { env: { XDG_DATA_HOME: `${home.home}/.local/share` } }).code, 0);
blocked(precheck(host(t), { env: { HOME: "/tmp" } }), /REFUSED HOME is \/tmp/);
const runs = host(t);
mkdirSync(join(runs.dataRoot, "launches", "acme"), { recursive: true, mode: 0o755 });
chmodSync(join(runs.dataRoot, "launches", "acme"), 0o755);
blocked(precheck(runs), /REFUSED run root .* is not a 0700 directory \(mode 755\)/);
chmodSync(join(runs.dataRoot, "launches", "acme"), 0o700);
assert.match(precheck(runs).out, /ok {6}run root .* is 0700/);
const bad = host(t);
writeFileSync(bad.config, "{ not json");
blocked(precheck(bad), /REFUSED config:/);
});
test("the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root", (t) => {
const nouser = host(t);
script(join(nouser.bin, "getent"), "exit 2");
blocked(precheck(nouser), /REFUSED user: no .* in the passwd database/);
const unparsed = host(t);
writeFileSync(join(unparsed.configDir, "businesses", "acme.json"), "{ not json");
blocked(precheck(unparsed), /REFUSED business file: cannot read .*acme\.json/);
const gone = host(t);
rmSync(join(gone.configDir, "businesses", "acme.json"));
blocked(precheck(gone), /REFUSED business file: cannot read .*acme\.json: ENOENT/);
const invalid = host(t);
const file = join(invalid.configDir, "businesses", "acme.json");
const doc = JSON.parse(read(file));
doc.notAField = true;
writeJson(file, doc);
const r = precheck(invalid);
blocked(r, /REFUSED business file: /);
assert.doesNotMatch(r.out, /REFUSED business file: cannot read/);
// stat follows a link, so a link to a 0700 directory would read as 0700.
const link = host(t);
const target = join(link.root, "elsewhere");
mkdirSync(target, { mode: 0o700 });
chmodSync(target, 0o700);
mkdirSync(join(link.dataRoot, "launches"), { recursive: true });
symlinkSync(target, join(link.dataRoot, "launches", "acme"));
blocked(precheck(link), /REFUSED run root .* is a symlink/);
});
test("a bwrap that fails refuses and prints the sysctl and where bwrap is", (t) => {
const x = host(t);
blocked(precheck(x, { env: { CASE2_TEST_BWRAP: "1" } }), new RegExp(`REFUSED bwrap .* failed; bwrap: ${join(x.bin, "bwrap")}; kernel\\.apparmor_restrict_unprivileged_userns: 1`));
});
test("an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit", (t) => {
for (const floor of ["443", "80"]) {
const x = host(t);
writeFileSync(join(x.sys, "net", "ipv4", "ip_unprivileged_port_start"), `${floor}\n`);
blocked(precheck(x), new RegExp(`REFUSED net\\.ipv4\\.ip_unprivileged_port_start is ${floor}, at or below the Console's port 443`));
}
const x = host(t);
rmSync(join(x.units, "mosaic-webui.service"));
blocked(precheck(x), /REFUSED no installed Console unit/);
const noPort = host(t);
writeFileSync(join(noPort.units, "mosaic-webui.service"), "[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --business acme\n");
blocked(precheck(noPort), /REFUSED .*mosaic-webui\.service has no --port in its ExecStart/);
const noFloor = host(t);
rmSync(join(noFloor.sys, "net", "ipv4", "ip_unprivileged_port_start"));
blocked(precheck(noFloor), /REFUSED cannot read net\.ipv4\.ip_unprivileged_port_start/);
});
test("bus.tester: library refuses it on, factory needs it on, and factory refuses a library business", (t) => {
blocked(precheck(host(t, { vars: { "bus.tester": true } })), /REFUSED bus\.tester is on; the library profile needs it off/);
blocked(precheck(host(t), { profile: "factory" }), /REFUSED bus\.tester is off; the factory profile needs it on/);
const on = precheck(host(t, { vars: { "bus.tester": true } }), { profile: "factory" });
assert.equal(on.code, 0, on.out);
const lib = host(t, { vars: { "bus.tester": true } });
const file = join(lib.configDir, "businesses", "acme.json");
const doc = JSON.parse(read(file));
doc.projects.stack.kind = "library";
writeJson(file, doc);
blocked(precheck(lib, { profile: "factory" }), /REFUSED profile-library-business: .* projects stack are library projects/);
});
test("the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong", (t) => {
// No claude-code instance and no claude anywhere on PATH: the precheck passes.
const none = host(t);
const r = precheck(none, { path: pathWithoutClaude(none) });
assert.equal(r.code, 0, r.out);
assert.doesNotMatch(r.out, /--system-prompt-file|claude is installed/);
const good = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(good.bin, "replace");
const pass = precheck(good);
assert.equal(pass.code, 0, pass.out);
assert.match(pass.out, /claude .*9\.9\.9 \(Claude Code, replace fake\)/);
assert.match(pass.out, /ok {6}claude replaces its prompt with --system-prompt-file \(40 input tokens\)/);
const bad = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(bad.bin, "append");
blocked(precheck(bad), /REFUSED claude adds to its own prompt with --system-prompt-file: 9000 input tokens/);
const nomarker = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(nomarker.bin, "nomarker");
blocked(precheck(nomarker), /REFUSED claude's reply to the marker prompt lacks the marker \(40 input tokens\)/);
const text = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(text.bin, "text");
blocked(precheck(text), /REFUSED claude gave no JSON reply to the marker prompt/);
const missing = host(t, { roles: { cto: { harness: "claude-code" } } });
blocked(precheck(missing, { path: pathWithoutClaude(missing) }), /REFUSED an instance runs claude-code and no claude is installed/);
});
test("the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH", (t) => {
const pinned = join(tmp(t, "case2-claude-"), "pinned");
const replace = fakeClaude(pinned, "replace");
const x = host(t, { roles: { cto: { harness: "claude-code" } }, busClaude: replace });
fakeClaude(x.bin, "append");
const r = precheck(x);
assert.equal(r.code, 0, r.out);
assert.match(r.out, /replace fake/);
const pinnedBad = join(tmp(t, "case2-claude-"), "pinned");
const append = fakeClaude(pinnedBad, "append");
const y = host(t, { roles: { cto: { harness: "claude-code" } }, busClaude: append });
fakeClaude(y.bin, "replace");
blocked(precheck(y), /REFUSED claude adds to its own prompt/);
});
test("the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line", (t) => {
blocked(precheck(host(t, { keyFile: null })), /REFUSED an instance's provider is zai and .*claude-session\.env is missing/);
const open = host(t);
chmodSync(join(open.secrets, "claude-session.env"), 0o644);
blocked(precheck(open), /REFUSED .*claude-session\.env must be owned by .* at 0600 \(mode 644\)/);
const link = host(t, { keyFile: null });
const real = join(link.root, "real.env");
writeFileSync(real, `ZAI_API_KEY=${FIXTURE_KEY}\n`);
chmodSync(real, 0o600);
symlinkSync(real, join(link.secrets, "claude-session.env"));
blocked(precheck(link), /REFUSED .*claude-session\.env is a symlink/);
blocked(precheck(host(t, { keyFile: "ZAI_API_KEY=\n" })), /REFUSED .*sets no nonempty ZAI_API_KEY/);
blocked(precheck(host(t, { keyFile: `ZAI_API_KEY=${FIXTURE_KEY}\nZAI_API_KEY=\n` })), /REFUSED .*sets no nonempty ZAI_API_KEY/);
const quoted = precheck(host(t, { keyFile: `ZAI_API_KEY=\nZAI_API_KEY="${FIXTURE_KEY}"\n` }));
assert.equal(quoted.code, 0, quoted.out);
});
// ---- the bind test (mos-1100) ----
// A session can't take the Console's port: inside the sandbox a real session gets, a bind below
// 1024 fails with EACCES, and the same bind on a free port of 1024 or above succeeds.
const sandboxSkip = sandboxProblem() ?? false;
const LOW_PORT = 980;
test("inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds", { skip: sandboxSkip, timeout: 30000 }, async (t) => {
const floor = Number(read("/proc/sys/net/ipv4/ip_unprivileged_port_start").trim());
assert.ok(floor > LOW_PORT, `this host's net.ipv4.ip_unprivileged_port_start is ${floor}, at or below the test port ${LOW_PORT}, so the test can't show the sandbox refuses it`);
const root = tmp(t, "c2b-");
const runDir = join(root, "run");
const workspace = join(root, "ws");
mkdirSync(runDir);
mkdirSync(workspace);
mkdirSync(join(runDir, "bundle"));
writeFileSync(join(runDir, "session.json"), "{}");
const adapter = join(root, "adapter.sh");
const brokerSocket = join(root, "broker.sock");
writeFileSync(adapter, "");
writeFileSync(brokerSocket, "");
prepareRun(runDir);
const list = binds({ runDir, workspace, adapter, harnessMounts: [], brokerSocket });
const high = await new Promise((done) => {
const s = createServer().listen(0, "127.0.0.1", () => { const { port } = s.address(); s.close(() => done(port)); });
});
const bindIn = (port) => {
const code = `require("net").createServer().on("error", (e) => { console.log(e.code); process.exit(0); }).listen(${port}, "127.0.0.1", function () { console.log("bound"); this.close(); });`;
const argv = sandboxArgv(list, runDir, [process.execPath, "-e", code], { business: "acme", role: "coder" });
const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf8", timeout: 10000 });
assert.equal(r.status, 0, r.stderr);
return r.stdout.trim();
};
assert.equal(bindIn(LOW_PORT), "EACCES");
assert.equal(bindIn(high), "bound");
});
@@ -0,0 +1,463 @@
# Deferred list
One line per item, written from evidence, never from inertia. Add a line
when a gap is found and not fixed in the current piece; strike it (move to
Done at the bottom with the commit) when a piece closes it. Nothing here
starts on its own; a line becomes a piece only when Jason calls it. Check it
at every gate. Started 2026-09-12 during the control board MVP.
## Open
- **Inherited Discord engine test intermittency during #1512 review.** Frozen
serialized run recorded a tool-turn engine assertion failure and then timed
out at 120s. A later full TAP run with a 15s test deadline passed 351/351.
Rocko isolated the busy/settled-event test race and missing failure cleanup;
Darkwing independently reproduced the event-split assertion. Rocko also reported
a timeout-log test race and a separate engine followUp-after-timeout defect.
No live incident established. Row21 owner orch-01 notified; no Discord source
takeover. Not assumed identical to earlier concurrent hangs.
Integration held pending owner disposition; logs and scope in
`agents/darkwing/work/relaunch-activity/r1-review.md`. (2026-09-15)
Outcome 2026-09-26: 1685deb4 fixed the busy/settled race and the engine no
longer sends a follow-up (held prompts). A re-run of the frozen candidate
against 21e3e908 passed the serial acceptance command 397/397 three times.
Two engine failures remain when tests run concurrently, and they reproduce on
clean HEAD without the #1512 files, so they are #1509's (entry below). Sage
ruled that #1512 integrates on the serial evidence; it landed in af4203ca.
Record: `agents/darkwing/work/relaunch-activity/rerun-2026-09-26.md`.
- **Registration has no "set by" field.** `mosaic seat task` records the
task and `updatedAt` but not who set it, so a human session and the
orchestrator look the same on the board. One-line record change; rides
with the orchestrator assignment piece. (2026-09-12, #1504)
- **Usage-limit notice was missed.** Jason reports Rocko exhausted usage without
Jason seeing the notice, and is now available again. Investigate where the
notice appeared and what operator notification is missing as part of Claude
console integration. User-reported, not independently reproduced; no root
cause established. Keep outside #1512's relaunch-display scope. (2026-09-15)
- **Claude Code seats register but get no board row.** Rocko has no Pi
session log, and rows come only from Pi logs. Needs a second row source
or a Claude-side log reader. (2026-09-12, darkwing's #1504 report)
2026-09-15 #1511 live-test preflight reconfirmed no Rocko row: expected
`.pi/state/rocko/sessions` discovery directory absent. Its existing repo
registration names dead PID 185602, while the verified native lock owner is
3707667. No registration repair, discovery expansion or relaunch performed;
test write refused before mutation. Evidence:
`agents/darkwing/work/task-attribution/backend-live-verification.json`.
- **Fleet seats (`~/.mosaic`) stay on the old launchers.** No registration,
no board reply, no `seat task`. Moving them is the stress test and comes
after the WebUI and fleet launch work. (Jason, 2026-09-12)
- **Comms skill has no rule for senders without a pane.** The board trailer
covers it today; if another pane-less sender appears, put the rule in
`skills/ms-communications` instead of a second trailer. (2026-09-12, #1505)
- **Board: multiple sessions per seat, stopping seats, auth beyond
loopback.** Out of scope for the MVP by the plan; listed so they are not
forgotten. (plan page, Boundaries)
- **`MOSAIC-STACK-D-001` is Answered, not Completed.** Mark `--completed` in
jarvis-brain once Jason says the board answers "who is waiting on me".
(2026-09-12)
- **Uncommitted working tree.** `AGENTS.md` autonomy section,
`agents/README.md` Sage row, Dewey's `work/wui` docs and evidence,
`docs/plans/reviews/2026-09-08..10_*`, `skills/ms-{arena,blast-radius,
board,executive-update,git-operations,technical-writing}`. Jason's edits
from before 2026-09-12; they ride along in the next commit that touches
them unless staged on purpose. (2026-09-12)
- **Fleet debt (orch-01, `~/.mosaic`):** `sweep-commit.sh` deny-list regex
skips `launch.env`; needs the fix plus a regression test. (2026-09-12)
- **Parked, not cancelled:** Dewey D05 dashboard mockup work beyond design 3,
registry increment 3 (headless identity-env leak), new seat directories.
(plan page, 2026-09-12 parking note)
- **Gitea helper response-file hardening.** Predictable `/tmp/gitea-api-response.$$`
has no explicit 0600 mode; use `mktemp` with cleanup in a separately reviewed
pass. Credential validation and later reread also have a race window. Other
non-blocking ledger review notes remain in
`reviews/2026-09-12_ledger-verdict.md`. No change beyond the reviewed GET
cleanup fix in this piece. Filbert, LEDGER-1506-R1, 2026-09-12, #1506.
- **One shared checkout for every seat and the coordinator.** Any `git add`
of a shared file (CURRENT.md, SESSIONS.md, the plan page) commits another
seat's in-progress edit under the wrong commit; it happened at f80404f5
(dewey's CURRENT.md status note went in with the piece 5 brief, harmless
but unowned). Fix candidates: per-seat worktrees, or shared files edited
only by the seat named in CURRENT.md. (2026-09-12)
- **Console mockup fields absent from the board API.** Controller, generation,
config fingerprint, turn count, goal/report and activity stream are omitted.
Workspace paths are shown as board data, not invented registered workspace
nodes. Launch, observe, stop, full transcript navigation, command palette and
other screens are absent. Existing palette/appearance switches remain on the
single page. Revisit only with a new brief. (2026-09-13 UTC, #1507)
- **Board send can turn into a Pi slash command.** `tools/tmux`
`send-message.sh` pastes onto whatever is already in Pi's composer without
clearing it. On Jason's 2026-09-26 20:10:57Z send, a leftover "/" landed in
front of the agent-send header. Pi read it as plain text that time, but the
same path could run a board reply as a slash command. Belongs to CHAT-03I/B3.
No tools/tmux change until that piece. Found by Dewey in the CHAT-02 brief
evidence. (2026-09-26, #1507)
- **Discord wedge restarts have no cumulative stop.** Since 6b R2, a wedged
pi ends the connector with exit 1, and the unit restarts it. With
turnTimeoutSeconds 600, one cycle takes at least 645 s, longer than the
600 s StartLimit window. So a pi that wedges on every prompt restarts once
per prompt and never hits the limit. It only happens when someone sends a
message, and that person gets an error reply. Sage accepted it for now. The
fix is a separate policy (a wedge count kept across restarts). (2026-09-26,
#1509, Rocko 6b R2 review)
- **Connector does not journal the source it started from.** The unit runs
from the live checkout, so an automatic restart loads whatever Discord
source is in the tree, even uncommitted source. Rocko recommends a startup
journal line with HEAD, the dirty state of `packages/discord` and a digest
of the runtime files. Until then the rule is that unreviewed Discord source
stays uncommitted in the tree only as long as a review needs it.
(2026-09-26, #1509)
- **Index-export suite runs leave Docker networks behind.** Each export
directory becomes a compose project, and its `<dir>_default` network
stays after the run. On 2026-09-26 the host ran out of address pools. The
task and release suites then failed 31 and 3 tests with "all predefined
address pools have been fully subnetted". Sage removed its own five stale
networks. The dw-* ones are still there. Fix: the export recipe, or the
suites, removes the project network on exit. (2026-09-26)
2026-09-27: it happened again on the Piece D candidate (task 31 and
release 3 failures). Sage removed 12 leftover export networks from
other seats, each checked for zero attached containers first, and both
suites then passed. Still open.
- **Ledger T3 read: snapshot isolation has no test.** `readT3` runs every
query in one read transaction, but no test proves a write landing during
the read stays invisible. Filbert's note 4 on the Gate F build (e47ec6da).
Notes 1 to 3 go to Darkwing's follow-up. (2026-09-26, #1506)
- **A T3 sender role with spaces counts as human.** The SetSpark lead's
reply to Sage (21:41:24Z) used the role "main content-engine agent". The
6a rule requires a one-word role, so the ledger counts that message as
human in Sage's row. Gate F reads only Filbert's row, so it is not
affected. Filbert found it through the live diagnostic (14 went to 15).
Sage asked the SetSpark lead to use a one-word role. The rule stays as
it is. (2026-09-26, #1506)
- **Queue tests: the cold-start failure came back (reopened).** Row 33
closed this as not reproduced in 20 idle cold runs. On 2026-10-04 Sage's
index export of the row 33 candidate ran the nine suites, then
`node --test packages/queue/tests/`, and got 147 passed, 1 failed. Sage's
recipe threw the output away, so again nobody knows which test failed.
Ten more runs all passed 148/148: three reruns in the same export, four
under parallel suite load, and three fresh exports run straight after
`test-queue.sh`. It needs a first run on a fresh tree after a full
suite pass, and even then it's rare. Darkwing's n2 on row 33 (the cold
runs were idle) was right. Next time: Sage's export recipe keeps the
node test output in a file, so the first failure has a name. Then fix it
as its own row. (2026-10-04, #1508)
- **Queue: `add --issue N` also sets closes to N.** Follow-up rows under
an umbrella issue, like rows 31 and 33 under #1508, would close the issue
when they finish. Sage narrowed both by hand (revs 36 and 43). Either
`add` takes `--closes` or the lead checks closes on every add. Not
urgent while Sage adds the rows. (2026-10-04, #1508)
- **Ledger metric call can orphan curl.** The metric call in
`packages/ledger/src/ledger.mjs` still relies on execFileSync's
timeout, which kills the helper but can leave curl running. E's queue
calls already use `timeout -s KILL`; the metric call should match.
(2026-09-27, #1508)
- **Ledger: a missing `timeout` command reads as a Gitea failure.** With
no `timeout` on PATH the queue issue calls report "credential or Gitea
request failure". Optional test and clearer message. Filbert, E r2.
(2026-09-27, #1508)
- **`review record` doesn't check which issue the verdict comment is on.**
Row 5 round 1's request (26671) and Filbert's verdict (26683) are on
#1507. Darkwing's verdict 26681 went to #1508, and `review record`
accepted it, because only `review resolve` reads the comment back
(`checkComment` in `packages/queue/src/review.mjs`). The receipt
points at a real comment on the wrong issue. Sage posted a pointer on
#1507 (comment 26685). Fix: have `record` check the comment's issue and author the way
`resolve` does, or refuse a comment id it can't verify. Found by
Filbert. (2026-10-04, #1508)
- **Host seats launch Pi with `--approve`.** A future `.pi/SYSTEM.md` or
`.pi/settings.json` in the checkout would load in every host seat. This
is latent, since neither file exists today. Fix in
`scripts/agent-host-dev.sh`: pass `--no-approve` and an explicit system
prompt. Filbert, meta-harness survey section 8.4. (2026-10-04)
- **Workers can read provider keys.** The worker container passes
`ZAI_API_KEY` and `ANTHROPIC_API_KEY` in the environment, which Pi's
`bash` inherits, and Pi's `read` can open the mounted `auth.json`.
Slice 1 role tokens stay in the broker (lead decision 47). The workers'
own exposure needs a separate piece. Filbert, survey section 8.6.
(2026-10-04)
- **CHAT-03 I1 round 2 follow-ups, for the I3 brief.** Neither reviewer
held any of these as blocking (Darkwing comment 26690, Filbert comment
26694 on #1507). Four of the five are closed by slice 1 S5 (see Done);
this one stays open.
- No test proves that the processes are listed only after the cgroup
is frozen. The code waits for `frozen 1` in `cgroup.events`, and
both Filbert and Dewey confirmed it by reading it. Mutants r2-B5b and
C4 survive. Proposed: a test-only hold after `members`, or a FUSE or
privileged fixture. Filbert F1.
(2026-10-04, #1507)
- **CHAT-03 `engine.envKeys` accepts founder credential names.**
`CREDENTIAL_NAME` in `packages/conversation/src/pi-pin.mjs` matches any
`*_API_KEY` or `*_TOKEN`, including `GITEA_TOKEN` and `GH_TOKEN`, which
the S6 runner refuses as founder credentials (REQ-CRED-2). Nothing
builds a Controller outside the tests yet. Whoever wires the entry point
narrows the pattern to provider names or checks the founder list.
Filbert N2 on #1522. (2026-10-10)
- **`test-task.sh` makes a live Pi call without the Docker guard.** The
"user recall" block near line 478 runs `run-task.sh` with no
availability check, so a run with Docker unavailable fails "user recall
run succeeds" and "recalled user name" (26 pass, 2 fail). The other
live groups skip. Rocko hit it during the S2 suites. Wrap the block in
the same guard as the other live groups, so a seat without provider
access gets clean skips. (2026-10-04, #1519)
- **`node --test <dir>/` fails on Node 24.** Node 24 reads a directory
argument as a module, so every package's test command in that form
fails there, while Node 26 accepts it. The glob form
(`node --test 'packages/x/tests/*.test.mjs'`) works on both. Package
READMEs, `docs/TOOLS.md` and Sage's suite recipe should use the glob
form wherever Node 24 is a target. Darkwing, S1 packet. (2026-10-04,
#1518)
- **`stack-upgrade.sh` doesn't check `$REPO`.** Nothing confirms the
source checkout is root-owned or that its tree is clean before the
upgrade builds from it. Filbert N2 on row 233 (29922, #1735).
(2026-10-21)
- **`stack-upgrade.sh` has no signal handling between A2 and A5.** A HUP,
INT or TERM in that stretch leaves the run failed closed, but recovery
is by hand. A trap that writes the stop point and the recovery step to
the log would make it a sheet step. Filbert N3 on row 233 (29922,
#1735). (2026-10-21)
- **`checkCreative` doesn't refuse `credentials`.** Row 213's
`checkCreative` (packages/business/src/role.mjs:143 at 2d2ad2760)
limits a creative role's tools, authority and messaging, but a role
file that gave companion or research a credential would still load.
The business file could then map a credential to that instance
(business.mjs:157). Nobody has traced whether a mapped credential
reaches the run's environment. The decision paths stay closed (M6),
but a creative role with `web` holding a live token is a wider channel
than design 4 means. The fix: refuse any `credentials` in
`checkCreative`, with a role.test.mjs case. It goes after row 213
lands, as a follow-up row or in the next row that edits role.mjs.
Webby N1 on row 213 r3 (REVIEW-r3.md, 3ab3c010b), mos-1160.
(2026-10-21)
- **The boot host check orders only one business's bus.**
`mosaic-hostcheck.service` (row 240) has
`[email protected]`. `case2-unit.sh` enables the
bus as `mosaic-bus@<business>.service`, so on a host whose business id
isn't `mosaic-stack` nothing orders the two. The bus can start before
the boot stop runs, and after a boot-time drift role sessions can use
bwrap until it does. The fix: `After=mosaic-hostcheck.service` in
`packages/cli/systemd/mosaic-bus.service.in`, which orders every
instance and does nothing where the unit isn't installed. It goes in
before the first install whose business id isn't `mosaic-stack`.
Filbert N1 on row 240 (29945, #1746). (2026-10-21)
- **The run sheet writes the apt hook before the first `--check`.**
Section 4 installs `80mosaic-hostcheck`, then runs
`hostcheck.sh --check`. If that check fails, the next package run
closes bwrap before anyone has read the failure. It fails closed, but
it surprises an installer. The fix: run `--check` first, or say what a
failure at that step means. Doc-only, for the next row that edits
`docs/guides/host-run-sheet.md`. Filbert N2 on row 240 (29945, #1746).
Filbert's N3, that a closed bwrap appends to `DRIFT` on every later
apt run and boot, is by design; ops-01's STATE read should expect the
repeats. (2026-10-21)
- **The sso-config first-step hint has no business.** When the target
copy has no `console.sso`, `stack-release.sh` (`:938`) refuses with
"activate $TO without --sso first, then ... activate $TO --sso". The
usage line is `activate ID BUSINESS` (`:47`), so pasting either command
as printed gives a usage error, not the first step. It fails safe,
since the usage error changes nothing. The fix: add `$BUSINESS`
(set at `:2138`) to both commands, and the business (`acme`)
to the three expected strings in `stack-release.test.mjs`
(`:1538`, `:1689`, `:1691`). For a later row that edits
`stack-release.sh`. Webby N1 on row 238 r1 (REVIEW-r1.md, 433a95292,
#1744). (2026-10-21)
- **Lane image prep inspects a base that BuildKit never tagged.**
(N7 of the row 197 proof.) After the Compose build,
`scripts/testing/lane-image.mjs:41` (`prepareImage`) runs
`docker image inspect node:24-bookworm-slim` to put the base digest
in the receipt. BuildKit resolves the base from the registry
without tagging it in the local store, so on a host without the tag
the inspect throws "No such image" after a good build. That
happened in Sage's proof try 1 (2026-10-21 09:03Z, run cf48efccf083).
The throw fails closed: the run's terminal record is unclean, both
lanes quarantine, and `gate-lane.sh recover` clears them. Pulling the
base first fixes it, and the receipt digest then matches what the
build used (91acba91 both ways). The fix: before the build, inspect
the base and refuse with a named reason if it's missing, or pull it.
After the build, refuse if the base digest changed. Add a test with a
fake Docker that has no base tag. Kept out of row 197 r6 by Sage
(sage-1883). Mos asked for this record in mos-1162. (2026-10-21)
- **Case 1 acceptance script: root parses with the node it's checking, and the header says less than root does.**
(Duckworth N1-N3 on the row 195 manifest, REVIEW-m1 6afcee2a5.)
N1: F9 runs `$ROOT$NODE` as root (`scripts/case1-accept.sh:298`)
before F10 checks that `--node` is the `/opt/mosaic-stack/node-<pin>`
path (:303-304). On a host installed as designed, `case2-unit.sh:128-133`
refused a non-root-owned `--node` and the args file is root's, so no
escalation path exists today. Run F9's parse through `AS_USER`, or
apply F10's path check first. N2: the header (:10) and the TOOLS.md
entry say root only reads, but root also runs `curl` (F2), `sudo -l`
(P1, P3), the jarvis probe (P9), `hostcheck.sh --check` and
`systemctl`. Doc-only. N3: F6's prefix match (:261) accepts a
`--claude` with `..` after `claude-<pin>/`. That claude runs only as
`mosaic`, and F10 uses equality. Darkwing has a follow-up on
`darkwing/row195-n1` 1e3c4767c, not yet run; it gets its own row now
that 195 has landed (b65014647), Duckworth reviews. (2026-10-21)
- **Morgana's row 213 mutation set: the B3 pattern no longer matches `bundle.mjs`.**
(Webby N3 on row 221 round 1, REVIEW-r1 a0a00630e.) Row 221's
candidate changes `packages/harness/src/bundle.mjs`, and the B3
anchor at Morgana's 213 `mutants.py:48` stops matching on that
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)
- **Row 66 live checks: the unslop notice on a live managed session, both harnesses.**
(mos-1186, Vikunja #454.) Row 66's gate asked for one landing session per
harness with a prompt that draws a known tell. Mos changed that gate. The
Claude Code session ran before landing on a scratch root from the
candidate tree (run r4ab24630d323, landed 2c38325ec). Pi ran launch only there (run r1dd94171bede):
this host has no credential pi reads until P19 #383 settles. Two live
checks remain. Claude Code: in the first re-pin whose tree includes row 66,
backup first (Q173). Pi: one full session with a tell after #383. Each
attaches the operator note on the RESULT, the next-turn correction and the
`unslop.jsonl` counts. (2026-10-21)
- **Console tester listener: `testerNoSignIn` is a dead parameter.**
(Duckworth N2 on the row 196 manifest, REVIEW-m1; carried as C1 in
REVIEW-m2 cdbf7b64e, #1694 comment 30156.) Row 196 landed 7247d80c7
with the parameter and its comment still at `serve.mjs:472-475` and
passed from `webui/src/cli.mjs:109` and `sso.test.mjs:392`. Nothing
in `serve.mjs` reads it after the destructure. Remove it, or wire it
with a test. This binds the next row that touches
`packages/webui/src/serve.mjs`. (2026-10-21)
- **Console library routes: the tester listener and `library.sync`.**
(Duckworth N5 on row 196 round 2, REVIEW-m2 cdbf7b64e.) At 7247d80c7
`startServer` (`serve.mjs:475`) takes none of `libraryFile`,
`libraryStatus` or `librarySync`, so the callbacks `cli.mjs` passes
do nothing yet. The row that adds the Console library routes decides
whether the tester listener gets `library.sync`. Duckworth's view is
that it shouldn't, because sync is a human write. (2026-10-21)
- **Console library routes: four library codes still map to 502.**
(Dewey N2 on the row 254 manifest, #1759 comment 30173.) Row 254
gave six library codes a STATUS entry in `packages/webui/src/bus.mjs`.
`library-mapping-invalid`, `library-remote-invalid`,
`library-state-invalid` and `library-git` still fall through
`busStatus` to 502. The row that adds the Console library routes
decides their statuses and gives its `humanCall` for `library.sync`
a timeout over 130 seconds. (2026-10-21)
- **T27 sweep test leaves two `sleep 60` children running.**
(Darkwing row 251 slot, darkwing-0113 and darkwing-0117; Dewey on
r2, comment 30168.) The sweep test spawns two detached shells,
`planted` at `host-run.test.mjs:528` and `decoy` at `:536`, each
running `sleep 60; :` with the run dir as `$0`. `sweepRunDir` kills
the planted shell by pid (`host-run.mjs:238`), and `t.after` kills
only the decoy shell. Neither kill reaches the `sleep` child, so each
one lives for up to 60 s after the test and trips any busy check that
follows. The fix is to kill the process group in both `t.after`
hooks: `process.kill(-pid, 'SIGKILL')`. Each shell is spawned
detached, so it leads its own group. `exec sleep 60` doesn't work.
The sweep matches the run dir in `/proc/<pid>/cmdline`
(`host-run.mjs:236`), and exec drops the `$0` that carries it, so the
planted process would survive and the "planted process killed" assert
would fail. This binds the next row that touches
`packages/bus/tests/host-run.test.mjs`. (2026-10-21)
- **Real-model suite cases run outside the gate.** Row 244 moved
`test-task.sh` fork recall (M11) and user recall (M16) behind
`MOSAIC_LIVE_PROVIDER=1`, and `test-release.sh` activations to the mock
adapter. Before any re-pin whose tree changes `packages/harness` or the
session fork code (`scripts/mosaic-task.mjs` session and
`sessionForkFrom` handling, the pi adapter), an operator runs
`MOSAIC_LIVE_PROVIDER=1 bash scripts/test-task.sh` with the real pi
credential and a real `release.sh activate`, and records both results on
the re-pin row. Morgana's wording (row 244 BUILD.md), Webby N2 on row 244
r1 (#1757). (2026-10-21)
## Queue
Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
## Done
- Ledger (piece 3, #1506): shipped cd0aa5fb, Gate D written 2026-09-12 (18.9 human messages per closed issue).
- Relaunched seat shows the old last message (darkwing, Step 3 log, 2026-09-12): closed by #1512 relaunch activity, af4203ca. Filbert authored it; Darkwing and Dewey reviewed. The live board restart and Gate F remain separate.
- Sage launcher hook uncommitted (2026-09-12): closed by the agents/sage launch files commit, Dewey review R1 revise, R2 approve (`agents/dewey/work/sage-launch-review-2026-09-26.md`), launcher test now covers sage.
- Ledger counts T3 agent messages as human (2026-09-26, #1506): fixed in ef0020ad (Darkwing 6a, Filbert reviewed). `messageKind` now counts a T3 header as agent and the control-board sender as board. It changes no current count, because no source the ledger reads has a T3 header yet. That waits for the T3 thread source (Gate F).
- Control board checked the bind address but not Host or Origin (2026-09-26, #1507, found by Dewey): closed by the board guard commit. `foreignRequest` refuses a non-loopback Host, a wrong port, userinfo or a path in Host, and any foreign Origin on every route. Dewey authored it, and Rocko approved it (`agents/rocko/work/board-guard-review-2026-09-26.md`, 5a12f08e). Rocko's one low finding: URL normalization accepts a root slash, empty userinfo and an empty query in Host, so the notes' wording overstates what the raw-syntax check refuses. No bypass was found.
- Discord engine: leaked fake pi and the timeout gap in `busy` (2026-09-26, #1509): fixed in 3edb15eb (Darkwing 6b R2, Rocko approved, review ed5510a0). Tests stop the engine in `finally`. A turn pi never started now stops pi with exit 1 instead of guessing, and the unit restarts it. Known limit: see the open entry on wedge restarts.
- Combined concurrent test hangs (2026-09-14, #1509): closed on evidence from 3edb15eb. At default concurrency the union passed 406/406 three times on the R2 snapshot and 408/408 on the commit's index, where clean HEAD had hung four runs out of four. The inherited-intermittency entry from 2026-09-15 stays open until a later run shows it is the same cause.
- A nested `node --test` passes even when its tests fail (2026-09-26, #1508): closed. Queue A1 (34a72af9) clears NODE_TEST_CONTEXT on its own path. N13 fixes `test-foundation.sh` and `test-discord.sh`, each with a check that fails the suite if the clearing is lost. Darkwing wrote it (n13.patch 00868b2f) and Filbert approved it (e464be6c). Sage's grep on 2026-09-27 finds no other nested `node --test`: in scripts, only the two suites, `test-queue.sh` and `queue-commit.sh`, and in JS, only the queue test that covers the fix.
- SetSpark service accepted any string as a decision approver (2026-09-26, #1509): closed. Fixed in shared-signals cc74d92 (Rocko approved R2, lead decision 24). The operations seat (T3 12fe8cda) deployed it to VM 1022 at 00:23Z on 2026-09-27. The running `service.py` hash matches cc74d92, and `/healthz` returns 200 inside the network and through the public tunnel. The read-only README survey found one row breaking the rule (DEC-009, Proposed, version 1), one open request that isn't ready (DEC-009's), and no Accepted decision with more than 16 approvers. No rows changed. DEC-009 stays as stored (lead decisions 21 and 28). The service now refuses approvals on it, so nothing can go wrong silently. Side effect: `up -d` also recreated caddy, about 20 s of edge downtime for all hosts. That is for the shared-signals deploy procedure to fix, not Mosaic.
- Queue suite failed its live checks outside the canonical checkout
(2026-09-27, #1508): closed by Piece D, f539466f. `test-queue.sh` now
skips `verify` and `render --check` with a printed reason when the
toplevel isn't the recorded `canonicalRoot`. An index export passed
27/0, and the canonical checkout 29/0.
- Queue: an owner listed as a reviewer blocked the row (2026-09-27,
#1508): closed by row 31, bb2fa970. `add`, `set reviewers` and `assign`
now refuse the owner as a reviewer with exit 2 and write nothing. Rocko
built it in the Gate G run, and Filbert approved round 1 (#1508 comment
26629, 8 of 8 mutants caught).
- Queue follow-ups from rows 12, 13 and 31 (2026-10-04, #1508): closed by
row 33, 6fb50cc0. Filbert built it, and Darkwing approved round 1
(#1508 comment 26651).
- `genesis` refuses a map row whose owner is one of its reviewers (exit
2, nothing written). The check is on the map, not in replay, so a log
from before the rule still loads.
- The `assign` refusal now reads "can't assign row N to SEAT: SEAT is
one of its reviewers".
- `queue-commit.sh` compares HEAD with H before each canary and again if
the clean run fails. A moved HEAD exits 2 with "HEAD moved since H was
recorded"; the guard message is kept for a real guard failure.
- Every time and date the validator accepts must format back to itself,
so month 13, day 32, 2026-02-30, 2027-02-29 and 24:00 are refused.
The live log at rev 45 still replays.
- The cold-start failure (2026-09-27): not reproduced in 20 cold runs.
Each ran `node --test packages/queue/tests/` in a fresh tree, 10
shared clones and 10 exports, one after another. All 20 passed
148/148 (2960 of 2960). The 300 ms deadline test is unchanged. Counts:
`agents/filbert/work/queue-33/cold-runs.txt`.
- CHAT-03 I1 round 2 follow-ups (2026-10-04, #1507): four of five closed
by slice 1 S5 (row 40, #1522; Dewey authored, Darkwing and Filbert
reviewing). The commit is named in the row's review record when it
lands.
- Engine command and `preArgs`: `engine` takes only `extraArgs`, `cwd`
and `envKeys`. The controller always launches the pinned Pi and
checks that at construction and at bind. The fake engine comes in
through a symbol key that JSON config can't carry (N24b).
- Engine environment: an explicit list (`ENGINE_ENV` in `pi-pin.mjs`)
plus `*_API_KEY` and `*_TOKEN` names from `engine.envKeys`. Nothing
else is inherited. `HOME` passes, so Pi reads `~/.pi/agent` unless
`PI_CODING_AGENT_DIR` is set (Darkwing F3).
- `escalating` is cleared when the force-stop fence throws (Darkwing F2,
races.test.mjs).
- Takeover then Enter in one chunk: input after Ctrl-T or Ctrl-O waits
for that action, in the same chunk or a later one, and also while an
earlier action is still pending, so it is judged as if typed one key
at a time (Filbert F2; Darkwing and Filbert T1 on #1522;
flows.test.mjs).
The frozen-cgroup test (Filbert F1) stays open above.
- Gate runs mount the host's pi auth file (Webby N4 on the row 197 proof
driver, 059577886): closed by row 244 (#1757), landing e69260a3c. The
suites and the lane's `cleanEnv` export a 0600 fixture `PI_AUTH_FILE`,
`guardDocker` refuses a Compose run without one, and
`packages/cli/tests/gate-lane-auth.test.mjs` proves no gate step mounts
the host default. The real-model cases this moved off the gate are open
above.
## Row253 F5: legacy Case AS_USER supervision
Tracked as row 276, Gitea #1781 (sage-2331), owner Gander, reviewer Duckworth, after row253.
Brief: docs/plans/2026-10-22_case-sudo-legacy-followup.md, “Brief: supervised legacy AS_USER calls”.
case1-accept.sh's legacy timeout330 around AS_USER talk and its AS_USER precheck, Claude version, PM launches and Node version diagnostics predate the new socket metadata/probe helpers. sudo's pty monitor can move them to a separate session; the old outer timeout does not prove their descendants are killed. The C4 repair in row253 covers only its new config query and peer probe. These legacy paths need the reviewed supervisor or a header explicitly explaining why each remains unsupervised. The follow-up must preserve the 300-second talk/reply contract, concurrent F5 scan, diagnostics and secret-output refusals, with fake-only session-hop tests and Sage's slot. No host or live unit change is authorized.
Row 276 / #1781 also owns the case1 root-side capture/tools mutant counterparts (duckworth-0112 N1) and documenting the setuid-member cleanup refusal in the headers (N2). A member that changes uid and cannot be killed causes inner-cleanup refusal, never successful observation. N4 is addressed in row253 by allowing SID confirmation until the existing inner watchdog expires, instead of a separate 200 ms scheduling margin.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,76 @@
--- a/scripts/case2-unit.sh
+++ b/scripts/case2-unit.sh
@@ -163,5 +163,3 @@
- local pid="$1" raw rest
- if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
- [ -e "/proc/$pid/stat" ] && return 1
- return 3
- fi
+ local pid="$1" raw="" rest
+ # EOF is expected: stat has no NUL. Decide on content, not read's status.
+ IFS= read -r -d '' raw < "/proc/$pid/stat" 2>/dev/null || :
@@ -181,3 +179 @@
- local entry pid rc live until
- clock_cs || return 1
- until=$((CLOCK_CS + 25))
+ local entry pid rc live until=""
@@ -200,0 +197,2 @@
+ # Inventory pass 1 is outside the bounded confirmation/retry budget.
+ [ -n "$until" ] || until=$((CLOCK_CS + 25))
@@ -221 +219,2 @@
-for ((attempt=0; attempt<100; attempt++)); do
+# Allow session creation the whole inner budget, rather than a 200 ms margin.
+while :; do
--- a/scripts/case1-accept.sh
+++ b/scripts/case1-accept.sh
@@ -121,5 +121,3 @@
- local pid="$1" raw rest
- if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
- [ -e "/proc/$pid/stat" ] && return 1
- return 3
- fi
+ local pid="$1" raw="" rest
+ # EOF is expected: stat has no NUL. Decide on content, not read's status.
+ IFS= read -r -d '' raw < "/proc/$pid/stat" 2>/dev/null || :
@@ -139,3 +137 @@
- local entry pid rc live until
- clock_cs || return 1
- until=$((CLOCK_CS + 25))
+ local entry pid rc live until=""
@@ -158,0 +155,2 @@
+ # Inventory pass 1 is outside the bounded confirmation/retry budget.
+ [ -n "$until" ] || until=$((CLOCK_CS + 25))
@@ -179 +177,2 @@
-for ((attempt=0; attempt<100; attempt++)); do
+# Allow session creation the whole inner budget, rather than a 200 ms margin.
+while :; do
--- a/packages/cli/tests/case2-unit.test.mjs
+++ b/packages/cli/tests/case2-unit.test.mjs
@@ -236,0 +237,22 @@
+});
+
+test('row253 C4 cleanup budget starts after inventory pass one',()=>{
+ // A deterministic clock models a slow inventory without host-load timing.
+ // All process reads and signals are stand-ins; no real PID is signalled.
+ const source=read(UNIT);
+ const body=source.slice(source.indexOf('stat_fields() {'),source.indexOf('finish() {'));
+ const standins=`
+child=424242; sleep_bin=:; seen=0; clocks=0; kills=0
+stat_fields() {
+ seen=$((seen+1)); ST_STATE=R; ST_SESSION=0
+ [ "$seen" != 1 ] || ST_SESSION="$child"
+ return 0
+}
+clock_cs() { clocks=$((clocks+1)); CLOCK_CS=$((clocks*1000)); }
+kill() { kills=$((kills+1)); return 0; }
+clean_session; rc=$?
+printf '%s' "$kills"
+exit "$rc"
+`;
+ const r=spawnSync('/bin/bash',['-c',body+standins],{encoding:'utf8'});
+ assert.equal(r.status,0,r.stderr);assert.equal(r.stdout,'1');
--- a/docs/plans/DEFERRED.md
+++ b/docs/plans/DEFERRED.md
@@ -461,0 +462,2 @@
+
+Row 276 / #1781 also owns the case1 root-side capture/tools mutant counterparts (duckworth-0112 N1) and documenting the setuid-member cleanup refusal in the headers (N2). A member that changes uid and cannot be killed causes inner-cleanup refusal, never successful observation. N4 is addressed in row253 by allowing SID confirmation until the existing inner watchdog expires, instead of a separate 200 ms scheduling margin.
@@ -0,0 +1,9 @@
{
"scripts/case2-unit.sh": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"scripts/case1-accept.sh": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"packages/cli/tests/case-sudo-fixture.mjs": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
"packages/cli/tests/case2-unit.test.mjs": "1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44",
"packages/cli/tests/case1-accept.test.mjs": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901",
"packages/webui/README.md": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9",
"docs/plans/DEFERRED.md": "3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f"
}
@@ -0,0 +1,282 @@
[
{
"id": "M1",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "4de5294507816ddc4114c418a6bb7c197b3fefe92f19b3b18cb95384e23de594"
},
{
"id": "M2",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "caf88ee7ad50b89169a0f57e12b696fe3828238af2ef1008ba6b6016ea7b2528"
},
{
"id": "M3",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "4401886e41fdb4c27610198b66f12d0b3103c5422e0862e9cea30a6f67c93fde"
},
{
"id": "M4",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "ef9a92bb24c72832bdba4d782bf4662394ee05e06a12b750a23d488d9181cd1d"
},
{
"id": "M5",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "4ab80b9db782a97c935f732bec87ac3bb45b58bf9076bc9f9abb4970fef1cada"
},
{
"id": "M6",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "7064e1926c77aedb5453c7771cd42091a5e90fc546821ddbb75d39e6a87c5a31"
},
{
"id": "M7",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "aece2a211aefa1dc4cfb6a3540f922ee5e7f23de26ce974b7655f8fe1722ee16"
},
{
"id": "M8",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "076d4f8f73570a21ad512b12eb5f6f494cb246b2a9d071fea27cc3c126efca2b"
},
{
"id": "M9",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "244cee7128d3b6e380ff24ab9e31f6c456d5153dddf88bb4719028277f523276"
},
{
"id": "M10",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "0e7d369f34cd41708d7b5104113093ebac0296d08902193ced03a1a2bbb83567"
},
{
"id": "M11",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "84181be89249975dd48e1a5daa51eab4bae7b483ea8edf5d6c0cdf373459c046"
},
{
"id": "M12",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "7f8b45e467b197bc00fb740a6507ee85cae332e404e8ffdd8c749b4264239e97"
},
{
"id": "M13",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "85ec11381f4e6662ec1729d84c57022e2f573a2f4c2682250fa005803cdfd63c"
},
{
"id": "M14",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "131f78c490b5ec26142c0703be0b5341551dd9d154c13e519604215fe1240b67"
},
{
"id": "M15",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "077b2eb210c853a03152733f4aa99e450ef0fa17d28f97f8adb78229b02ef147"
},
{
"id": "M16",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "2439d201b902c13fe3903a2d936c8700f0124a430b7d9b2877d485e1f381d3fb"
},
{
"id": "M17",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "1be36e62095e97c64c0bdc5d50b4a3a2e6dd18f1c45cfe1da0fe66cbb8aebd61"
},
{
"id": "M18",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "f5e61c286c51e5a2b26acab22fc5a32c35add0cb8126dc25b5ce747677e34888"
},
{
"id": "M19",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "2f337c03acc60978f1850275a2c8ba6d1b2c74befeb8d73f94d2b477e43b8c56"
},
{
"id": "M20",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "99689c22c6eb73ff9d599ba7305da552357baa99145305ca400f083017bbe3b1"
},
{
"id": "M21",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "bff90f2b1f59086575f98ff96dbd51bacb0265342f595b4bee96cb228d67c030"
},
{
"id": "M22",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "7a8cb7ce47774ab80a55331fc831b7458487be7187b10128746321b375dc83ff"
},
{
"id": "M23",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "e6814801188a6eb98f705d93ac123e988e4daf43afbe96464451f925b1c98d1c"
},
{
"id": "M24",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "ea8d8875f0b4503a6cf4fb96bd88184af65a7f61d1236b9b49baacc748b0c741"
},
{
"id": "M25",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "cfed9261a03eafcdcfb903226b0ddb3fba779d1105e3b4c411b66c10e1672d11"
},
{
"id": "M26",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "1a245b5403b1b9b5f25dfb8cc8ee24acf99121ff063277d52c928ba18543f997"
},
{
"id": "M27",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "efaa027cf7ec3252a675be80219e40fa7b5a3317b28c21fe02363d23ec6e5323"
},
{
"id": "M28",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "bb734cc1a42c0853068f77154dddaaee8eeef5696f67de2e27d1a898409f3b44"
},
{
"id": "M29",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "a6f353aa419862a473eee28e41c5208b464a378ae23e14a73b62ed92bd36cd1f"
},
{
"id": "M30",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "d22cf0f492178b3071604abd550355573c28bf4c26f4a3582f6895710ad3d788"
},
{
"id": "M31",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "3e7450a19cf152653bf6e73be39ef9fb552ec7a656564ca35e44bc12f1586ef5"
},
{
"id": "M32",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "ff11fc1013d70d861643cfd70aa964422cb6a82fb8b4b959bae04703803838b9"
},
{
"id": "M33",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "1c2a58bef2369d8af90b65f9473dd7a112f484db983d8460e2524cb8a673f18b"
},
{
"id": "M34",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "13c1ab861017f3c3c65f50d34a64d7028482e3bc11a8e5908632ec6685416025"
},
{
"id": "M35",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "8f05c1a3a18c5294c63a5853317ce5a5066303145410039629697b5d15d99bfe"
},
{
"id": "M36",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "c1247a7cab2c24e0ab017c49f66797ed16149d3f52cfa41c0968f154261d0d00"
},
{
"id": "M37",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "f210b9c796322333849bc4897b9db3e44bd573fef24c4ba757151981b891f11b"
},
{
"id": "M38",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "736418014b96a09ebe82d996ff6f45b9eb1f3c1e0ada274c505f89fbbd732783"
},
{
"id": "M39",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "1486b8907be4f327bc17b64ddf615cd8b3db1193c185f7934bcba1c7ccc5ec31"
},
{
"id": "M40",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "2f654a76d887ca62525e01c307a2a8f828ee426087dc63a2d4df6a07dc525d53"
}
]
@@ -0,0 +1,142 @@
// Shared fake process model for the two Case host-script tests. No real sudo.
// Call case_sudo_run RECORD_DIR COMMAND... after the caller's argv checks.
// Missing session-mode preserves the original exec-style fake.
import { spawn } from 'node:child_process';
import { readFileSync, readdirSync } from 'node:fs';
export const CASE_SUDO_SESSION = String.raw`
case_sudo_entry() {
local record="$1" here sid; shift
here="$BASHPID"
read -r sid < <(/bin/ps -o sid= -p "$here")
printf "%s %s\n" "$here" "$sid" >> "$record/session-pids"
if { [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
{ [ -f "$record/stall-probe" ] && [[ "$*" == *SO_PEERCRED* ]]; }; then
trap '' TERM HUP
[ ! -f "$record/stall-output" ] || /bin/cat "$record/stall-output"
exec /bin/sleep 60
fi
exec "$@"
}
case_sudo_run() {
local record="$1" child="" rc=0 mode=exec; shift
[ ! -f "$record/session-mode" ] || mode="$(<"$record/session-mode")"
if [ "$mode" != hop ]; then case_sudo_entry "$record" "$@"; return "$?"; fi
# The fake stays as parent, just as sudo does on its pty path. KILL cannot
# be caught or forwarded. The child creates the separate terminal session.
trap '[ -z "$child" ] || kill -TERM -- "-$child" 2>/dev/null || :' TERM
trap '[ -z "$child" ] || kill -HUP -- "-$child" 2>/dev/null || :' HUP
trap '[ -z "$child" ] || kill -INT -- "-$child" 2>/dev/null || :' INT
export -f case_sudo_entry
/usr/bin/setsid /bin/bash -c 'case_sudo_entry "$@"' -- "$record" "$@" &
child=$!
# An interrupted wait returns while the child still runs; keep waiting.
while :; do
wait "$child"; rc=$?
kill -0 "$child" 2>/dev/null || break
done
return "$rc"
}
`;
export function liveRecordedSessions(recordFile, readFile, procDirs) {
// Exec fake entries share the test runner's session. Only self-led sessions
// are isolated and safe to scan here; exec coverage also checks exact PIDs.
const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number)).filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
const live=[];
for(const entry of procDirs()){
if(!/^\d+$/.test(entry))continue;
try{
const stat=readFile(`/proc/${entry}/stat`), fields=stat.slice(stat.lastIndexOf(') ')+2).split(' ');
if(sessions.has(Number(fields[3])) && fields[0]!=='Z') live.push(Number(entry));
}catch{}
}
return live;
}
export function recordedPids(file) {
try{return readFileSync(file,'utf8').trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number));}
catch(error){if(error.code==='ENOENT')return [];throw error;}
}
export function recordedLivePids(file) {
return sessionPairs(recordedPids(file)).filter(([pid,sid])=>{
try{
const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
return Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X';
}catch(error){if(error.code==='ENOENT'||error.code==='ESRCH')return false;throw error;}
}).map(([pid])=>pid);
}
export function killRecordedPids(file) {
killPairs(recordedPids(file));
}
function killPairs(pairs) {
for(const [pid,sid] of sessionPairs(pairs)){
try{
const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
if(Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X')process.kill(pid,'SIGKILL');
}catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
}
}
function sessionPairs(pairs) {
// Only self-led records authorize a session scan. Exec-mode records belong
// to the runner's session and authorize checking their exact PIDs only.
const sessions=new Set(pairs.filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
const found=new Map(pairs.map(pair=>[pair[0],pair]));
if(sessions.size)for(const entry of readdirSync('/proc')){
if(!/^\d+$/.test(entry))continue;
try{
const raw=readFileSync(`/proc/${entry}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' '),sid=Number(fields[3]);
if(sessions.has(sid))found.set(Number(entry),[Number(entry),sid]);
}catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
}
return [...found.values()];
}
// Safety net for a removed-outer-bound change. It is fixture cleanup, never
// product supervision. The guard reads only fixture records and checks sid
// immediately before every kill. Normal outer-bound cases return before it.
export function fixtureGuard(t,files,milliseconds=9500) {
const program=`const fs=require('node:fs');setTimeout(()=>{for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}},Number(process.argv[2]));`;
const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds)],{stdio:'ignore'});
const stop=async()=>{
if(child.exitCode!==null||child.signalCode!==null)return;
const exited=new Promise(resolve=>child.once('exit',resolve));
child.kill('SIGKILL');await exited;
};
t.after(stop);return stop;
}
export function cleanupFixture(t,files) {
let captured=[];
const capture=()=>{captured=files.flatMap(recordedPids);};
// tmp(t)'s directory-removal hook may run first. Retain PID/SID pairs in
// memory before assertions, then verify the kernel sid again before killing.
t.after(()=>killPairs([...captured,...files.flatMap(recordedPids)]));
return capture;
}
export async function pollFixtureGone(files) {
for(let n=0;n<50;n++){
if(files.every(file=>recordedLivePids(file).length===0))return [];
await new Promise(resolve=>setTimeout(resolve,10));
}
return files.flatMap(recordedLivePids);
}
export function blockingQuerySource(records,kind='signals',member,proof) {
const py=`import os,sys\nos.setpgid(0,0)\nwith open(sys.argv[2],"a") as f:f.write(str(os.getpid())+" "+str(os.getsid(0))+chr(10))\nos.execv(sys.argv[1],[sys.argv[1],"60"])`;
return `import fs from 'node:fs';import {spawn} from 'node:child_process';
const records=${JSON.stringify(records)};
function record(pid){const s=fs.readFileSync('/proc/'+pid+'/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);fs.appendFileSync(records,pid+' '+sid+String.fromCharCode(10));}
record(process.pid);
export function loadSystem(){
${kind==='sleep'?`const c=spawn('/usr/bin/sleep',['60'],{stdio:'inherit'});record(c.pid);`:''}
${kind==='signals'?`process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});`:''}
${kind==='member'?`const c=spawn('/usr/bin/python3',['-I','-S','-c',${JSON.stringify(py)},${JSON.stringify(member)},records],{stdio:'inherit'});for(let n=0;n<200;n++){try{const s=fs.readFileSync('/proc/'+c.pid+'/stat','utf8');if(s.includes('('+${JSON.stringify(member?.split('/').at(-1))}+') ')){fs.writeFileSync(${JSON.stringify(proof)},s);break;}}catch{}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,5);}`:''}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
}`;
}
@@ -0,0 +1,706 @@
#!/usr/bin/env bash
# Row 195 (#1693): the Case 1 acceptance on 10.1.1.53, design r7 section 5
# (agents/darkwing/work/queue-195/DESIGN.md). Deviations and decisions: agents/darkwing/work/queue-195/BUILD.md.
#
# case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N
#
# 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, main port and tester socket marker 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 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. A bounded config/target query imports the copy only as the stack
# user, with sudo and an empty environment. Root parses its two facts in bash. P10 uses the
# fixed system Python, also as the stack user, for a read-only SO_PEERCRED observation.
# The config/probe supervisor is fixed root-validated system Bash plus setsid
# and sleep, after the uid drop: query inner/outer 5/7 s, probe 2/4 s. Root
# independently bounds its sudo wait. Legacy AS_USER talk and diagnostics below
# retain their old behavior and are NOT covered by this supervisor (F5 follow-up).
#
# 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
# `unnamed`, which means no Q182, Q183 or Mos ruling names the path; each unnamed line goes to Mos.
#
# Exit 0 when every F check passes, 1 when one fails, 2 on usage or a refusal (a bad args file, or
# --root misused). A P check never changes the exit.
#
# Test seam, for packages/cli/tests/case1-accept.test.mjs only. Root takes none:
# --root DIR prefixes the args file, /etc, /proc, the stack's home, the host check and node, and
# reads the account database from DIR/etc/passwd and DIR/etc/group instead of getent
set -uo pipefail
usage() { echo "usage: case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N" >&2; exit 2; }
refuse() { echo "case1-accept: refusing: $*" >&2; exit 2; }
BUSINESS="" ARGS_FILE="" CLAUDE_PIN="" NODE_PIN="" ROOT=""
while [ $# -gt 0 ]; do
case "$1" in
--business) [ $# -ge 2 ] && [[ "$2" =~ ^[a-z0-9][a-z0-9-]*$ ]] || usage; BUSINESS="$2"; shift 2 ;;
--args) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] || usage; ARGS_FILE="$2"; shift 2 ;;
--claude-pin) [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; CLAUDE_PIN="$2"; shift 2 ;;
--node-pin) [ $# -ge 2 ] && [[ "$2" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; NODE_PIN="$2"; shift 2 ;;
--root) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [ -d "$2" ] || usage; ROOT="${2%/}"; shift 2 ;;
*) usage ;;
esac
done
[ -n "$BUSINESS" ] && [ -n "$ARGS_FILE" ] && [ -n "$CLAUDE_PIN" ] && [ -n "$NODE_PIN" ] || usage
if [ "$(id -u)" = 0 ]; then
[ -z "$ROOT" ] || refuse "--root is a test seam, and root takes none"
PATH=/usr/sbin:/usr/bin:/sbin:/bin
else
[ -n "$ROOT" ] || refuse "run it as root"
fi
cd / || refuse "cannot cd to /"
# --- The args file, by stack-upgrade.sh's read_args rules, plus the serve ports ---
f="$ROOT$ARGS_FILE"
[ -f "$f" ] && [ ! -L "$f" ] || refuse "no regular file $ARGS_FILE"
st="$(stat -c '%u %a' -- "$f")" || refuse "cannot stat $ARGS_FILE"
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) ||
refuse "$ARGS_FILE must be $(id -un)'s with no group or other write (it is uid ${st% *}, mode ${st#* })"
mapfile -t ARGS < "$f" || refuse "cannot read $ARGS_FILE"
S_USER="" S_HOME="" A_BUSINESS="" NODE="" COPY_ID="" CLAUDE="" PROFILE="" PORT="" TESTER_SOCKET=0
copies=0 past=""
for ((i = 0; i < ${#ARGS[@]}; i++)); do
a="${ARGS[i]}" v="${ARGS[i + 1]:-}"
if [ -n "$past" ]; then
case "$a" in
--port) PORT="$v"; i=$((i + 1)) ;;
--business) [ "$v" = "$BUSINESS" ] || refuse "serve business differs"; i=$((i + 1)) ;;
--tester-socket) TESTER_SOCKET=$((TESTER_SOCKET + 1)) ;;
--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;
*) refuse "unknown serve flag $a" ;;
esac
continue
fi
case "$a" in
--) past=1 ;;
--pm) ;;
--user) S_USER="$v"; i=$((i + 1)) ;;
--home) S_HOME="$v"; i=$((i + 1)) ;;
--business) A_BUSINESS="$v"; i=$((i + 1)) ;;
--node) NODE="$v"; i=$((i + 1)) ;;
--claude) CLAUDE="$v"; i=$((i + 1)) ;;
--profile) PROFILE="$v"; i=$((i + 1)) ;;
--copy) COPY_ID="$v"; copies=$((copies + 1)); i=$((i + 1)) ;;
*) refuse "$ARGS_FILE has $a before --, which case2-unit.sh does not take" ;;
esac
done
[ "$copies" = 1 ] || refuse "$ARGS_FILE needs exactly one --copy before --, not $copies"
[[ "$S_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] && [[ "$S_HOME" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$A_BUSINESS" =~ ^[a-z0-9][a-z0-9-]*$ ]] &&
[[ "$NODE" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] && [[ "$CLAUDE" =~ ^/[A-Za-z0-9/._+@-]+$ ]] ||
refuse "$ARGS_FILE lacks a plain --user, --home, --business, --node, --claude or --copy"
[ "$PROFILE" = factory ] || refuse "$ARGS_FILE has --profile '${PROFILE}', not factory"
[ "$A_BUSINESS" = "$BUSINESS" ] || refuse "$ARGS_FILE is for business $A_BUSINESS, not $BUSINESS"
[[ "$PORT" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$PORT" -le 1023 ] || refuse "$ARGS_FILE lacks a privileged numeric --port after --"
[ "$TESTER_SOCKET" = 1 ] || refuse "$ARGS_FILE needs one bare --tester-socket after --"
COPY="$S_HOME/.local/share/mosaic-stack/copies/$COPY_ID"
DATA_ROOT="$S_HOME/.mosaic-dev"
CRED="$S_HOME/.config/mosaic-dev/secrets/claude-session.env"
BUSINESS_FILE="$S_HOME/.config/mosaic-dev/businesses/$BUSINESS.json"
BUS_UNIT="mosaic-bus@$BUSINESS.service" CON_UNIT=mosaic-webui.service
HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh
SETTINGS=/etc/claude-code/managed-settings.json
LABEL_OUTER='bwrap (enforce)' LABEL_CHILD='bwrap//&unpriv_bwrap (enforce)'
AS_USER=(sudo -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin")
WORK="$(mktemp -d)" || refuse "cannot make a temp dir"
trap 'rm -rf -- "$WORK"' EXIT
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
# row253-supervisor-begin
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
stat_fields() {
local pid="$1" raw="" rest
# EOF is expected: stat has no NUL. Decide on content, not read's status.
IFS= read -r -d '' raw < "/proc/$pid/stat" 2>/dev/null || :
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
[[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
}
clock_cs() {
local boot spare
read -r boot spare < /proc/uptime || return 1
[[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
}
clean_session() {
local entry pid rc live until=""
while :; do
live=0
for entry in /proc/[0-9]*/stat; do
pid="${entry%/stat}"; pid="${pid##*/}"
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" != 3 ] || continue
[ "$rc" = 0 ] || return 1
[ "$ST_SESSION" = "$child" ] || continue
case "$ST_STATE" in Z|X) continue ;; esac
live=1
if ! kill -KILL "$pid" 2>/dev/null; then
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
fi
done
[ "$live" != 0 ] || return 0
clock_cs || return 1
# Inventory pass 1 is outside the bounded confirmation/retry budget.
[ -n "$until" ] || until=$((CLOCK_CS + 25))
[ "$CLOCK_CS" -lt "$until" ] || return 1
"$sleep_bin" 0.01
done
}
finish() {
local rc="$?"
trap - EXIT HUP INT TERM
if [ "$verified" = 1 ]; then
clean_session || rc=86
elif [ -n "$child" ]; then
kill -KILL "$child" 2>/dev/null || :
fi
[ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
"$setsid_bin" "$@" & child=$!
# Allow session creation the whole inner budget, rather than a 200 ms margin.
while :; do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 85
if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
tester_tools() {
local input target p st magic
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
p="$target"
while :; do
st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
[ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
[ "$p" != / ] || break
p="${p%/*}"; [ -n "$p" ] || p=/
done
magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
case "$input" in
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
/usr/bin/head) TS_HEAD="$target" ;;
esac
done
}
tester_capture_reason() {
case "$1" in
81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
*) echo inner-command ;;
esac
}
tester_capture() (
local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
tester_tools || exit 84
file="$(mktemp)" || exit 88
notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
trap 'exit 88' HUP INT TERM
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
set -o pipefail
maximum="$1"; head_bin="$2"; shift 2
"$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
# Only GNU timeout and the trusted head can write this root-side channel.
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
[ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
[ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
[ "${5:-}" != observe ] || return 0
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# --- Output ---
MARKS=(pass FAIL closed "accepted Q182+Q183" "accepted Q183" "accepted mos-1096" unnamed)
declare -A COUNT=()
F_FAILED=0
# Text the stack's user wrote, made safe to print: printable ASCII, one line, at most 200 bytes.
clean() { tr -c '[:print:]' ' ' <<< "$1" | cut -c1-200 | sed 's/ *$//'; }
line() { local id="$1" path="$2" mark="$3"; shift 3; echo "$id $path $mark $*"; COUNT[$mark]=$(( ${COUNT[$mark]:-0} + 1 )); }
fpass() { line "$1" "$2" pass "${@:3}"; }
ffail() { line "$1" "$2" FAIL "${@:3}"; F_FAILED=1; }
# --- The account database: getent on the host, the seam's files under --root ---
db() {
if [ -n "$ROOT" ]; then
if [ $# -gt 1 ]; then awk -F: -v k="$2" '$1 == k' "$ROOT/etc/$1"; else cat "$ROOT/etc/$1"; fi
else
getent "$@"
fi
}
uid_of() { db passwd "$1" | cut -d: -f3; }
gid_of() { db passwd "$1" | cut -d: -f4; }
# The group ids USER belongs to, as `id -G USER` prints them: the primary gid, then each group
# whose member list names USER.
groups_of() {
local g; g="$(gid_of "$1")"
[ -n "$g" ] || return 0
{ echo "$g"; db group | awk -F: -v u="$1" '{ n = split($4, m, ","); for (i = 1; i <= n; i++) if (m[i] == u) print $3 }'; } | awk '!seen[$0]++'
}
names_of_groups() { local g; for g in "$@"; do db group | awk -F: -v g="$g" '$3 == g { print $1 }'; done; }
MOSAIC_UID="$(uid_of "$S_USER")"
[[ "$MOSAIC_UID" =~ ^[1-9][0-9]*$ ]] || refuse "stack account has no non-root uid"
[ "$(db passwd "$S_USER" | cut -d: -f6)" = "$S_HOME" ] || refuse "saved home differs from account home"
QUERY="$(tester_capture 5 7 4096 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query $(tester_capture_reason "$?")"
tester_facts "$QUERY" || refuse "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || refuse "target does not support --tester-socket"
tester_location "$TS_DATA" "$S_HOME" "$MOSAIC_UID" "$ROOT" observe || refuse "tester path refuses $TS_REASON"
TESTER_PATH="$TS_SOCKET"
# --- systemd, /proc and listeners ---
# KEY=VALUE lines for UNIT's properties.
unit_show() { local u="$1"; shift; systemctl show "$u" "${@/#/-p}" 2>/dev/null; }
prop() { sed -n "s/^$2=//p" <<< "$1" | tail -n 1; }
# The real, effective, saved and filesystem uids of PID, space-separated.
proc_uids() { awk '$1 == "Uid:" { print $2, $3, $4, $5 }' "$ROOT/proc/$1/status" 2>/dev/null; }
proc_label() { local l; l="$(cat "$ROOT/proc/$1/attr/current" 2>/dev/null)" || return 1; printf '%s' "${l%$'\n'}"; }
# One `ADDRESS PIDS` line per TCP listener on port $1, PIDS comma-separated.
listeners() {
ss -Hltnp 2>/dev/null | awk -v p="$1" '{
n = split($4, a, ":"); if (a[n] != p) next
addr = substr($4, 1, length($4) - length(p) - 1); pids = ""; s = $0
while (match(s, /pid=[0-9]+/)) { pids = pids (pids == "" ? "" : ",") substr(s, RSTART + 4, RLENGTH - 4); s = substr(s, RSTART + RLENGTH) }
print addr, pids
}'
}
# 0 when port $1 has a listener and every one is on 127.0.0.1, held by pid $2 alone.
loopback_by() {
local addr pids p any=0
[ -n "$2" ] && [ "$2" != 0 ] || return 1
while read -r addr pids; do
[ -n "$addr" ] || continue
any=1
[ "$addr" = 127.0.0.1 ] || return 1
for p in ${pids//,/ }; do [ "$p" = "$2" ] || return 1; done
[ -n "$pids" ] || return 1
done <<< "$(listeners "$1")"
[ "$any" = 1 ]
}
listener_text() { local t; t="$(listeners "$1" | tr '\n' ';')"; echo "${t:-none}"; }
BUS_SHOW="$(unit_show "$BUS_UNIT" ActiveState User MainPID)"
CON_SHOW="$(unit_show "$CON_UNIT" ActiveState User MainPID)"
CON_PID="$(prop "$CON_SHOW" MainPID)"
# --- F checks ---
check_f1() {
local u show state user pid uids why=""
for u in "$BUS_UNIT" "$CON_UNIT"; do
if [ "$u" = "$BUS_UNIT" ]; then show="$BUS_SHOW"; else show="$CON_SHOW"; fi
state="$(prop "$show" ActiveState)" user="$(prop "$show" User)" pid="$(prop "$show" MainPID)"
uids="$(proc_uids "${pid:-0}")"
[ "$state" = active ] || { why+="$u is ${state:-unknown}; "; continue; }
[ "$user" = "$S_USER" ] || { why+="$u has User=${user:-unset}; "; continue; }
[ -n "$MOSAIC_UID" ] && [ "$uids" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] ||
why+="$u MainPID ${pid:-none} has uids '${uids:-unreadable}', not $S_USER's ${MOSAIC_UID:-unknown}; "
done
if [ -z "$why" ]; then fpass F1 units-user "$BUS_UNIT and $CON_UNIT are active as $S_USER (uid $MOSAIC_UID)"
else ffail F1 units-user "${why%; }"; fi
}
check_f2() {
local rc=0
curl -fsS --max-time 10 -o /dev/null "http://127.0.0.1:$PORT/healthz" 2>/dev/null || rc=$?
if [ "$rc" != 0 ]; then ffail F2 console-loopback "curl http://127.0.0.1:$PORT/healthz exited $rc"
elif ! loopback_by "$PORT" "$CON_PID"; then ffail F2 console-loopback "port $PORT listeners are '$(listener_text "$PORT")', not 127.0.0.1 by the Console's MainPID ${CON_PID:-none}"
else fpass F2 console-loopback "healthz answers, and $PORT is bound to 127.0.0.1 only, by MainPID $CON_PID"; fi
}
# F5's scan: an outer /usr/bin/bwrap owned by the stack's user with a claude descendant. Sets F5_OK
# when one pair has both labels exactly, F5_WHY to the last mismatch otherwise.
F5_OK="" F5_WHY="no /usr/bin/bwrap process of $S_USER with a claude descendant was seen while the reply was pending"
scan_f5() {
local pid ppid uids kids q c argv0 comm lo lc
local -A parent=() children=() bw=()
# One read of every PPid line and one of every exe link, so a scan stays short on a busy host.
while read -r pid ppid; do
parent[$pid]="$ppid" children[$ppid]+=" $pid"
done < <(grep -H '^PPid:' "$ROOT"/proc/[0-9]*/status 2>/dev/null | sed -n 's#^.*/proc/\([0-9]*\)/status:PPid:[[:space:]]*\([0-9]*\)$#\1 \2#p')
while read -r pid; do
[[ "$pid" =~ ^[0-9]+$ ]] && [ -n "${parent[$pid]:-}" ] && bw[$pid]=1
done < <(find "$ROOT/proc" -mindepth 2 -maxdepth 2 -name exe -lname /usr/bin/bwrap -printf '%h\n' 2>/dev/null | sed 's#.*/##')
for pid in "${!bw[@]}"; do
[ -z "${bw[${parent[$pid]}]:-}" ] || continue
uids="$(proc_uids "$pid")"
[ -n "$MOSAIC_UID" ] && [ "${uids%% *}" = "$MOSAIC_UID" ] || continue
# Every descendant, breadth first.
kids=() q=("$pid")
while [ ${#q[@]} -gt 0 ]; do
for c in ${children[${q[0]}]:-}; do kids+=("$c") q+=("$c"); done
q=("${q[@]:1}")
done
for c in "${kids[@]}"; do
argv0="$(tr '\0' '\n' < "$ROOT/proc/$c/cmdline" 2>/dev/null | sed -n 1p)"
comm="$(cat "$ROOT/proc/$c/comm" 2>/dev/null)"
[[ "${argv0##*/}" == claude* ]] || [[ "$comm" == claude* ]] || continue
lo="$(proc_label "$pid")" lc="$(proc_label "$c")"
if [ "$lo" = "$LABEL_OUTER" ] && [ "$lc" = "$LABEL_CHILD" ]; then
F5_OK="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc'"; return 0
fi
F5_WHY="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc', not '$LABEL_OUTER' and '$LABEL_CHILD'"
done
done
return 1
}
# F3 and F5 together: the message goes out in the background and the scan runs until it is answered.
check_f3_f5() {
local marker text rc=0 out err n tpid
marker="case1-accept-$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"
text="$marker: this is the Case 1 acceptance; reply with one short line."
timeout 330 "${AS_USER[@]}" "$COPY/scripts/mosaic" talk chief "$text" --business "$BUSINESS" --tester --wait 300 > "$WORK/talk.out" 2> "$WORK/talk.err" &
tpid=$!
while :; do
scan_f5 && break
kill -0 "$tpid" 2>/dev/null || break
sleep 0.5
done
wait "$tpid" || rc=$?
# Both the sent line and the reply are on stdout; an error is on stderr.
out="$(cat "$WORK/talk.out")" err="$(cat "$WORK/talk.err")"
n="$(grep -c . <<< "$out")"
if [ "$rc" = 0 ] && grep -q "^sent [^ ]* to chief" <<< "$out" && [ "$n" -ge 2 ]; then
fpass F3 tester-reply "chief replied to $marker"
else
ffail F3 tester-reply "talk exited $rc with $n lines on stdout; last: $(clean "$(tail -n 1 <<< "${err:-$out}")")"
fi
if [ -n "$F5_OK" ]; then F5_LINE=(fpass "$F5_OK"); else F5_LINE=(ffail "$F5_WHY"); fi
}
check_f4() {
local rc=0 out
out="$("${AS_USER[@]}" "$COPY/scripts/case2-precheck.sh" --business "$BUSINESS" --profile factory 2>&1)" || rc=$?
if [ "$rc" = 0 ]; then fpass F4 precheck "case2-precheck.sh --profile factory exits 0"
else ffail F4 precheck "case2-precheck.sh --profile factory exited $rc; last: $(clean "$(tail -n 1 <<< "$out")")"; fi
}
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
}
# The PM check confirm runs (stack-release.sh pm_external): exactly one live launches list entry for
# launch.by, for this business. The stack's user runs the whole check and prints one result line.
F7_JS='
const {readFileSync}=require("node:fs");const {spawnSync}=require("node:child_process");
const [file,cli,b]=process.argv.slice(1);
let by;
try{by=JSON.parse(readFileSync(file,"utf8")).launch?.by}catch(e){console.log(`fail cannot read ${file}: ${e.code??"not JSON"}`);process.exit(0)}
if(typeof by!=="string"||!by){console.log("fail the business file has no launch.by");process.exit(0)}
const r=spawnSync(cli,["launches","list","--json"],{encoding:"utf8"});
if(r.status!==0){console.log(`fail launches list exited ${r.status}`);process.exit(0)}
let l;try{l=JSON.parse(r.stdout)}catch{console.log("fail launches list printed no JSON");process.exit(0)}
const live=Array.isArray(l)?l.filter((x)=>x&&x.instance===by&&x.live===true):[];
console.log(live.length!==1?`fail ${live.length} live launches of ${by}`:live[0].business!==b?`fail the live launch of ${by} is for ${live[0].business}`:`ok ${by} run ${live[0].run}`);'
check_f7() {
local out
out="$("${AS_USER[@]}" "$NODE" -e "$F7_JS" -- "$BUSINESS_FILE" "$COPY/scripts/mosaic" "$BUSINESS" 2>/dev/null | sed -n 1p)"
if [[ "$out" =~ ^ok\ [a-z0-9][a-z0-9-]*\ run\ [A-Za-z0-9._-]+$ ]]; then fpass F7 pm-live "one live launch: ${out#ok }"
else ffail F7 pm-live "$(clean "${out#fail }")"; fi
}
check_f8() {
local s
s="$(stat -c '%U %a %F' -- "$ROOT$CRED" 2>/dev/null)" || s="missing"
if [ ! -L "$ROOT$CRED" ] && [ "$s" = "$S_USER 600 regular file" ]; then fpass F8 credential-file "$CRED is '$s'"
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 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
}
check_f10() {
local want="/opt/mosaic-stack/node-$NODE_PIN/bin/node" v
[ "$NODE" = "$want" ] || { ffail F10 node-pin "--node $NODE is not $want"; return; }
v="$("${AS_USER[@]}" "$NODE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$NODE_PIN" ]; then fpass F10 node-pin "$NODE prints $v"
else ffail F10 node-pin "$NODE --version prints '$(clean "$v")', not $NODE_PIN"; fi
}
check_f11() {
local d="$ROOT$DATA_ROOT/discord" found
if [ -L "$d" ]; then ffail F11 no-discord "$DATA_ROOT/discord is a symlink"; return; fi
[ -d "$d" ] || { fpass F11 no-discord "$DATA_ROOT/discord is absent"; return; }
found="$(find "$d" -maxdepth 1 -name '*.json' -printf '%f ' 2>/dev/null)"
if [ -z "$found" ]; then fpass F11 no-discord "$DATA_ROOT/discord holds no binding"
else ffail F11 no-discord "$DATA_ROOT/discord holds bindings: ${found% }"; fi
}
# --- P checks ---
check_p1() {
local out
out="$(sudo -l -U "$S_USER" 2>&1)"
if grep -q "is not allowed to run sudo" <<< "$out"; then line P1 "$S_USER-sudo" closed "sudo -l: $(clean "$(sed -n 1p <<< "$out")")"
else line P1 "$S_USER-sudo" unnamed "sudo -l: $(clean "$(tr '\n' ' ' <<< "$out")")"; fi
}
check_p2() {
local gs keys="$ROOT$S_HOME/.ssh/authorized_keys" kstate n
gs="$(groups_of "$S_USER" | tr '\n' ' ')"; gs="${gs% }"
n="$(wc -w <<< "$gs")"
if [ ! -e "$keys" ] && [ ! -L "$keys" ]; then kstate=absent
elif [ -f "$keys" ] && [ ! -L "$keys" ] && [ ! -s "$keys" ]; then kstate=empty
else kstate=present; fi
if [ "$n" = 1 ] && [ "$kstate" != present ]; then line P2 "$S_USER-groups-keys" closed "id -G is '$gs'; authorized_keys is $kstate"
else line P2 "$S_USER-groups-keys" unnamed "id -G is '$gs'; authorized_keys is $kstate"; fi
}
check_p3() {
local n
n="$(sudo -l -U localadmin 2>&1 | grep -c NOPASSWD)"
line P3 localadmin-sudo "accepted Q183" "sudo -l -U localadmin lists $n NOPASSWD lines"
}
check_p4() { line P4 pve-root-pbs "accepted Q182+Q183" "root on pve over VM 1008, and PBS backups that hold $S_HOME with the credential; this script can't see pve"; }
check_p5() {
local gs reach
if [ -z "$(uid_of jarvis)" ]; then line P5 jarvis-reach closed "no jarvis account"; return; fi
gs="$(names_of_groups $(groups_of jarvis) | tr '\n' ' ')"
reach="$(tr ' ' '\n' <<< "$gs" | grep -xE 'sudo|docker|lxd' | tr '\n' ' ')"
if [ -z "$reach" ]; then line P5 jarvis-reach closed "jarvis is in none of sudo, docker and lxd (groups: ${gs% })"
else line P5 jarvis-reach unnamed "jarvis is in ${reach% }"; fi
}
check_p6() {
local g m members bad=0
for g in sudo docker lxd; do
members="$(db group "$g" | cut -d: -f4)"
for m in ${members//,/ }; do
[ "$g" = sudo ] && [ "$m" = localadmin ] && continue
line P6 "group-$g" unnamed "$m is a member of $g"; bad=1
done
done
[ "$bad" = 1 ] || line P6 group-members closed "sudo holds only localadmin, and docker and lxd have no members"
}
check_p7() {
local found
found="$(grep -rlw -- "$S_USER" "$ROOT/etc/sudoers.d" 2>/dev/null | sed "s#^$ROOT##" | tr '\n' ' ')"
if [ -z "$found" ]; then line P7 sudoers-d closed "no file under /etc/sudoers.d names $S_USER"
else line P7 sudoers-d unnamed "files naming $S_USER: ${found% }"; fi
}
check_p8() {
local other
other="$(listeners 7331)$(listeners 7333)"
if loopback_by "$PORT" "$CON_PID" && [ -z "$other" ]; then
line P8 lan-listeners closed "$PORT is on 127.0.0.1 only by MainPID $CON_PID; no TCP listener on 7331 or 7333; tester uses $TESTER_PATH"
else
line P8 lan-listeners unnamed "$PORT: $(listener_text "$PORT"); 7333: $(listener_text 7333); 7331: $(listener_text 7331); Console MainPID ${CON_PID:-none}"
fi
}
check_p9() {
local out n missing=() rc=0 err mg others
out="$("$ROOT$HOSTCHECK" --check 2>/dev/null)"
for n in 1 2 3 4 5 6; do grep -q "^ok $n " <<< "$out" || missing+=("hostcheck item $n has no ok line")
done
err="$(sudo -u jarvis /usr/bin/bwrap --version 2>&1 >/dev/null)" || rc=$?
[ "$rc" != 0 ] && grep -q "Permission denied" <<< "$err" || missing+=("sudo -u jarvis /usr/bin/bwrap --version exited $rc without Permission denied")
mg="$(gid_of "$S_USER")"
[ -n "$mg" ] && [ -z "$(db group "$S_USER" | cut -d: -f4)" ] || missing+=("group $S_USER lists members, or $S_USER has no group")
others="$(db passwd | awk -F: -v g="$mg" -v u="$S_USER" '$4 == g && $1 != u { print $1 }' | tr '\n' ' ')"
[ -z "$others" ] || missing+=("other accounts have $S_USER's gid $mg: ${others% }")
if [ ${#missing[@]} = 0 ]; then
line P9 bwrap-userns "accepted mos-1096" "hostcheck items 1-6 ok, jarvis is denied bwrap, and group $S_USER has no other member"
else
for n in "${missing[@]}"; do line P9 bwrap-userns unnamed "$n"; done
fi
}
# P10 is an observation of mutable same-uid state, never a closure ruling.
TESTER_PEER='import socket,struct,sys
s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
try:
s.settimeout(1)
s.connect(sys.argv[1])
p=struct.unpack("3i",s.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
print(*p)
finally:
s.close()'
tester_snapshot() {
local parent="${TESTER_PATH%/*}" p s pid
[ ! -L "$ROOT$parent" ] && [ ! -L "$ROOT$TESTER_PATH" ] || return 1
p="$(stat -c '%u %a %F %d %i' -- "$ROOT$parent" 2>/dev/null)" || return 1
s="$(stat -c '%u %a %F %d %i' -- "$ROOT$TESTER_PATH" 2>/dev/null)" || return 1
[[ "$p" =~ ^$MOSAIC_UID\ 700\ directory\ [0-9]+\ [0-9]+$ ]] || return 1
[[ "$s" =~ ^$MOSAIC_UID\ 600\ socket\ [0-9]+\ [0-9]+$ ]] || return 1
pid="$(prop "$(unit_show "$CON_UNIT" MainPID)" MainPID)"
[[ "$pid" =~ ^[1-9][0-9]*$ ]] && [ "$pid" = "$CON_PID" ] || return 1
[ "$(proc_uids "$pid")" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] || return 1
printf '%s|%s|%s' "$pid" "$p" "$s"
}
tester_python() {
local real bad magic
# Resolve the system entry, then validate the target and every ancestor.
[ -f "$ROOT/usr/bin/python3" ] && [ -x "$ROOT/usr/bin/python3" ] || return 1
real="$(realpath -e -- "$ROOT/usr/bin/python3" 2>/dev/null)" || return 1
[ -z "$ROOT" ] || real="${real#"$ROOT"}"
case "$real" in /home/*|/root/*|/run/user/*|"$S_HOME"/*) return 1 ;; esac
bad="$(not_root_only "$real")"; [ -z "$bad" ] || return 1
bad="$(not_root_only /usr/bin)"; [ -z "$bad" ] || return 1
magic="$(od -An -tx1 -N4 -- "$ROOT$real" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
printf '%s' "$real"
}
check_p10() {
local floor before after python peer pid uid gid why="" pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})$'
floor="$(cat "$ROOT/proc/sys/net/ipv4/ip_unprivileged_port_start" 2>/dev/null)"
[[ "$floor" =~ ^[0-9]{1,5}$ ]] && [ "$floor" -gt "$PORT" ] || why=floor
before="$(tester_snapshot)" || why=metadata
python="$(tester_python)" || why=interpreter
if [ -z "$why" ]; then
peer="$(tester_capture 2 4 128 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why="probe-$(tester_capture_reason "$?")"
if [[ "$peer" =~ $pattern ]]; then
pid="${BASH_REMATCH[1]}" uid="${BASH_REMATCH[2]}" gid="${BASH_REMATCH[3]}"
[ "$pid" = "$CON_PID" ] && [ "$uid" = "$MOSAIC_UID" ] && [ "$gid" -le 2147483647 ] || why=peer
else [ -n "$why" ] || why=probe; fi
after="$(tester_snapshot)" || why=metadata
[ "$before" = "$after" ] || why=changed
fi
if [ -z "$why" ]; then
line P10 tester-socket-squat unnamed "boundary=observed path=$TESTER_PATH parent=0700 owner=$MOSAIC_UID socket=0600 peer_pid=$pid peer_uid=$uid floor=$floor port=$PORT; hostile same-uid substitution remains"
else
line P10 tester-socket-squat unnamed "boundary=failed reason=$why path=$TESTER_PATH MainPID=${CON_PID:-none}; hostile same-uid substitution remains"
fi
}
check_p11() {
local en before
en="$(systemctl is-enabled mosaic-hostcheck.service 2>/dev/null)"
before="$(prop "$(unit_show mosaic-hostcheck.service Before)" Before)"
if [ "$en" = enabled ] && grep -qxF -- "$BUS_UNIT" <<< "${before// /$'\n'}"; then
line P11 hostcheck-before-bus closed "mosaic-hostcheck.service is enabled, and its Before= names $BUS_UNIT"
else
line P11 hostcheck-before-bus unnamed "mosaic-hostcheck.service is '${en:-unknown}', Before='$before'; it needs enabled and $BUS_UNIT"
fi
}
check_f1
check_f2
F5_LINE=()
check_f3_f5
check_f4
"${F5_LINE[0]}" F5 bwrap-label "${F5_LINE[@]:1}"
check_f6
check_f7
check_f8
check_f9
check_f10
check_f11
check_p1; check_p2; check_p3; check_p4; check_p5; check_p6
check_p7; check_p8; check_p9; check_p10; check_p11
for m in "${MARKS[@]}"; do echo "count $m ${COUNT[$m]:-0}"; done
if [ "$F_FAILED" = 0 ]; then echo "case1-accept: every F check passes; send the whole output to Duckworth, and each unnamed line to Mos"; exit 0; fi
echo "case1-accept: an F check failed, so the row fails"
exit 1
@@ -0,0 +1,717 @@
// Row 195 (#1693): scripts/case1-accept.sh, root's acceptance run on the Case 1 host. Design r7 section 5
// (agents/darkwing/work/queue-195/DESIGN.md); deviations in agents/darkwing/work/queue-195/BUILD.md.
// Every case runs the script with --root on a scratch host tree: /etc, /proc, /srv/mosaic, /opt and the
// host check live under it. Fake systemctl, curl, ss, sudo and stat on PATH record their argv; the fake
// sudo runs the stack's user's commands from the tree. Nothing here reads or writes the real /etc,
// /proc or /srv, and the stand-in credential is not a credential.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
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";
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const SCRIPT = join(REPO, "scripts", "case1-accept.sh");
const COPY = "0123456789abcdef0123456789abcdef01234567";
const NODE_PIN = "v24.1.0", CLAUDE_PIN = "2.1.0";
const NODE = `/opt/mosaic-stack/node-${NODE_PIN}/bin/node`;
const CLAUDE = `/opt/mosaic-stack/claude-${CLAUDE_PIN}/node_modules/@anthropic-ai/claude-code/bin/claude.exe`;
const HOME = "/srv/mosaic";
const CLI = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/mosaic`;
const PRECHECK = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/case2-precheck.sh`;
const CRED = `${HOME}/.config/mosaic-dev/secrets/claude-session.env`;
const CRED_VALUE = "stand-in-not-a-credential-7f3e2a";
const AS_MOSAIC = `-u mosaic -H env PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin`;
const ARGS = ["--profile", "factory", "--user", "mosaic", "--home", HOME, "--business", "mosaic-stack", "--copy", COPY,
"--node", NODE, "--claude", CLAUDE, "--pm", "--", "--port", "732", "--business", "mosaic-stack", "--tester-socket"];
const OUTER = "bwrap (enforce)", CHILD = "bwrap//&unpriv_bwrap (enforce)";
const MARKS = ["pass", "FAIL", "closed", "accepted Q182+Q183", "accepted Q183", "accepted mos-1096", "unnamed"];
const P_PASS = { P1: "closed", P2: "closed", P3: "accepted Q183", P4: "accepted Q182+Q183", P5: "closed", P6: "closed",
P7: "closed", P8: "closed", P9: "accepted mos-1096", P10: "unnamed", P11: "closed" };
const PASSWD = "root:x:0:0:root:/root:/bin/bash\nmosaic:x:997:997::/srv/mosaic:/usr/sbin/nologin\nlocaladmin:x:1000:1000::/home/localadmin:/bin/bash\njarvis:x:1001:1001::/home/jarvis:/bin/bash\n";
const GROUP = "root:x:0:\nsudo:x:27:localadmin\nmosaic:x:997:\nlxd:x:998:\ndocker:x:999:\nlocaladmin:x:1000:\njarvis:x:1001:\n";
const SS = "LISTEN 0 511 127.0.0.1:732 0.0.0.0:* users:((\"node\",pid=200,fd=21))\n"
+ "LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:((\"sshd\",pid=900,fd=3))\n";
const HOSTCHECK_OK = [1, 2, 3, 4, 5, 6, 7].map((n) => `ok ${n} item ${n} holds`).join("\n") + "\nnote bubblewrap 0.9.0 is the install pin\n";
function put(file, text, mode) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, text);
if (mode) chmodSync(file, mode);
}
const edit = (file, f) => writeFileSync(file, f(readFileSync(file, "utf8")));
const log = (name) => `printf '%s' ${JSON.stringify(name)} >> "$HC/calls.log"; printf ' %s' "$@" >> "$HC/calls.log"; echo >> "$HC/calls.log"`;
// Fakes on PATH. Each records its argv and answers from a file in the state directory.
const FAKES = {
systemctl: `case "$1" in
show) [ -f "$HC/show-$2" ] && cat "$HC/show-$2" ;;
is-enabled) cat "$HC/enabled-$2" 2>/dev/null || { echo disabled; exit 1; } ;;
esac`,
curl: `exit "$(cat "$HC/curl-rc")"`,
ss: `cat "$HC/ss"`,
od: `if [ "$1" = -An ] && [ "$2" = -tx1 ] && [ "$3" = -N4 ]; then
[ -f "$HC/elf" ] && cat "$HC/elf" || echo 7f454c46
else exec /usr/bin/od "$@"; fi`,
// The credential and the settings file answer from the state directory, as root would see them, and
// only those calls are recorded; everything else is the real stat, which reads the args file.
stat: `case "\${@: -1}" in
*claude-session.env) ${log("stat")}; cat "$HC/stat-cred" ;;
*console/tester.sock) cat "$HC/stat-socket" ;;
*console) [ "$2" = '%u %a %F %d %i' ] && cat "$HC/stat-parent" || echo "997 700" ;;
*managed-settings.json) ${log("stat")}; cat "$HC/stat-settings" ;;
*) exec /usr/bin/stat "$@" ;;
esac`,
// -l -U USER prints the state file; -u jarvis is the bwrap probe; -u mosaic -H env PATH=... CMD runs
// CMD from the tree, with the PATH entries and each absolute argument that exists there moved under it.
sudo: `${CASE_SUDO_SESSION}
case "$1" in
-l) [ -f "$HC/sudo-l-$3" ] && cat "$HC/sudo-l-$3"; exit 0 ;;
-u) ;;
*) exit 97 ;;
esac
user="$2"; shift 2
if [ "$user" = jarvis ]; then cat "$HC/jarvis-bwrap" >&2; exit "$(cat "$HC/jarvis-bwrap-rc")"; fi
[ "$user" = mosaic ] && [ "$1" = -H ] && [ "$2" = env ] || exit 98
shift 2
if [ "$1" = -i ]; then
[ "$2" = HOME=/srv/mosaic ] || exit 98
shift 2
fi
[[ "$1" == PATH=* ]] || exit 98
p="\${1#PATH=}"; shift
np=""; IFS=: read -ra parts <<< "$p"; for x in "\${parts[@]}"; do np+="$ROOT_DIR$x:"; done
cmd="$1"; shift
args=(); for a in "$@"; do if [[ "$a" == /* ]] && [ -e "$ROOT_DIR$a" ]; then args+=("$ROOT_DIR$a"); else args+=("$a"); fi; done
export HOME="$ROOT_DIR/srv/mosaic" PATH="$np$PATH"
[ ! -e "$ROOT_DIR$cmd" ] || cmd="$ROOT_DIR$cmd"
case_sudo_run "$HC" "$cmd" "\${args[@]}"`,
};
// 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" "$@"`;
// The real Python payload runs against a mock socket object, never an OS listener.
const PYTHON_STUB = `[ -f "$HC/peer-output" ] && { cat "$HC/peer-output"; exit "$(cat "$HC/peer-status")"; }
[ -f "$HC/peer-hang" ] && {
here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here")
echo "$here" > "$HC/peer-pid"; printf '%s %s\\n' "$here" "$sid" >> "$HC/query-pids"
trap '' TERM HUP; exec sleep 60
}
if [ -f "$HC/peer-change" ]; then cat "$HC/peer-change" > "$HC/stat-socket"; fi
if [ -f "$HC/peer-pid-change" ]; then cat "$HC/peer-pid-change" > "$HC/show-mosaic-webui.service"; fi
exec /usr/bin/python3 -I -S -c '
import os,socket,struct,sys
class Fake:
def __init__(self,*a): assert a==(socket.AF_UNIX,socket.SOCK_STREAM)
def settimeout(self,n): assert n==1
def connect(self,p): assert p.endswith("/console/tester.sock")
def getsockopt(self,*a):
assert a==(socket.SOL_SOCKET,socket.SO_PEERCRED,12)
return struct.pack("3i",*[int(x) for x in open(os.environ["HC"]+"/peer").read().split()])
def close(self): pass
socket.socket=Fake
payload=sys.argv[4]
sys.argv=["probe",sys.argv[5]]
exec(payload)
' "$@"`;
const TREE = {
"/usr/bin/python3": PYTHON_STUB,
[NODE]: NODE_STUB,
[CLAUDE]: `[ "\${1:-}" = --version ] && cat "$HC/claude-version"; exit 0`,
[CLI]: `${log("mosaic")}
case "$1" in
talk) case "$(cat "$HC/talk-mode")" in
reply) echo "sent ab12cd34 to chief"; echo "[chief -> tester] seen" ;;
noreply) echo "sent ab12cd34 to chief"; echo "no reply from chief within 300 s" >&2; exit 1 ;;
sentonly) echo "sent ab12cd34 to chief" ;;
esac ;;
launches) cat "$HC/launches.json" ;;
*) exit 2 ;;
esac`,
[PRECHECK]: `${log("precheck")}
echo "precheck: done"; exit "$(cat "$HC/precheck-rc")"`,
"/usr/local/lib/mosaic-stack/hostcheck.sh": `${log("hostcheck")}
cat "$HC/hostcheck-out"`,
};
function proc(root, pid, { ppid, uid = 997, exe, argv0 = "x", comm = "x", label }) {
const d = join(root, "proc", String(pid));
put(join(d, "status"), `Name:\t${comm}\nState:\tS (sleeping)\nPPid:\t${ppid}\nUid:\t${uid}\t${uid}\t${uid}\t${uid}\n`);
put(join(d, "cmdline"), `${argv0}\0--x\0`);
put(join(d, "comm"), `${comm}\n`);
rmSync(join(d, "exe"), { force: true });
if (exe) symlinkSync(exe, join(d, "exe"));
if (label !== undefined) put(join(d, "attr", "current"), label);
}
// A scratch Case 1 host where every F check passes and every P check is closed or accepted.
function host(t) {
const dir = tmp(t, "c1accept-");
const root = join(dir, "root"), hc = join(dir, "hc"), bin = join(dir, "bin");
const args = join(root, "etc", "mosaic-stack", "dev.args");
put(args, ARGS.join("\n") + "\n", 0o644);
put(join(root, "etc", "passwd"), PASSWD);
put(join(root, "etc", "group"), GROUP);
put(join(root, "etc", "sudoers.d", "90-localadmin"), "localadmin ALL=(ALL) NOPASSWD: ALL\n");
put(join(root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: "1" } }));
put(join(root, CRED), `CLAUDE_CODE_OAUTH_TOKEN=${CRED_VALUE}\n`, 0o600);
put(join(root, HOME, ".config", "mosaic-dev", "businesses", "mosaic-stack.json"), JSON.stringify({ id: "mosaic-stack", launch: { by: "pm" } }));
put(join(root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
proc(root, 1, { ppid: 0, uid: 0, exe: "/usr/lib/systemd/systemd", comm: "systemd" });
proc(root, 100, { ppid: 1, exe: NODE, argv0: NODE, comm: "node" });
proc(root, 200, { ppid: 1, exe: NODE, argv0: NODE, comm: "node" });
proc(root, 300, { ppid: 100, exe: "/usr/bin/bwrap", argv0: "/usr/bin/bwrap", comm: "bwrap", label: `${OUTER}\n` });
proc(root, 301, { ppid: 300, exe: NODE, argv0: NODE, comm: "node", label: `${CHILD}\n` });
proc(root, 302, { ppid: 301, exe: CLAUDE, argv0: CLAUDE, comm: "claude.exe", label: `${CHILD}\n` });
// A root bwrap with a mislabelled claude child, which F5 must pass over.
proc(root, 400, { ppid: 1, uid: 0, exe: "/usr/bin/bwrap", argv0: "/usr/bin/bwrap", comm: "bwrap", label: "unconfined\n" });
proc(root, 401, { ppid: 400, uid: 0, argv0: "claude", comm: "claude", label: "unconfined\n" });
const state = {
"[email protected]": "ActiveState=active\nUser=mosaic\nMainPID=100\n",
"show-mosaic-webui.service": "ActiveState=active\nUser=mosaic\nMainPID=200\n",
"show-mosaic-hostcheck.service": "[email protected] mosaic-webui.service multi-user.target\n",
"enabled-mosaic-hostcheck.service": "enabled\n",
"curl-rc": "0", ss: SS,
"stat-parent": "997 700 directory 9 10\n", "stat-socket": "997 600 socket 9 11\n", peer: "200 997 997\n",
"stat-cred": "mosaic 600 regular file\n", "stat-settings": "root:root 644 regular file\n",
"sudo-l-mosaic": "User mosaic is not allowed to run sudo on dev-53.\n",
"sudo-l-localadmin": "User localadmin may run the following commands on dev-53:\n (ALL : ALL) ALL\n (ALL) NOPASSWD: ALL\n",
"jarvis-bwrap": "sudo: unable to execute /usr/bin/bwrap: Permission denied\n", "jarvis-bwrap-rc": "1",
"node-version": `${NODE_PIN}\n`, "claude-version": `${CLAUDE_PIN} (Claude Code)\n`,
"talk-mode": "reply", "precheck-rc": "0", "hostcheck-out": HOSTCHECK_OK,
"launches.json": JSON.stringify([
{ instance: "pm", live: true, business: "mosaic-stack", run: "r-0002" },
{ instance: "pm", live: false, business: "mosaic-stack", run: "r-0001" },
{ instance: "dev-1", live: true, business: "mosaic-stack", run: "r-0003" },
]),
};
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);
const copyRoot = join(root, HOME, '.local/share/mosaic-stack/copies', COPY);
put(join(copyRoot, 'packages/cli/src/config.mjs'), `import fs from 'node:fs';export const loadSystem=()=>({dataRoot:fs.readFileSync(${JSON.stringify(join(hc,'data-root'))},'utf8').trim()});`);
put(join(copyRoot, 'packages/webui/src/cli.mjs'), '// MOSAIC_TESTER_SOCKET_V1\n');
put(join(hc, 'data-root'), `${HOME}/.mosaic-dev\n`);
put(join(root, HOME, '.mosaic-dev/console/tester.sock'), 'stand-in socket metadata only');
chmodSync(join(root, HOME, '.mosaic-dev/console'), 0o700);
// 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 };
}
function run(h, extra = {}, argv = ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN, "--root", h.root]) {
const r = spawnSync(SCRIPT, argv, {
encoding: "utf8",
env: { ...process.env, PATH: `${h.bin}:${process.env.PATH}`, HC: h.hc, ROOT_DIR: h.root, REAL_NODE: process.execPath, TMPDIR: h.dir, ...extra },
});
const lines = r.stdout.split("\n").filter(Boolean);
// Each check line as { id, path, mark, evidence }.
const checks = lines.filter((l) => /^[FP][0-9]+ /.test(l)).map((l) => {
const [id, path, ...rest] = l.split(" ");
const tail = rest.join(" ");
const mark = MARKS.find((m) => tail === m || tail.startsWith(`${m} `));
return { id, path, mark, evidence: tail.slice(mark?.length ?? 0).trim(), line: l };
});
return { status: r.status, out: r.stdout, err: r.stderr, lines, checks };
}
const calls = (h, name) => (existsSync(join(h.hc, "calls.log")) ? readFileSync(join(h.hc, "calls.log"), "utf8") : "")
.split("\n").filter((l) => l === name || l.startsWith(`${name} `)).map((l) => l.slice(name.length + 1));
const failed = (r) => r.checks.filter((c) => c.mark === "FAIL").map((c) => c.id);
const marksOf = (r, id) => r.checks.filter((c) => c.id === id).map((c) => c.mark);
const mustRun = (h, extra) => { const r = run(h, extra); assert.ok(r.status === 0 || r.status === 1, r.out + r.err); return r; };
test('row253 socket observation uses configured root, metadata and the Console peer without sending bytes', t=>{
const h=host(t), data='/srv/mosaic/custom-data';
put(join(h.hc,'data-root'),data+'\n');
put(join(h.root,data,'console/tester.sock'),'metadata stand-in');
const r=run(h);
assert.equal(r.status,0,r.out+r.err);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=observed path=\/srv\/mosaic\/custom-data\/console\/tester.sock/);
assert.match(r.out,/peer_pid=200 peer_uid=997/);
assert.match(r.out,/hostile same-uid substitution remains/);
assert.doesNotMatch(r.out,/accepted mos-1101/);
assert.deepEqual(marksOf(r,'P8'),['closed']);
});
test('row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure',t=>{
const faults={
'parent-mode': h=>put(join(h.hc,'stat-parent'),'997 755 directory 9 10\n'),
'parent-owner': h=>put(join(h.hc,'stat-parent'),'998 700 directory 9 10\n'),
'socket-mode': h=>put(join(h.hc,'stat-socket'),'997 660 socket 9 11\n'),
'socket-owner': h=>put(join(h.hc,'stat-socket'),'998 600 socket 9 11\n'),
'socket-type': h=>put(join(h.hc,'stat-socket'),'997 600 regular file 9 11\n'),
symlink:h=>{const p=join(h.root,HOME,'.mosaic-dev/console/tester.sock');rmSync(p);symlinkSync('/absent',p);},
peer:h=>put(join(h.hc,'peer'),'200 998 997\n'),
'peer-pid':h=>put(join(h.hc,'peer'),'555 997 997\n'),
'python-mode':h=>chmodSync(join(h.root,'usr/bin/python3'),0o777),
'python-missing':h=>rmSync(join(h.root,'usr/bin/python3')),
'python-not-elf':h=>put(join(h.hc,'elf'),'23212f62\n'),
'peer-malformed':h=>{put(join(h.hc,'peer-output'),'200 997 997 extra\n');put(join(h.hc,'peer-status'),'0');},
'peer-error':h=>{put(join(h.hc,'peer-output'),'200 997 997\n');put(join(h.hc,'peer-status'),'1');},
'peer-uid-fields':h=>proc(h.root,200,{ppid:1,uid:998,exe:NODE,comm:'node'}),
'changed-inode':h=>put(join(h.hc,'peer-change'),'997 600 socket 9 99\n'),
'changed-pid':h=>put(join(h.hc,'peer-pid-change'),'ActiveState=active\nUser=mosaic\nMainPID=201\n'),
floor:h=>put(join(h.root,'proc/sys/net/ipv4/ip_unprivileged_port_start'),'700\n'),
};
for(const [name,fault] of Object.entries(faults)){
const h=host(t);fault(h);const r=run(h);
assert.equal(r.status,name==='peer-uid-fields'?1:0,`${name}: ${r.out}${r.err}`);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=failed/,name);
assert.doesNotMatch(r.out,/boundary=observed/,name);
}
});
test('row253 acceptance rejects bad config and unsupported copies before talk',t=>{
const faults={
outside:h=>put(join(h.hc,'data-root'),'/srv/mosaic2/data\n'),
dot:h=>put(join(h.hc,'data-root'),'/srv/mosaic/./data\n'),
absent:h=>rmSync(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs')),
unmarked:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/webui/src/cli.mjs'),'// old copy'),
schema:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),'console.log(JSON.stringify({dataRoot:"/srv/mosaic/.mosaic-dev",socket:true,extra:1}));process.exit(0);'),
failed:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),'console.log(JSON.stringify({dataRoot:"/srv/mosaic/.mosaic-dev",socket:true}));process.exit(1);'),
};
for(const [name,fault] of Object.entries(faults)){
const h=host(t);fault(h);const r=run(h);
assert.equal(r.status,2,`${name}: ${r.out}${r.err}`);assert.equal(r.out,'');
assert.match(r.err,/tester (config query|path refuses)|support/);
assert.deepEqual(calls(h,'mosaic'),[]);
}
});
test('row253 stalled peer observation ends within its bound without leaving a reader',async t=>{
const h=host(t);put(join(h.hc,'peer-hang'),'');
const r=run(h);assert.equal(r.status,0,r.out+r.err);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=failed reason=probe/);
const pid=Number(readFileSync(join(h.hc,'peer-pid'),'utf8'));
let state='gone';
for(let n=0;n<50;n++){
try{state=readFileSync(`/proc/${pid}/stat`,'utf8').split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`peer helper ${pid} still ${state}`);
});
test('row253 P8 refuses both retired TCP and board listeners',t=>{
for(const port of [7333,7331]){
const h=host(t);put(join(h.hc,'ss'),SS+`LISTEN 0 511 127.0.0.1:${port} 0.0.0.0:* users:(("node",pid=200,fd=22))\n`);
assert.deepEqual(marksOf(run(h),'P8'),['unnamed']);
}
});
test('row253 C4 acceptance query inner reason holds under both sudo models',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];
put(join(h.hc,'session-mode'),mode);
put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),blockingQuerySource(files[1]));
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query inner-timeout\n');
assert.ok(seconds<7,`${mode}: ${seconds}s`);assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
assert.deepEqual(await pollFixtureGone(files),[]);await stop();
}
});
test('row253 C4 acceptance query outer reason discards stdout and names residual cleanup',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-config'),'');
put(join(h.hc,'stall-output'),JSON.stringify({dataRoot:HOME+'/.mosaic-dev',socket:true}));
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query outer-timeout\n');
assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s preflight slack`);
assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'R3 session remains before fixture cleanup');
killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit R3 fixture cleanup');await stop();
}
});
test('row253 C4 acceptance query overflow is output-limit in both models',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);const capture=cleanupFixture(t,files);
put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:'${HOME}/.mosaic-dev'});`);
const r=run(h);capture();assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query output-limit\n');
assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);assert.deepEqual(await pollFixtureGone(files),[]);
}
});
for(const fault of ['inner','outer','overflow'])test(`row253 C4 P10 ${fault} reason holds under both sudo models`,async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];put(join(h.hc,'session-mode'),mode);
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files,7000);
if(fault==='inner')put(join(h.hc,'peer-hang'),'');
if(fault==='outer'){put(join(h.hc,'stall-probe'),'');put(join(h.hc,'stall-output'),'200 997 997\n');}
if(fault==='overflow'){put(join(h.hc,'peer-output'),'200 997 997\n'+'X'.repeat(256));put(join(h.hc,'peer-status'),'0');}
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,0,r.out+r.err);
const reason=fault==='inner'?'inner-timeout':fault==='outer'?'outer-timeout':'output-limit';
assert.match(r.out,new RegExp('P10 tester-socket-squat unnamed boundary=failed reason=probe-'+reason+' '));
assert.doesNotMatch(r.out,/boundary=observed/);
if(fault==='inner')assert.ok(seconds<4,`${mode}: ${seconds}s includes functional checks`);
if(fault==='outer'){
assert.ok(seconds>=4&&seconds<6,`${mode}: ${seconds}s; 2s functional-check slack`);
if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'stalled supervisor is R3 residual');
for(const file of files)killRecordedPids(file);
}
assert.deepEqual(await pollFixtureGone(files),[],fault==='outer'?'explicit R3 cleanup':mode);await stop();
}
});
// One fault per F check; each makes that check, and only that one, fail.
const F_FAULTS = {
F1: (h) => proc(h.root, 100, { ppid: 1, uid: 0, exe: NODE, comm: "node" }),
F2: (h) => put(join(h.hc, "ss"), SS.replace("127.0.0.1:732 ", "0.0.0.0:732 ")),
F3: (h) => put(join(h.hc, "talk-mode"), "noreply"),
F4: (h) => put(join(h.hc, "precheck-rc"), "1"),
F5: (h) => put(join(h.root, "proc", "302", "attr", "current"), "unconfined\n"),
F6: (h) => put(join(h.hc, "claude-version"), "2.0.9 (Claude Code)\n"),
F7: (h) => put(join(h.hc, "launches.json"), JSON.stringify([{ instance: "pm", live: false, business: "mosaic-stack", run: "r-1" }])),
F8: (h) => put(join(h.hc, "stat-cred"), "mosaic 640 regular file\n"),
F9: (h) => put(join(h.root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: "0" } })),
F10: (h) => put(join(h.hc, "node-version"), "v24.0.0\n"),
F11: (h) => put(join(h.root, HOME, ".mosaic-dev", "discord", "binding.json"), "{}"),
};
test("row 195 accept: every check passing exits 0, with each P mark and counts that match the lines", (t) => {
const h = host(t);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
assert.deepEqual(r.checks.map((c) => c.id), ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11",
"P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9", "P10", "P11"]);
for (const c of r.checks.filter((x) => x.id.startsWith("F"))) assert.equal(c.mark, "pass", c.line);
for (const [id, mark] of Object.entries(P_PASS)) assert.deepEqual(marksOf(r, id), [mark], id);
const counts = r.lines.filter((l) => l.startsWith("count ")).map((l) => l.slice(6));
assert.deepEqual(counts, MARKS.map((m) => `${m} ${r.checks.filter((c) => c.mark === m).length}`));
assert.match(r.lines.at(-1), /^case1-accept: every F check passes/);
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) => {
for (const [id, fault] of Object.entries(F_FAULTS)) {
const h = host(t);
fault(h);
const r = mustRun(h);
assert.equal(r.status, 1, `${id}: ${r.out}`);
assert.deepEqual(failed(r), [id], `${id}: ${r.out}`);
assert.equal(r.lines.at(-1), "case1-accept: an F check failed, so the row fails");
}
});
test("row 195 accept: F3 fails on exit 0 with no reply line, and its evidence takes talk's error", (t) => {
let h = host(t);
put(join(h.hc, "talk-mode"), "sentonly");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F3"]);
assert.match(r.out, /^F3 tester-reply FAIL talk exited 0 with 1 lines on stdout; last: sent ab12cd34 to chief$/m);
h = host(t);
put(join(h.hc, "talk-mode"), "noreply");
r = mustRun(h);
assert.match(r.out, /^F3 tester-reply FAIL talk exited 1 with 1 lines on stdout; last: no reply from chief within 300 s$/m);
});
test("row 195 accept: F1 fails on a unit that isn't active or isn't User=mosaic", (t) => {
for (const show of ["ActiveState=failed\nUser=mosaic\nMainPID=0\n", "ActiveState=active\nUser=root\nMainPID=100\n"]) {
const h = host(t);
put(join(h.hc, "show-mosaic-webui.service"), show);
const r = mustRun(h);
assert.equal(r.status, 1);
assert.ok(failed(r).includes("F1"), r.out);
}
});
test("row 195 accept: F2 fails when healthz doesn't answer and when another pid holds the port", (t) => {
let h = host(t);
put(join(h.hc, "curl-rc"), "7");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F2"]);
assert.match(r.out, /^F2 console-loopback FAIL curl http:\/\/127\.0\.0\.1:732\/healthz exited 7$/m);
h = host(t);
put(join(h.hc, "ss"), SS.replace("pid=200,fd=21", "pid=555,fd=4"));
r = mustRun(h);
assert.deepEqual(failed(r), ["F2"]);
});
test("row 195 accept: F5 fails on unpriv_bwrap (enforce), on complain and on a trailing space", (t) => {
for (const label of ["unpriv_bwrap (enforce)\n", "bwrap//&unpriv_bwrap (complain)\n", `${CHILD} \n`]) {
const h = host(t);
put(join(h.root, "proc", "302", "attr", "current"), label);
const r = mustRun(h);
assert.deepEqual(failed(r), ["F5"], `${JSON.stringify(label)}: ${r.out}`);
}
const h = host(t);
put(join(h.root, "proc", "300", "attr", "current"), `${OUTER} \n`);
assert.deepEqual(failed(mustRun(h)), ["F5"]);
});
test("row 195 accept: F5 fails with no bwrap of mosaic's, and passes on comm alone", (t) => {
let h = host(t);
proc(h.root, 300, { ppid: 100, uid: 1001, exe: "/usr/bin/bwrap", comm: "bwrap", label: `${OUTER}\n` });
let r = mustRun(h);
assert.deepEqual(failed(r), ["F5"]);
assert.match(r.out, /^F5 bwrap-label FAIL no \/usr\/bin\/bwrap process of mosaic/m);
h = host(t);
proc(h.root, 302, { ppid: 301, argv0: "/proc/self/fd/3", comm: "claude", label: `${CHILD}\n` });
assert.deepEqual(failed(mustRun(h)), []);
});
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);
assert.deepEqual(failed(r), ["F6"]);
h = host(t);
const other = CLAUDE.replace(`claude-${CLAUDE_PIN}`, "claude-2.0.9");
put(h.args, ARGS.map((a) => (a === CLAUDE ? other : 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"]);
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) => {
const two = [{ instance: "pm", live: true, business: "mosaic-stack", run: "a" }, { instance: "pm", live: true, business: "mosaic-stack", run: "b" }];
for (const [file, text] of [["launches.json", JSON.stringify(two)], ["launches.json", JSON.stringify([{ instance: "pm", live: true, business: "other", run: "a" }])]]) {
const h = host(t);
put(join(h.hc, file), text);
assert.deepEqual(failed(mustRun(h)), ["F7"]);
}
const h = host(t);
put(join(h.root, HOME, ".config", "mosaic-dev", "businesses", "mosaic-stack.json"), JSON.stringify({ id: "mosaic-stack" }));
const r = mustRun(h);
assert.deepEqual(failed(r), ["F7"]);
assert.match(r.out, /^F7 pm-live FAIL the business file has no launch\.by$/m);
});
test("row 195 accept: F8 fails on 0640, on another owner and on a symlink, and never prints the credential", (t) => {
for (const fault of [
(h) => put(join(h.hc, "stat-cred"), "mosaic 640 regular file\n"),
(h) => put(join(h.hc, "stat-cred"), "root 600 regular file\n"),
(h) => { const f = join(h.root, CRED); rmSync(f); put(`${f}.real`, `X=${CRED_VALUE}\n`, 0o600); symlinkSync(`${f}.real`, f); },
]) {
const h = host(t);
fault(h);
const r = mustRun(h);
assert.deepEqual(failed(r), ["F8"], r.out);
assert.ok(!(r.out + r.err).includes(CRED_VALUE));
}
});
test("row 195 accept: F9 fails on another owner, a group-writable mode and a number for the flag", (t) => {
for (const fault of [
(h) => put(join(h.hc, "stat-settings"), "root:root 664 regular file\n"),
(h) => put(join(h.hc, "stat-settings"), "mosaic:root 644 regular file\n"),
(h) => put(join(h.root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: 1 } })),
]) {
const h = host(t);
fault(h);
assert.deepEqual(failed(mustRun(h)), ["F9"]);
}
});
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"), "{}");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F11"]);
assert.match(r.out, /^F11 no-discord FAIL \/srv\/mosaic\/\.mosaic-dev\/discord holds bindings: chief\.json$/m);
h = host(t);
mkdirSync(join(h.root, HOME, ".mosaic-dev", "discord"), { recursive: true });
assert.deepEqual(failed(mustRun(h)), []);
});
// Each P check in its unnamed mark, with the line count each fault makes; none changes the exit.
const P_FAULTS = {
P1: [1, (h) => put(join(h.hc, "sudo-l-mosaic"), "User mosaic may run the following commands on dev-53:\n")],
P2: [1, (h) => put(join(h.root, HOME, ".ssh", "authorized_keys"), "ssh-ed25519 AAAA test\n")],
P5: [1, (h) => edit(join(h.root, "etc", "group"), (s) => s.replace("docker:x:999:", "docker:x:999:jarvis"))],
P6: [2, (h) => edit(join(h.root, "etc", "group"), (s) => s.replace("sudo:x:27:localadmin", "sudo:x:27:localadmin,bob").replace("lxd:x:998:", "lxd:x:998:carol"))],
P7: [1, (h) => put(join(h.root, "etc", "sudoers.d", "50-mosaic"), "mosaic ALL=(root) NOPASSWD: /usr/bin/systemctl\n")],
P8: [1, (h) => put(join(h.hc, "ss"), `${SS}LISTEN 0 511 0.0.0.0:7331 0.0.0.0:* users:(("node",pid=200,fd=23))\n`)],
P9: [3, (h) => { put(join(h.hc, "hostcheck-out"), HOSTCHECK_OK.replace("ok 2 ", "FAIL 2 ").replace("ok 5 ", "FAIL 5 ")); put(join(h.hc, "jarvis-bwrap-rc"), "0"); }],
P10: [1, (h) => put(join(h.root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "700\n")],
P11: [1, (h) => put(join(h.hc, "show-mosaic-hostcheck.service"), "[email protected] multi-user.target\n")],
};
test("row 195 accept: each P check reports unnamed on its fault, and the exit stays 0", (t) => {
for (const [id, [n, fault]] of Object.entries(P_FAULTS)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 0, `${id}: ${r.out}`);
assert.deepEqual(marksOf(r, id), Array(n).fill("unnamed"), `${id}: ${r.out}`);
}
});
test("row 195 accept: P2 is unnamed with a supplementary group, and P5 names docker", (t) => {
let h = host(t);
put(join(h.root, "etc", "group"), GROUP.replace("lxd:x:998:", "lxd:x:998:mosaic"));
let r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P2"), ["unnamed"]);
assert.match(r.out, /^P2 mosaic-groups-keys unnamed id -G is '997 998'; authorized_keys is absent$/m);
h = host(t);
P_FAULTS.P5[1](h);
r = run(h);
assert.match(r.out, /^P5 jarvis-reach unnamed jarvis is in docker$/m);
assert.match(r.out, /^P6 group-docker unnamed jarvis is a member of docker$/m);
});
test("row 195 accept: P5 is closed with no jarvis account, and P2 with an empty authorized_keys", (t) => {
const h = host(t);
put(join(h.root, "etc", "passwd"), PASSWD.replace(/^jarvis:.*\n/m, ""));
put(join(h.root, HOME, ".ssh", "authorized_keys"), "");
const r = run(h);
assert.deepEqual(marksOf(r, "P5"), ["closed"]);
assert.deepEqual(marksOf(r, "P2"), ["closed"]);
});
test("row 195 accept: P9 is unnamed once for each item missing", (t) => {
const items = {
hostcheck: (h) => put(join(h.hc, "hostcheck-out"), HOSTCHECK_OK.replace("ok 3 ", "FAIL 3 ")),
jarvis: (h) => { put(join(h.hc, "jarvis-bwrap-rc"), "0"); put(join(h.hc, "jarvis-bwrap"), ""); },
member: (h) => put(join(h.root, "etc", "group"), GROUP.replace("mosaic:x:997:", "mosaic:x:997:jarvis")),
gid: (h) => put(join(h.root, "etc", "passwd"), `${PASSWD}hermes:x:1002:997::/home/hermes:/bin/bash\n`),
};
for (const [name, fault] of Object.entries(items)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P9"), ["unnamed"], `${name}: ${r.out}`);
}
const h = host(t);
put(join(h.hc, "hostcheck-out"), "");
assert.deepEqual(marksOf(run(h), "P9"), Array(6).fill("unnamed"));
});
test("row253 accept: P10 fails its observation with another peer; P11 with the unit disabled", (t) => {
let h = host(t);
put(join(h.hc, "peer"), "555 997 997\n");
let r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P10"), ["unnamed"]);
assert.match(r.out, /P10 tester-socket-squat unnamed boundary=failed reason=peer/);
h = host(t);
put(join(h.hc, "enabled-mosaic-hostcheck.service"), "disabled\n");
r = run(h);
assert.deepEqual(marksOf(r, "P11"), ["unnamed"]);
});
test("row 195 accept: every P check unnamed together still exits 0", (t) => {
const h = host(t);
for (const [, fault] of Object.values(P_FAULTS)) fault(h);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
assert.deepEqual(failed(r), []);
for (const id of ["P1", "P2", "P5", "P6", "P7", "P8", "P9", "P10", "P11"]) assert.ok(marksOf(r, id).every((m) => m === "unnamed"), id);
assert.match(r.lines.at(-1), /^case1-accept: every F check passes/);
});
test("row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason", (t) => {
const bad = {
"group write": [(h) => chmodSync(h.args, 0o664), /dev\.args must be \S+'s with no group or other write \(it is uid \d+, mode 664\)$/],
symlink: [(h) => { put(`${h.args}.real`, ARGS.join("\n") + "\n", 0o644); rmSync(h.args); symlinkSync(`${h.args}.real`, h.args); }, /no regular file \/etc\/mosaic-stack\/dev\.args$/],
"unknown flag": [(h) => put(h.args, ["--send", ...ARGS].join("\n") + "\n", 0o644), /has --send before --, which case2-unit\.sh does not take$/],
"two copies": [(h) => put(h.args, ["--copy", COPY, ...ARGS].join("\n") + "\n", 0o644), /needs exactly one --copy before --, not 2$/],
"library profile": [(h) => put(h.args, ARGS.map((a) => (a === "factory" ? "library" : a)).join("\n") + "\n", 0o644), /has --profile 'library', not factory$/],
"another business": [(h) => put(h.args, ARGS.map((a, i) => (a === "mosaic-stack" && i < 14 ? "acme" : a)).join("\n") + "\n", 0o644), /is for business acme, not mosaic-stack$/],
"no tester socket": [(h) => put(h.args, ARGS.slice(0, -1).join("\n") + "\n", 0o644), /needs one bare --tester-socket after --$/],
"retired TCP": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\n')+'\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],
"socket value": [(h) => put(h.args, [...ARGS,'7333'].join('\n')+'\n',0o644), /unknown serve flag 7333$/],
"duplicate socket": [(h) => put(h.args, [...ARGS,'--tester-socket'].join('\n')+'\n',0o644), /needs one bare --tester-socket after --$/],
"short copy": [(h) => put(h.args, ARGS.map((a) => (a === COPY ? "abc123" : a)).join("\n") + "\n", 0o644), /lacks a plain --user, --home, --business, --node, --claude or --copy$/],
"no file": [(h) => rmSync(h.args), /no regular file \/etc\/mosaic-stack\/dev\.args$/],
};
for (const [name, [fault, why]] of Object.entries(bad)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 2, `${name}: ${r.out}${r.err}`);
assert.match(r.err.trimEnd(), /^case1-accept: refusing: /, name);
assert.match(r.err.trimEnd(), why, name);
assert.equal(r.out, "", name);
assert.ok(!existsSync(join(h.hc, "calls.log")), name);
}
});
test("row 195 accept: usage errors and a run without --root as a user exit 2", (t) => {
const h = host(t);
let r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--node-pin", NODE_PIN, "--root", h.root]);
assert.equal(r.status, 2);
assert.match(r.err, /^usage: /);
r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", "24.1.0", "--root", h.root]);
assert.equal(r.status, 2);
r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN]);
assert.equal(r.status, 2);
assert.equal(r.err, "case1-accept: refusing: run it as root\n");
assert.ok(!existsSync(join(h.hc, "calls.log")));
});
test("row 195 accept: every in-instance call goes through sudo -u mosaic -H env PATH=<node dir>:...", (t) => {
const h = host(t);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
const sudo = calls(h, "sudo");
const asMosaic = sudo.filter((a) => a.startsWith("-u mosaic"));
const isolated = asMosaic.filter(a=>a.startsWith('-u mosaic -H env -i '));
assert.equal(isolated.length, 2);
for(const a of isolated) assert.ok(a.startsWith(`-u mosaic -H env -i HOME=${HOME} PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin `),a);
for (const a of asMosaic.filter(a=>!isolated.includes(a))) assert.ok(a.startsWith(`${AS_MOSAIC} /`), a);
const cmds = asMosaic.filter(a=>!isolated.includes(a)).map((a) => a.slice(AS_MOSAIC.length + 1).split(" ")[0]);
assert.deepEqual([...new Set(cmds)].sort(), [CLI, PRECHECK, CLAUDE, NODE].sort());
assert.deepEqual(sudo.filter((a) => !a.startsWith("-u mosaic")).sort(),
["-l -U localadmin", "-l -U mosaic", "-u jarvis /usr/bin/bwrap --version"]);
const talk = calls(h, "mosaic").filter((a) => a.startsWith("talk "));
assert.equal(talk.length, 1);
assert.match(talk[0], /^talk chief case1-accept-[0-9a-f]{12}: .* --business mosaic-stack --tester --wait 300$/);
assert.deepEqual(calls(h, "precheck"), ["--business mosaic-stack --profile factory"]);
});
test("row 195 accept: the host check runs with --check and never --enforce", (t) => {
const h = host(t);
run(h);
assert.deepEqual(calls(h, "hostcheck"), ["--check"]);
});
@@ -0,0 +1,609 @@
#!/usr/bin/env bash
# Case 2 row C2-13a (design 9.3, 10 step 2c): render and install the two
# system units a hub runs as, [email protected] (enabled as
# mosaic-bus@<business>.service) and mosaic-webui.service, from the existing
# templates, which it doesn't change.
#
# case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH
# [--profile factory|library] [--pm] [--claude PATH]
# -- SERVE FLAGS
#
# Root runs it from a root-owned checkout of the reviewed commit outside
# <home>, never from the release, because <user> can write the release. It
# writes both units under /etc/systemd/system as root:root 0644, runs
# `systemctl daemon-reload`, enables both, then starts the bus and then the
# Console (Q189). Both come back after a boot on their own. It never links
# a unit file into place. Root only stats files under <home> and reads bounded
# stdout from a config/target query run as the stack user through sudo. Root
# never imports copy code. This adds sudo and GNU timeout to the installer
# dependencies. The inner query supervisor runs after the uid drop: 5 s inside,
# 7 s on root's wait. Fixed system Bash, setsid, sleep, timeout and head are
# resolved and root-validated. Only the config query uses this supervisor.
# It renders the default instance only.
#
# The library profile takes exactly these serve flags, each once:
# --port N (1..1023) --business ID (the unit's) --public-host NAME
# --auth-header HEADER --allow-user ID --send
# --proof-header NAME --proof-file PATH
# The factory profile takes --port N (1..1023) and --business ID, and an
# optional bare --tester-socket marker. Retired tester TCP refuses. Only factory takes
# --pm and --claude.
#
# On a re-install the installed units must equal the render in everything
# but the copy id, and both must name one copy, so a re-pin or rollback
# re-renders the pair together. A unit that was already running stays on its
# old copy, since `systemctl start` leaves it alone; the script says so and
# that the bus and Console must restart together, and restarts nothing.
#
# Test seams, for packages/cli/tests/case2-unit.test.mjs only:
# CASE2_UNIT_DIR the unit directory, default /etc/systemd/system;
# the root and root-owned checks apply only there
# CASE2_SYSTEMCTL the systemctl command, default systemctl
# CASE2_GETENT the getent command, default getent
# CASE2_BUS_TEMPLATE default packages/cli/systemd/mosaic-bus.service.in
# CASE2_WEBUI_TEMPLATE default packages/webui/systemd/mosaic-webui.service.in
#
# Exit 0 done, 1 refused, 4 usage.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEFAULT_UNIT_DIR=/etc/systemd/system
UNIT_DIR="${CASE2_UNIT_DIR:-$DEFAULT_UNIT_DIR}"
SYSTEMCTL="${CASE2_SYSTEMCTL:-systemctl}"
GETENT="${CASE2_GETENT:-getent}"
BUS_TEMPLATE="${CASE2_BUS_TEMPLATE:-$REPO/packages/cli/systemd/mosaic-bus.service.in}"
WEBUI_TEMPLATE="${CASE2_WEBUI_TEMPLATE:-$REPO/packages/webui/systemd/mosaic-webui.service.in}"
[email protected]
WEBUI_UNIT=mosaic-webui.service
usage() { echo 'usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS' >&2; exit 4; }
die() { echo "case2-unit: $*" >&2; exit 1; }
# A value the unit carries as itself: no space, %, $, quote, backslash, & or |.
plain() { case "$1" in *[!A-Za-z0-9/._+-]*) return 1 ;; esac; }
abs_plain() {
case "$1" in /*) ;; *) die "$2 must be an absolute path: $1" ;; esac
plain "$1" || die "$2 contains a character the unit cannot carry: $1"
case "$1/" in */./*|*/../*|?*//*) die "$2 must be a normalized path: $1" ;; esac
[ "$1" = / ] || [ "${1%/}" = "$1" ] || die "$2 must not end in /: $1"
}
PROFILE=library USER_NAME='' HOME_DIR='' BUSINESS='' COPY_ID='' NODE='' PM=0 CLAUDE='' CLAUDE_SET=0
while [ $# -gt 0 ]; do
case "$1" in
--profile|--user|--home|--business|--copy|--node|--claude)
[ $# -ge 2 ] || usage
case "$1" in
--profile) PROFILE="$2" ;; --user) USER_NAME="$2" ;; --home) HOME_DIR="$2" ;;
--business) BUSINESS="$2" ;; --copy) COPY_ID="$2" ;; --node) NODE="$2" ;;
--claude) CLAUDE="$2"; CLAUDE_SET=1 ;;
esac
shift 2 ;;
--pm) PM=1; shift ;;
--) shift; break ;;
*) usage ;;
esac
done
SERVE=("$@")
[ -n "$USER_NAME" ] && [ -n "$HOME_DIR" ] && [ -n "$BUSINESS" ] && [ -n "$COPY_ID" ] && [ -n "$NODE" ] || usage
case "$PROFILE" in factory|library) ;; *) die "--profile must be factory or library: $PROFILE" ;; esac
if [ "$PROFILE" = library ]; then
[ "$PM" = 0 ] || die "the library profile refuses --pm"
[ "$CLAUDE_SET" = 0 ] || die "the library profile refuses --claude"
fi
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
[ "$(id -u)" = 0 ] || die "writing $DEFAULT_UNIT_DIR needs root"
fi
# The user, its uid, primary group and home, from the passwd database.
[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "--user must be a user name: $USER_NAME"
PW="$("$GETENT" passwd "$USER_NAME")" || die "no user $USER_NAME in the passwd database"
IFS=: read -r _ _ USER_UID USER_GID _ PW_HOME _ <<<"$PW"
[[ "$USER_UID" =~ ^[0-9]+$ ]] && [ "$USER_UID" -ne 0 ] || die "--user must not be root: $USER_NAME"
abs_plain "$HOME_DIR" '--home'
[ "$HOME_DIR" = "$PW_HOME" ] || die "--home $HOME_DIR is not $USER_NAME's home in the passwd database ($PW_HOME)"
GR="$("$GETENT" group "$USER_GID")" || die "no group $USER_GID in the group database"
GROUP="${GR%%:*}"
[[ "$GROUP" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "the primary group of $USER_NAME is not a group name: $GROUP"
case "$BUSINESS" in [a-z0-9]*) ;; *) die "--business must be a business id: $BUSINESS" ;; esac
case "$BUSINESS" in *[!a-z0-9._-]*) die "--business must be a business id: $BUSINESS" ;; esac
[ "${#BUSINESS}" -le 64 ] || die "--business must be a business id: $BUSINESS"
# The copy: copy_root's rule (scripts/stack-copy.sh:8, :22), under <home>.
COPIES="$HOME_DIR/.local/share/mosaic-stack/copies"
[[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] || die "--copy must be a full 40-character lower-case commit id: $COPY_ID"
COPY="$COPIES/$COPY_ID"
[ -d "$COPY" ] && [ ! -L "$COPY" ] || die "no copy directory $COPY"
[ -f "$COPY/scripts/mosaic" ] || die "no copy $COPY_ID in $COPIES: $COPY/scripts/mosaic is missing"
# Owned by root and writable by no one else, the file and every directory above it.
root_only() {
local p="$1" what="$2" owner mode
while :; do
owner="$(stat -c '%u' -- "$p")" || die "cannot stat $p"
mode="$(stat -c '%a' -- "$p")" || die "cannot stat $p"
[ "$owner" = 0 ] || die "$what: $p is not owned by root"
(( (8#$mode & 8#022) == 0 )) || die "$what: $p is writable by its group or others"
[ "$p" = / ] && break
p="$(dirname -- "$p")"
done
}
# --node: an absolute path to a root-owned regular file outside <home>.
abs_plain "$NODE" '--node'
case "$NODE/" in "$HOME_DIR"/*) die "--node must be outside $HOME_DIR: $NODE" ;; esac
[ ! -L "$NODE" ] || die "--node must not be a symlink; pass its target: $NODE"
[ -f "$NODE" ] && [ -x "$NODE" ] || die "--node must be an executable regular file: $NODE"
root_only "$NODE" '--node'
# --claude follows the --node rule, so nothing running as <user> can replace
# the binary the bus runs.
if [ "$CLAUDE_SET" = 1 ]; then
abs_plain "$CLAUDE" '--claude'
case "$CLAUDE/" in "$HOME_DIR"/*) die "--claude must be outside $HOME_DIR: $CLAUDE" ;; esac
[ ! -L "$CLAUDE" ] || die "--claude must not be a symlink; pass its target: $CLAUDE"
[ -f "$CLAUDE" ] && [ -x "$CLAUDE" ] || die "--claude must be an executable regular file: $CLAUDE"
root_only "$CLAUDE" '--claude'
fi
# The checkout the templates come from, when installing for real.
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
case "$REPO/" in "$HOME_DIR"/*) die "run this from a root-owned checkout outside $HOME_DIR, not $REPO" ;; esac
root_only "$BUS_TEMPLATE" 'bus template'
root_only "$WEBUI_TEMPLATE" 'Console template'
fi
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
# row253-supervisor-begin
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
stat_fields() {
local pid="$1" raw="" rest
# EOF is expected: stat has no NUL. Decide on content, not read's status.
IFS= read -r -d '' raw < "/proc/$pid/stat" 2>/dev/null || :
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
[[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
}
clock_cs() {
local boot spare
read -r boot spare < /proc/uptime || return 1
[[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
}
clean_session() {
local entry pid rc live until=""
while :; do
live=0
for entry in /proc/[0-9]*/stat; do
pid="${entry%/stat}"; pid="${pid##*/}"
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" != 3 ] || continue
[ "$rc" = 0 ] || return 1
[ "$ST_SESSION" = "$child" ] || continue
case "$ST_STATE" in Z|X) continue ;; esac
live=1
if ! kill -KILL "$pid" 2>/dev/null; then
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
fi
done
[ "$live" != 0 ] || return 0
clock_cs || return 1
# Inventory pass 1 is outside the bounded confirmation/retry budget.
[ -n "$until" ] || until=$((CLOCK_CS + 25))
[ "$CLOCK_CS" -lt "$until" ] || return 1
"$sleep_bin" 0.01
done
}
finish() {
local rc="$?"
trap - EXIT HUP INT TERM
if [ "$verified" = 1 ]; then
clean_session || rc=86
elif [ -n "$child" ]; then
kill -KILL "$child" 2>/dev/null || :
fi
[ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
"$setsid_bin" "$@" & child=$!
# Allow session creation the whole inner budget, rather than a 200 ms margin.
while :; do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 85
if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
tester_tools() {
local input target p st magic
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
p="$target"
while :; do
st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
[ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
[ "$p" != / ] || break
p="${p%/*}"; [ -n "$p" ] || p=/
done
magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
case "$input" in
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
/usr/bin/head) TS_HEAD="$target" ;;
esac
done
}
tester_capture_reason() {
case "$1" in
81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
*) echo inner-command ;;
esac
}
tester_capture() (
local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
tester_tools || exit 84
file="$(mktemp)" || exit 88
notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
trap 'exit 88' HUP INT TERM
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
set -o pipefail
maximum="$1"; head_bin="$2"; shift 2
"$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
# Only GNU timeout and the trusted head can write this root-side channel.
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
[ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
[ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# The serve flags, checked as scripts/webui-service.sh serve_flags checks
# them (:201-264), and kept in the order given. That script compares the
# proof file's owner to `id -u`, which is root here, so this copy has its own.
serve_flags() {
local -A seen=()
local flags='' name value
local port='' public_host='' auth_header='' allow_user='' proof_header='' proof_file='' tester_socket=0
local i=0 n=${#SERVE[@]}
while [ "$i" -lt "$n" ]; do
name="${SERVE[$i]}"
case "$name" in
--tester-port) die "case2-unit.sh retires tester TCP; use --tester-socket" ;;
--tester-socket) [ "$PROFILE" = factory ] || die "the library profile refuses --tester-socket" ;;
--checkout) die "refusing --checkout: Case 2 has no queue panel" ;;
--board) die "refusing --board: Case 2 runs no control board" ;;
--sso) die "refusing --sso: the Console signs people in through the proxy, with the preview set and the proof pair (Q175 phase 2 is a follow-up)" ;;
--port|--business) ;;
--public-host|--auth-header|--allow-user|--proof-header|--proof-file|--send)
[ "$PROFILE" = library ] || die "the factory profile refuses $name: its host has no proxy" ;;
*) die "unknown serve flag: $name" ;;
esac
[ -z "${seen[$name]:-}" ] || die "serve flag $name given twice"
seen[$name]=1
if [ "$name" = --tester-socket ]; then tester_socket=1; flags="$flags --tester-socket"; i=$((i + 1)); continue; fi
if [ "$name" = --send ]; then flags="$flags --send"; i=$((i + 1)); continue; fi
[ $((i + 1)) -lt "$n" ] || die "serve flag $name needs a value"
value="${SERVE[$((i + 1))]}"
case "$name" in
--port) port="$value"
[[ "$port" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$port" -le 1023 ] || die "--port must be 1..1023, so only the unit's capability can bind it: $port" ;;
--business) [ "$value" = "$BUSINESS" ] || die "serve --business $value is not the unit's business $BUSINESS" ;;
--public-host) public_host="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "public host must be a hostname, no port or scheme: $value" ;; esac
case "$value" in *[!A-Za-z0-9.-]*) die "public host must be a hostname, no port or scheme: $value" ;; esac ;;
--auth-header) auth_header="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "auth header must be a header name: $value" ;; esac
case "$value" in *[!A-Za-z0-9-]*) die "auth header must be a header name: $value" ;; esac ;;
--allow-user) allow_user="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "allowed user must start with a letter or digit: $value" ;; esac
case "$value" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $value" ;; esac ;;
--proof-header) proof_header="$value"
[[ "$value" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,63}$ ]] || die "invalid proxy proof header" ;;
--proof-file) proof_file="$value" ;;
esac
flags="$flags $name $value"
i=$((i + 2))
done
local required=(--port --business)
[ "$PROFILE" = factory ] || required+=(--public-host --auth-header --allow-user --send --proof-header --proof-file)
for name in "${required[@]}"; do
[ -n "${seen[$name]:-}" ] || die "serve flag $name is required"
done
if [ "$PROFILE" = factory ]; then
if [ "$tester_socket" = 1 ]; then
local query
query="$(tester_capture 5 7 4096 "$USER_NAME" "$HOME_DIR" "$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query $(tester_capture_reason "$?")"
tester_facts "$query" || die "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || die "the target does not support --tester-socket"
tester_location "$TS_DATA" "$HOME_DIR" "$USER_UID" "" || die "tester path refuses $TS_REASON"
fi
echo "${flags# }"; return 0
fi
# Lead decision 101: the uid header carries the Authentik uid.
if [ "${auth_header,,}" = x-authentik-uid ] && ! [[ "$allow_user" =~ ^[0-9a-f]{64}$ ]]; then
die "with --auth-header $auth_header the allowed user must be an Authentik uid, 64 lower-case hex characters (lead decision 101): $allow_user"
fi
case "${proof_header,,}" in host|origin|x-mosaic-tester|"${auth_header,,}") die "proxy proof header must differ from Host, Origin, tester and auth headers" ;; esac
# The proof file: metadata only, never opened (design 9.3).
local secrets="$HOME_DIR/.config/mosaic-dev/secrets" real owner mode
abs_plain "$proof_file" 'proxy proof file'
[ ! -L "$proof_file" ] || die "proxy proof file must not be a symlink: $proof_file"
[ -f "$proof_file" ] || die "proxy proof file must be a regular file: $proof_file"
real="$(realpath -e -- "$proof_file")" || die "cannot resolve proxy proof file: $proof_file"
[ "$real" = "$proof_file" ] || die "proxy proof file path must have no symlink in it: $proof_file"
case "$proof_file" in "$secrets"/*) ;; *) die "proxy proof file must be under $secrets, outside the data root: $proof_file" ;; esac
owner="$(stat -c '%u' -- "$proof_file")" || die "cannot stat proxy proof file"
mode="$(stat -c '%a' -- "$proof_file")" || die "cannot stat proxy proof file"
[ "$owner" = "$USER_UID" ] || die "proxy proof file must be owned by $USER_NAME (uid $USER_UID): $proof_file"
[ "$mode" = 600 ] || [ "$mode" = 400 ] || die "proxy proof file must be mode 0600 or 0400, not $mode: $proof_file"
echo "${flags# }"
}
# PATH: the node's directory first, then the default list without it. Unlike
# scripts/webui-service.sh:303-305 this always puts node's directory first,
# so the bus's `exec node` (scripts/mosaic) finds the same node.
NODE_DIR="$(dirname -- "$NODE")"
UNIT_PATH="$NODE_DIR"
for d in /usr/local/bin /usr/bin /bin; do [ "$d" = "$NODE_DIR" ] || UNIT_PATH="$UNIT_PATH:$d"; done
FLAGS="$(serve_flags)" || exit 1
HARDENING="ProtectSystem=strict
ReadWritePaths=$HOME_DIR
ProtectHome=yes
PrivateTmp=yes"
CAPS="AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE"
# The default instance, as scripts/instance.sh:85-107 renders it: no
# MOSAIC_CONFIG line, and the session env under %h, which becomes <home>.
SESSION_ENV='%h/.config/mosaic-dev/secrets/claude-session.env'
ENV_FILE="-$HOME_DIR/.config/mosaic-dev/secrets/claude-session.env"
PM_FLAGS=''
[ "$PM" = 0 ] || PM_FLAGS=' --pm'
CLAUDE_ENV=''
[ "$CLAUDE_SET" = 0 ] || CLAUDE_ENV="Environment=MOSAIC_CLAUDE_BIN=$CLAUDE"
# One unit from its template. kind is bus or webui.
render() {
local kind="$1" template="$2" text tokens
[ -f "$template" ] && [ ! -L "$template" ] || die "template missing or not a regular file: $template"
text="$(sed -e "s|@REPO@|$COPY|g" -e "s|@PATH@|$UNIT_PATH|g" -e "s|@NODE@|$NODE|g" \
-e "s|@PM_FLAGS@|$PM_FLAGS|g" -e "s|^@CLAUDE_ENV@\$|$CLAUDE_ENV|" \
-e '/^@CONFIG_ENV@$/d' -e "s|@SESSION_ENV@|$SESSION_ENV|g" \
-e "s|%h|$HOME_DIR|g" -- "$template")"
# Checked before @FLAGS@ goes in, since an allowed user may hold @.
tokens="$(printf '%s\n' "$text" | grep -oE '@[A-Z_]+@' || true)"
if [ "$kind" = webui ]; then
[ "$tokens" = @FLAGS@ ] || die "$template: a token other than one @FLAGS@ is left after the render: $(echo $tokens)"
text="${text//@FLAGS@/$FLAGS}"
else
[ -z "$tokens" ] || die "$template: a token is left after the render: $(echo $tokens)"
fi
if printf '%s\n' "$text" | grep -q '%h'; then die "$template: %h is left after the render"; fi
local add="User=$USER_NAME\nGroup=$GROUP\n${HARDENING//$'\n'/\\n}\n"
[ "$kind" = bus ] || add="$add${CAPS//$'\n'/\\n}\n"
# The added lines go at the end of [Service], before its trailing blank
# lines. In [Install], WantedBy=multi-user.target replaces
# WantedBy=default.target in both units, so both start at boot (Q189).
printf '%s\n' "$text" | awk -v add="$add" '
function blanks_out() { for (; blanks > 0; blanks--) print "" }
/^\[.*\]$/ {
if (sec == "[Service]") printf "%s", add
sec = $0
blanks_out(); print; next
}
$0 == "" { blanks++; next }
{ blanks_out() }
sec == "[Install]" && $0 == "WantedBy=default.target" { $0 = "WantedBy=multi-user.target" }
{ print }
END { if (sec == "[Service]") printf "%s", add; blanks_out() }'
}
# The unit's settings as section<TAB>key<TAB>value, one per line, keys
# trimmed as systemd trims them. A continued line refuses, so no setting
# hides in one.
settings() {
awk '
{ sub(/^[ \t]+/, ""); sub(/[ \t]+$/, "") }
$0 == "" || /^[#;]/ { next }
/\\$/ { print "CONTINUED"; exit }
/^\[.*\]$/ { sec = $0; next }
{
i = index($0, "="); if (i == 0) { print "NOKEY\t" $0; next }
k = substr($0, 1, i - 1); v = substr($0, i + 1)
sub(/[ \t]+$/, "", k); sub(/^[ \t]+/, "", v)
print sec "\t" k "\t" v
}'
}
# Refuses unless one unit's fixed lines hold. what names the unit in messages,
# copy is the copy directory it should run from.
check_unit() {
local kind="$1" text="$2" what="$3" copy="$4" s
s="$(printf '%s\n' "$text" | settings)"
! printf '%s\n' "$s" | grep -qx CONTINUED || die "$what: a line continues onto the next"
! printf '%s\n' "$s" | grep -q '^NOKEY' || die "$what: a line is neither a section, a comment nor a setting"
# %h in a system unit is root's home, not <user>'s.
! printf '%s\n' "$text" | grep -q '%h' || die "$what: %h is left; a system unit reads it as root's home"
local key
for key in RestrictNamespaces PrivateUsers SystemCallFilter PermissionsStartOnly; do
! printf '%s\n' "$s" | awk -F'\t' -v k="$key" '$2 == k { found = 1 } END { exit !found }' \
|| die "$what: refusing $key=, which the hub's units must not hold"
done
# An Exec*= value whose leading prefix characters hold + or ! runs with
# full privileges, whatever User= says.
local bad
bad="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^Exec/ { p = $3; sub(/[^-@:+!].*$/, "", p); if (p ~ /[+!]/) print $2 "=" $3 }')"
[ -z "$bad" ] || die "$what: refusing an Exec line that runs with full privileges: $bad"
# The fixed lines, each exactly once, in [Service].
one() {
local k="$1" v="$2" got
got="$(printf '%s\n' "$s" | awk -F'\t' -v k="$k" '$2 == k { print $1 "\t" $3 }')"
[ "$got" = "[Service] $v" ] || die "$what: $k= must be exactly once in [Service], as $k=$v"
}
none() {
! printf '%s\n' "$s" | awk -F'\t' -v k="$1" '$2 == k { found = 1 } END { exit !found }' || die "$what: refusing $1=, which this unit must not hold"
}
one User "$USER_NAME"
one Group "$GROUP"
one WorkingDirectory "$copy"
# Environment= lines: PATH, and MOSAIC_CLAUDE_BIN only with --claude.
local envs want_envs="[Service] PATH=$UNIT_PATH"
[ "$kind" = webui ] || [ -z "$CLAUDE_ENV" ] || want_envs="$want_envs
[Service] MOSAIC_CLAUDE_BIN=$CLAUDE"
envs="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "Environment" { print $1 "\t" $3 }')"
[ "$envs" = "$want_envs" ] || die "$what: the Environment= lines must be exactly: $(printf '%s' "$want_envs" | cut -f2 | tr '\n' ' ')"
one NoNewPrivileges yes
one ProtectSystem strict
one ReadWritePaths "$HOME_DIR"
one ProtectHome yes
one PrivateTmp yes
local order
order="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)$/ { printf "%s ", $2 }')"
[ "$order" = "ProtectSystem ReadWritePaths ProtectHome PrivateTmp " ] || die "$what: the hardening lines must be in the order ProtectSystem, ReadWritePaths, ProtectHome, PrivateTmp"
local wanted
wanted="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "WantedBy" { print $1 "\t" $3 }')"
[ "$wanted" = "[Install] multi-user.target" ] || die "$what: WantedBy= must be exactly once, as WantedBy=multi-user.target"
if [ "$kind" = bus ]; then
one ExecStart "$copy/scripts/mosaic bus start %i$PM_FLAGS"
one EnvironmentFile "$ENV_FILE"
none AmbientCapabilities
none CapabilityBoundingSet
else
one ExecStart "$NODE $copy/packages/webui/src/cli.mjs serve $FLAGS"
none EnvironmentFile
one AmbientCapabilities CAP_NET_BIND_SERVICE
one CapabilityBoundingSet CAP_NET_BIND_SERVICE
fi
}
BUS_TEXT="$(render bus "$BUS_TEMPLATE")" || exit 1
WEBUI_TEXT="$(render webui "$WEBUI_TEMPLATE")" || exit 1
check_unit bus "$BUS_TEXT" "rendered $BUS_UNIT" "$COPY"
check_unit webui "$WEBUI_TEXT" "rendered $WEBUI_UNIT" "$COPY"
# A re-install: each installed unit passes the same checks, both name one
# copy, and each equals the render once its copy id is the new one.
[ -d "$UNIT_DIR" ] && [ ! -L "$UNIT_DIR" ] || die "no unit directory $UNIT_DIR"
installed_ids=''
for pair in "bus:$BUS_UNIT:BUS_TEXT" "webui:$WEBUI_UNIT:WEBUI_TEXT"; do
IFS=: read -r kind name var <<<"$pair"
path="$UNIT_DIR/$name"
[ ! -L "$path" ] || die "refusing: $path is a symlink"
[ -e "$path" ] || continue
[ -f "$path" ] || die "refusing: $path is not a regular file"
text="$(cat -- "$path")"
old="$(printf '%s\n' "$text" | settings | awk -F'\t' -v c="$COPIES/" '$2 == "WorkingDirectory" && index($3, c) == 1 { print substr($3, length(c) + 1) }')"
[[ "$old" =~ ^[0-9a-f]{40}$ ]] || die "installed $path names no copy under $COPIES"
check_unit "$kind" "$text" "installed $path" "$COPIES/$old"
[ "$text" = "$(printf '%s\n' "${!var}" | sed "s|$COPY|$COPIES/$old|g")" ] \
|| die "installed $path differs from the render in more than the copy id; the node path, serve flags, --pm and --claude must equal the installed ones"
installed_ids="$installed_ids $old"
done
read -r -a ids <<<"$installed_ids"
[ "${#ids[@]}" -lt 2 ] || [ "${ids[0]}" = "${ids[1]}" ] || die "the installed units name different copies, ${ids[0]} and ${ids[1]}"
# `systemctl start` leaves a running unit on the copy it started from, so
# note which units run before the write (row 195 owns the restart).
BUS_NAME="mosaic-bus@$BUSINESS.service"
running=''
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
if "$SYSTEMCTL" is-active --quiet "$name"; then running="$running $name"; fi
done
# Write each unit to a temp file in the unit directory, then rename.
write_unit() {
local name="$1" text="$2" tmp
tmp="$(mktemp "$UNIT_DIR/.case2-unit.XXXXXX")"
printf '%s\n' "$text" >"$tmp"
chmod 0644 "$tmp"
[ "$(id -u)" != 0 ] || chown root:root "$tmp"
mv -fT -- "$tmp" "$UNIT_DIR/$name"
}
write_unit "$BUS_UNIT" "$BUS_TEXT"
write_unit "$WEBUI_UNIT" "$WEBUI_TEXT"
# Enable both, then start the bus, then the Console (design 10 step 2c).
# serve calls the bus per request, so the units carry no After= or Wants=.
"$SYSTEMCTL" daemon-reload || die "systemctl daemon-reload failed; the units are written, nothing is enabled or started"
"$SYSTEMCTL" enable "$BUS_NAME" "$WEBUI_UNIT" || die "systemctl enable $BUS_NAME $WEBUI_UNIT failed; nothing is started"
"$SYSTEMCTL" start "$BUS_NAME" || die "systemctl start $BUS_NAME failed; the Console is not started"
"$SYSTEMCTL" start "$WEBUI_UNIT" || die "systemctl start $WEBUI_UNIT failed; the bus is running"
if [ -z "$running" ]; then
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID; enabled and started $BUS_NAME, then $WEBUI_UNIT"
exit 0
fi
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID and enabled both"
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
case " $running " in
*" $name "*) echo "case2-unit: $name was already running, so it is still on ${ids[0]:+copy }${ids[0]:-the copy it started from}, not copy $COPY_ID" ;;
*) echo "case2-unit: started $name on copy $COPY_ID" ;;
esac
done
echo "case2-unit: restart $BUS_NAME and $WEBUI_UNIT together to put both on copy $COPY_ID; until then they may run different copies"
@@ -0,0 +1,872 @@
// Case 2 row C2-13a (#1732): scripts/case2-unit.sh renders and installs the hub's two system units,
// and scripts/case2-precheck.sh checks the host before they start. Brief: docs/plans/2026-10-20_case2-rows.md
// "C2-13a". Every case runs against a scratch unit directory, a scratch home and fake systemctl,
// getent, sudo, id, node, bwrap and claude. Nothing here reads or writes /etc/systemd,
// ~/.mosaic-dev or ~/.config/mosaic-dev, and no fixture value is a real credential.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createServer } from "node:net";
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { businessDoc, writeJson } from "../../business/tests/helpers.mjs";
import { binds, prepareRun, sandboxArgv, sandboxProblem } from "../../seat/src/sandbox.mjs";
import { tmp } from "./helpers.mjs";
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const UNIT = join(REPO, "scripts", "case2-unit.sh");
const PRECHECK = join(REPO, "scripts", "case2-precheck.sh");
const BUS_TEMPLATE = join(REPO, "packages", "cli", "systemd", "mosaic-bus.service.in");
const WEBUI_TEMPLATE = join(REPO, "packages", "webui", "systemd", "mosaic-webui.service.in");
const COPY_A = "a".repeat(40);
const COPY_B = "b".repeat(40);
const UID = process.getuid();
const GID = process.getgid();
// A made-up key value; no output may hold it.
const FIXTURE_KEY = "fixture-zai-key-3c9e51";
// A root-owned executable outside /usr/local/bin, /usr/bin and /bin, for the PATH case.
function outsideNode() {
const dir = "/usr/lib/systemd";
for (const name of readdirSync(dir).sort()) {
const p = join(dir, name);
const s = statSync(p, { throwIfNoEntry: false });
if (s?.isFile() && s.uid === 0 && (s.mode & 0o111) && (s.mode & 0o022) === 0 && realpathSync(p) === p) return p;
}
throw new Error(`no root-owned executable in ${dir}`);
}
const USR_NODE = realpathSync("/usr/bin/node");
const TRUE_BIN = realpathSync("/usr/bin/true");
function script(file, text) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, `#!/usr/bin/env bash\n${text}\n`);
chmodSync(file, 0o755);
return file;
}
// A scratch hub: a home with two copies and a proof file, a unit directory, and fakes.
function hub(t, { uid = UID } = {}) {
const root = tmp(t, "case2-unit-");
const home = join(root, "home", "mosaic");
for (const id of [COPY_A, COPY_B]) {
mkdirSync(join(home, ".local", "share", "mosaic-stack", "copies", id, "scripts"), { recursive: true });
writeFileSync(join(home, ".local", "share", "mosaic-stack", "copies", id, "scripts", "mosaic"), "");
}
const secrets = join(home, ".config", "mosaic-dev", "secrets");
mkdirSync(secrets, { recursive: true, mode: 0o700 });
const proof = join(secrets, "proxy-proof");
writeFileSync(proof, "made-up-proof\n");
chmodSync(proof, 0o600);
const units = join(root, "units");
mkdirSync(units);
const bin = join(root, "bin");
const log = join(root, "systemctl.log");
script(join(bin, "getent"), `case "$1" in
passwd) [ "$2" = mosaic ] && echo "mosaic:x:${uid}:${GID}::${home}:/bin/bash" ;;
group) [ "$2" = "${GID}" ] && echo "mosaic:x:${GID}:" ;;
*) exit 2 ;;
esac`);
// is-active answers yes for each unit named in CASE2_TEST_ACTIVE.
script(join(bin, "systemctl"), `echo "$*" >>'${log}'
if [ "$1" = is-active ]; then case " \${CASE2_TEST_ACTIVE:-} " in *" $3 "*) exit 0 ;; *) exit 3 ;; esac; fi
[ "$*" != "\${CASE2_TEST_FAIL:-}" ]`);
const dataRoot = join(home, '.mosaic-dev'), query = join(root, 'query.json'), sudoLog = join(root, 'sudo.log');
mkdirSync(dataRoot, { recursive: true });
writeJson(query, { dataRoot });
for (const copy of [COPY_A, COPY_B]) {
const src = join(home, '.local/share/mosaic-stack/copies', copy, 'packages');
mkdirSync(join(src, 'cli/src'), { recursive: true }); mkdirSync(join(src, 'webui/src'), { recursive: true });
writeFileSync(join(src, 'cli/src/config.mjs'), `import fs from 'node:fs';export const loadSystem=()=>JSON.parse(fs.readFileSync(${JSON.stringify(query)},'utf8'));`);
writeFileSync(join(src, 'webui/src/cli.mjs'), '// MOSAIC_TESTER_SOCKET_V1\n');
}
script(join(bin, 'sudo'), `${CASE_SUDO_SESSION}
echo "$*" >>'${sudoLog}'
[ "$1" = -u ] && [ "$2" = mosaic ] && [ "$3" = -H ] || exit 97
shift 3
[ "$1" = env ] && [ "$2" = -i ] && [ "$3" = 'HOME=${home}' ] || exit 98
if [ -f '${root}/query-hang' ]; then
trap '' TERM
(trap '' TERM; echo "$BASHPID" >>'${root}/query-pids'; exec sleep 60) &
echo "$BASHPID" >>'${root}/query-pids'; wait; exit
fi
if [ -f '${root}/query-orphan' ]; then
(trap '' TERM; here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here"); printf '%s %s\\n' "$here" "$sid" >'${root}/query-owned'; exec sleep 60) </dev/null >/dev/null 2>&1 &
echo "$!" >'${root}/query-pids'
cat '${root}/query-output'; exit 0
fi
if [ -f '${root}/query-output' ]; then cat '${root}/query-output'; exit "$(cat '${root}/query-status')"; fi
case_sudo_run '${root}' "$@"`);
return { root, home, secrets, proof, units, bin, log, dataRoot, query, sudoLog };
}
const libraryFlags = (h) => ["--port", "443", "--business", "acme", "--public-host", "books.example", "--auth-header", "X-Auth-User",
"--allow-user", "melanie", "--send", "--proof-header", "X-Proxy-Proof", "--proof-file", h.proof];
const factoryFlags = () => ["--port", "443", "--business", "acme", "--tester-socket"];
function unit(h, { profile = "library", node = USR_NODE, copy = COPY_A, extra = [], flags, env = {} } = {}) {
const serve = flags ?? (profile === "factory" ? factoryFlags() : libraryFlags(h));
const args = ["--user", "mosaic", "--home", h.home, "--business", "acme", "--copy", copy, "--node", node, "--profile", profile, ...extra, "--", ...serve];
const r = spawnSync(UNIT, args, {
encoding: "utf8",
timeout: 30000,
env: { PATH: `${h.bin}:${process.env.PATH}`, CASE2_UNIT_DIR: h.units, CASE2_SYSTEMCTL: join(h.bin, "systemctl"), CASE2_GETENT: join(h.bin, "getent"), ...env },
});
return { code: r.status, out: r.stdout, err: r.stderr };
}
const busFile = (h) => join(h.units, "[email protected]");
const webuiFile = (h) => join(h.units, "mosaic-webui.service");
const read = (f) => readFileSync(f, "utf8");
const lines = (text, key) => text.split("\n").filter((l) => l.startsWith(`${key}=`)).map((l) => l.slice(key.length + 1));
const copyDir = (h, id = COPY_A) => join(h.home, ".local", "share", "mosaic-stack", "copies", id);
const HARDENING = (h) => ["ProtectSystem=strict", `ReadWritePaths=${h.home}`, "ProtectHome=yes", "PrivateTmp=yes"];
test('row253 factory socket uses actual config and target-uid query without widening hardening', t=>{
const h=hub(t),custom=join(h.home,'custom');mkdirSync(custom);writeJson(h.query,{dataRoot:custom});
const r=unit(h,{profile:'factory'});assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(lines(read(webuiFile(h)),'ExecStart'),[`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${factoryFlags().join(' ')}`]);
assert.deepEqual(read(webuiFile(h)).split('\n').filter(l=>/^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)=/.test(l)),HARDENING(h));
assert.match(read(h.sudoLog),new RegExp(`^-u mosaic -H env -i HOME=${h.home} PATH=`, 'm'));
assert.equal(existsSync(join(custom,'console')),false,'installer never creates the socket parent');
});
test('row253 installer refuses unsafe location and query facts before unit writes',t=>{
const cases=[
['sibling prefix',h=>writeJson(h.query,{dataRoot:h.home+'2/data'}),/outside.*home/],
['dot',h=>writeJson(h.query,{dataRoot:h.home+'/./data'}),/normalized/],
['dotdot',h=>writeJson(h.query,{dataRoot:h.home+'/x/../data'}),/normalized/],
['double slash',h=>writeJson(h.query,{dataRoot:h.home+'//data'}),/normalized/],
['trailing slash',h=>writeJson(h.query,{dataRoot:h.home+'/data/'}),/normalized/],
['newline',h=>writeJson(h.query,{dataRoot:h.home+'/x\nP10 forged closed'}),/query/],
['non ASCII',h=>writeJson(h.query,{dataRoot:h.home+'/é'}),/query/],
['middle symlink',h=>{symlinkSync(h.dataRoot,join(h.home,'link'));writeJson(h.query,{dataRoot:join(h.home,'link','nested')});},/symlink/],
['long path',h=>writeJson(h.query,{dataRoot:join(h.home,'a'.repeat(108))}),/too.long/],
['parent file',h=>writeFileSync(join(h.dataRoot,'console'),'x',{mode:0o600}),/parent-type/],
['parent symlink',h=>symlinkSync(h.home,join(h.dataRoot,'console')),/symlink/],
['parent mode',h=>{mkdirSync(join(h.dataRoot,'console'));chmodSync(join(h.dataRoot,'console'),0o755);},/parent/],
['foreign parent',h=>{mkdirSync(join(h.dataRoot,'console'),{mode:0o700});},/parent/],
['unsupported',h=>writeFileSync(join(copyDir(h),'packages/webui/src/cli.mjs'),'// old copy'),/support/],
['schema',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true,extra:true}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['wrong type',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:'true'}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['noisy query',h=>{writeFileSync(join(h.root,'query-output'),'noise\n'+JSON.stringify({dataRoot:h.dataRoot,socket:true}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['failed valid query',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));writeFileSync(join(h.root,'query-status'),'1');},/query/],
];
for(const [name,change,why] of cases){
const h=hub(t,{uid:name==='foreign parent'?UID+4242:UID});change(h);
const r=unit(h,{profile:'factory'});refuses(r,why);
assert.equal(existsSync(busFile(h)),false,name);assert.equal(existsSync(webuiFile(h)),false,name);assert.equal(existsSync(h.log),false,name);
assert.equal((r.out+r.err).includes('P10 forged closed'),false,name);
}
});
test('row253 query timeout kills its process group and leaves no running child',async t=>{
const h=hub(t);writeFileSync(join(h.root,'query-hang'),'');
const r=unit(h,{profile:'factory'});refuses(r,/query/);
const pids=read(join(h.root,'query-pids')).trim().split('\n').map(Number);assert.ok(pids.length>=2);
for(const pid of pids){
let state='gone';
for(let n=0;n<50;n++){
try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`query descendant ${pid} still ${state}`);
}
assert.equal(existsSync(h.log),false);assert.equal(existsSync(busFile(h)),false);
});
test('row253 protected homes refuse before any filesystem access',()=>{
const source=read(UNIT), body=source.slice(source.indexOf('tester_location() {'),source.indexOf('# tester-location-end'));
for(const home of ['/home/mosaic','/root/mosaic','/run/user/997','/tmp/mosaic','/var/tmp/mosaic']){
const r=spawnSync('/bin/bash',['-c',body+'\nif tester_location "$1/data" "$1" 997 ""; then exit 99; fi; printf "%s" "$TS_REASON"','--',home],{encoding:'utf8'});
assert.equal(r.status,0);assert.equal(r.stdout,'protected-private-home');
}
});
// C4 cases run through both sudo process models. Exec shares the runner's
// session, so its session-wide check would be vacuous; exact recorded PIDs
// are checked in both modes. Hop additionally isolates the fake session.
for(const kind of ['sleep','signals','member']) test(`row253 C4 ${kind} query has exact inner reason and cleans all members`,async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),sessions=join(h.root,'session-pids'),queries=join(h.root,'query-pids'),files=[sessions,queries];
writeFileSync(join(h.root,'session-mode'),mode);
const member=join(h.root,'q ) '+String.fromCharCode(10)+'mem'),proof=join(h.root,'comm-proof');
if(kind==='member')symlinkSync('/usr/bin/sleep',member);
writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),blockingQuerySource(queries,kind,member,proof));
const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
refuses(r,/^case2-unit: tester config query inner-timeout\n$/);
assert.ok(seconds<7,`${kind}/${mode}: ${seconds}s exceeds outer bound`);
assert.deepEqual(await pollFixtureGone(files),[],mode);
if(kind==='member')assert.ok(read(proof).includes('('+member.split('/').at(-1)+') '),'member comm with newline and closing parenthesis was observed');
assert.equal(existsSync(h.log),false,mode);await stopGuard();
}
});
test('row253 C4 overflow is output-limit in both sudo modes',async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),files=[join(h.root,'session-pids')];writeFileSync(join(h.root,'session-mode'),mode);const capture=cleanupFixture(t,files);
writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:${JSON.stringify(h.dataRoot)}});`);
const r=unit(h,{profile:'factory'});capture();refuses(r,/^case2-unit: tester config query output-limit\n$/);
assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
assert.deepEqual(await pollFixtureGone(files),[]);
}
});
test('row253 C4 stalled supervisor uses the outer reason and explicit residual cleanup',async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),file=join(h.root,'session-pids'),files=[file];writeFileSync(join(h.root,'session-mode'),mode);
writeFileSync(join(h.root,'stall-config'),'');writeFileSync(join(h.root,'stall-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
refuses(r,/^case2-unit: tester config query outer-timeout\n$/);
assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s slack includes root preflight`);
assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
if(mode==='hop')assert.ok(recordedLivePids(file).length>0,'R3 residual exists before fixture cleanup');
killRecordedPids(file);assert.deepEqual(await pollFixtureGone(files),[], 'explicit fixture cleanup of R3 residual');await stopGuard();
}
});
test('row253 C4 inline supervisor is byte-identical in both host scripts',()=>{
const extract=s=>s.split('# row253-supervisor-begin\n')[1]?.split('# row253-supervisor-end')[0];
const a=extract(read(UNIT)),b=extract(read(join(REPO,'scripts/case1-accept.sh')));
assert.ok(a?.length>100);assert.equal(a,b);
});
test('row253 C4 cleanup budget starts after inventory pass one',()=>{
// A deterministic clock models a slow inventory without host-load timing.
// All process reads and signals are stand-ins; no real PID is signalled.
const source=read(UNIT);
const body=source.slice(source.indexOf('stat_fields() {'),source.indexOf('finish() {'));
const standins=`
child=424242; sleep_bin=:; seen=0; clocks=0; kills=0
stat_fields() {
seen=$((seen+1)); ST_STATE=R; ST_SESSION=0
[ "$seen" != 1 ] || ST_SESSION="$child"
return 0
}
clock_cs() { clocks=$((clocks+1)); CLOCK_CS=$((clocks*1000)); }
kill() { kills=$((kills+1)); return 0; }
clean_session; rc=$?
printf '%s' "$kills"
exit "$rc"
`;
const r=spawnSync('/bin/bash',['-c',body+standins],{encoding:'utf8'});
assert.equal(r.status,0,r.stderr);assert.equal(r.stdout,'1');
});
test('row253 query reaps its group even when a child closes stdout before the query returns',async t=>{
const h=hub(t);writeFileSync(join(h.root,'query-orphan'),'');
writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
let pid;
const capture=cleanupFixture(t,[join(h.root,'query-owned')]);
const r=unit(h,{profile:'factory'});capture();
pid=Number(read(join(h.root,'query-pids')).trim());
assert.equal(r.code,0,r.out+r.err);
let state='gone';
for(let n=0;n<50;n++){
try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`query descendant ${pid} still ${state}`);
});
test('row253 config parser is strict',()=>{
const source=read(UNIT), body=source.slice(source.indexOf('tester_facts() {'),source.indexOf('tester_location() {'));
const valid=spawnSync('/bin/bash',['-c',body+'\ntester_facts "$1" && printf "%s %s" "$TS_DATA" "$TS_MARKED"','--','{"dataRoot":"/srv/mosaic/data","socket":true}'],{encoding:'utf8'});
assert.equal(valid.status,0);assert.equal(valid.stdout,'/srv/mosaic/data true');
const facts=['{"dataRoot":"/srv/mosaic/data","socket":true,"extra":1}','{"dataRoot":4,"socket":true}','{"dataRoot":"/srv/mosaic/data","socket":"true"}','{"dataRoot":"/srv/mosaic/data","socket":true}\nP10 forged closed'];
for(const fact of facts){
const r=spawnSync('/bin/bash',['-c',body+'\ntester_facts "$1"','--',fact]);
assert.notEqual(r.status,0,fact);
}
});
function refuses(r, pattern) {
assert.equal(r.code, 1, `expected a refusal, got ${r.code}: ${r.out}${r.err}`);
assert.match(r.err, pattern);
}
// A template copy with one edit, for the template cases.
function template(h, from, name, edit) {
const file = join(h.root, name);
writeFileSync(file, edit(read(from)));
return file;
}
test("a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console", (t) => {
const h = hub(t);
const r = unit(h);
assert.equal(r.code, 0, r.err);
for (const f of [busFile(h), webuiFile(h)]) assert.equal(statSync(f).mode & 0o777, 0o644);
const bus = read(busFile(h));
const webui = read(webuiFile(h));
assert.deepEqual(lines(bus, "Environment"), ["PATH=/usr/bin:/usr/local/bin:/bin"]);
assert.deepEqual(lines(bus, "ExecStart"), [`${copyDir(h)}/scripts/mosaic bus start %i`]);
assert.deepEqual(lines(bus, "EnvironmentFile"), [`-${h.home}/.config/mosaic-dev/secrets/claude-session.env`]);
assert.deepEqual(lines(webui, "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${libraryFlags(h).join(" ")}`]);
assert.deepEqual(lines(webui, "EnvironmentFile"), []);
for (const text of [bus, webui]) {
assert.deepEqual(lines(text, "WantedBy"), ["multi-user.target"]);
assert.deepEqual(lines(text, "User"), ["mosaic"]);
assert.deepEqual(lines(text, "Group"), ["mosaic"]);
assert.deepEqual(lines(text, "NoNewPrivileges"), ["yes"]);
const hardening = text.split("\n").filter((l) => /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)=/.test(l));
assert.deepEqual(hardening, HARDENING(h));
assert.doesNotMatch(text, /@[A-Z_]+@|%h|MOSAIC_CONFIG=|MOSAIC_CLAUDE_BIN|After=|Wants=/);
}
assert.deepEqual(lines(bus, "AmbientCapabilities"), []);
assert.deepEqual(lines(bus, "CapabilityBoundingSet"), []);
assert.deepEqual(lines(webui, "AmbientCapabilities"), ["CAP_NET_BIND_SERVICE"]);
assert.deepEqual(lines(webui, "CapabilityBoundingSet"), ["CAP_NET_BIND_SERVICE"]);
assert.match(r.out, /enabled and started mosaic-bus@acme\.service, then mosaic-webui\.service\n$/);
assert.deepEqual(read(h.log).trim().split("\n"), [
"is-active --quiet [email protected]",
"is-active --quiet mosaic-webui.service",
"daemon-reload",
"enable [email protected] mosaic-webui.service",
"start [email protected]",
"start mosaic-webui.service",
]);
});
test("PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node", (t) => {
const h = hub(t);
const node = outsideNode();
assert.equal(unit(h, { node }).code, 0);
for (const f of [busFile(h), webuiFile(h)]) assert.deepEqual(lines(read(f), "Environment"), [`PATH=${dirname(node)}:/usr/local/bin:/usr/bin:/bin`]);
assert.match(lines(read(webuiFile(h)), "ExecStart")[0], new RegExp(`^${node} `));
});
test("a failed bus start refuses with the Console not started", (t) => {
const h = hub(t);
const r = unit(h, { env: { CASE2_TEST_FAIL: "start [email protected]" } });
refuses(r, /start mosaic-bus@acme\.service failed; the Console is not started/);
assert.deepEqual(read(h.log).trim().split("\n").slice(2), ["daemon-reload", "enable [email protected] mosaic-webui.service", "start [email protected]"]);
});
test("the script never links a unit file", () => {
assert.doesNotMatch(read(UNIT), /systemctl link/);
});
test("factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty", (t) => {
const h = hub(t);
assert.equal(unit(h, { profile: "factory", extra: ["--pm", "--claude", TRUE_BIN] }).code, 0);
const bus = read(busFile(h));
const exec = lines(bus, "ExecStart");
assert.equal(exec.length, 1);
assert.match(exec[0], / bus start %i --pm$/);
assert.equal(exec[0].split("--pm").length, 2);
assert.deepEqual(lines(bus, "Environment"), ["PATH=/usr/bin:/usr/local/bin:/bin", `MOSAIC_CLAUDE_BIN=${TRUE_BIN}`]);
assert.deepEqual(lines(read(webuiFile(h)), "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${factoryFlags().join(" ")}`]);
const plain = hub(t);
assert.equal(unit(plain, { profile: "factory" }).code, 0);
const text = read(busFile(plain));
assert.deepEqual(lines(text, "ExecStart"), [`${copyDir(plain)}/scripts/mosaic bus start %i`]);
assert.doesNotMatch(text, /MOSAIC_CLAUDE_BIN|@PM_FLAGS@|@CLAUDE_ENV@/);
});
test("a re-install over running units says they are still on the old copy and must restart together, never that it started them", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
const both = unit(h, { copy: COPY_B, env: { CASE2_TEST_ACTIVE: "[email protected] mosaic-webui.service" } });
assert.equal(both.code, 0, both.err);
assert.doesNotMatch(both.out, /started/);
assert.match(both.out, new RegExp(`mosaic-bus@acme\\.service was already running, so it is still on copy ${COPY_A}, not copy ${COPY_B}`));
assert.match(both.out, new RegExp(`mosaic-webui\\.service was already running, so it is still on copy ${COPY_A}, not copy ${COPY_B}`));
assert.match(both.out, new RegExp(`restart mosaic-bus@acme\\.service and mosaic-webui\\.service together to put both on copy ${COPY_B}`));
assert.deepEqual(read(h.log).trim().split("\n").slice(-2), ["start [email protected]", "start mosaic-webui.service"]);
// Only the Console ran (the bus had stopped): the bus starts on the new copy, the Console stays.
const one = unit(h, { copy: COPY_A, env: { CASE2_TEST_ACTIVE: "mosaic-webui.service" } });
assert.equal(one.code, 0, one.err);
assert.match(one.out, new RegExp(`started mosaic-bus@acme\\.service on copy ${COPY_A}`));
assert.match(one.out, new RegExp(`mosaic-webui\\.service was already running, so it is still on copy ${COPY_B}, not copy ${COPY_A}`));
assert.doesNotMatch(one.out, /started mosaic-webui/);
assert.match(one.out, /restart mosaic-bus@acme\.service and mosaic-webui\.service together/);
});
test("a re-install to another copy re-renders both units, and an edited installed unit refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
assert.equal(unit(h, { copy: COPY_B }).code, 0);
assert.match(read(busFile(h)), new RegExp(`copies/${COPY_B}/scripts/mosaic`));
assert.doesNotMatch(read(busFile(h)) + read(webuiFile(h)), new RegExp(COPY_A));
const edits = [
[busFile, (s) => s.replace("User=mosaic", "User=root"), /User= must be exactly once in \[Service\], as User=mosaic/],
[webuiFile, (s) => s.replace(/^ExecStart=(.*)$/m, "ExecStart=$1 --board http://127.0.0.1:1"), /ExecStart= must be exactly once/],
[busFile, (s) => s.replace("ProtectHome=yes\n", ""), /ProtectHome= must be exactly once/],
[webuiFile, (s) => s.replace("PrivateTmp=yes\n", ""), /PrivateTmp= must be exactly once/],
[busFile, (s) => s.replace("Type=simple", "Type=simple\nX-Note=%h"), /%h is left/],
];
for (const [file, edit, pattern] of edits) {
const before = read(file(h));
writeFileSync(file(h), edit(before));
refuses(unit(h, { copy: COPY_B }), pattern);
writeFileSync(file(h), before);
}
assert.equal(unit(h, { copy: COPY_B }).code, 0);
});
test("a template with a namespace, user or system call restriction refuses", (t) => {
const h = hub(t);
for (const line of ["RestrictNamespaces=yes", "PrivateUsers=yes", "SystemCallFilter=@system-service"]) {
const key = line.split("=")[0];
for (const [from, env] of [[BUS_TEMPLATE, "CASE2_BUS_TEMPLATE"], [WEBUI_TEMPLATE, "CASE2_WEBUI_TEMPLATE"]]) {
const file = template(h, from, `t-${key}.in`, (s) => s.replace("NoNewPrivileges=yes", `NoNewPrivileges=yes\n${line}`));
refuses(unit(h, { env: { [env]: file } }), new RegExp(`refusing ${key}=`));
}
}
assert.equal(existsSync(busFile(h)), false);
});
test("the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none", (t) => {
const h = hub(t);
const gone = template(h, BUS_TEMPLATE, "no-env.in", (s) => s.replace(/^EnvironmentFile=.*\n/m, ""));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: gone } }), /EnvironmentFile= must be exactly once/);
const elsewhere = template(h, BUS_TEMPLATE, "env-elsewhere.in", (s) => s.replace("EnvironmentFile=-@SESSION_ENV@", "EnvironmentFile=-/etc/other.env"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: elsewhere } }), /EnvironmentFile= must be exactly once/);
const added = template(h, WEBUI_TEMPLATE, "webui-env.in", (s) => s.replace("NoNewPrivileges=yes", "NoNewPrivileges=yes\nEnvironmentFile=-/etc/other.env"));
refuses(unit(h, { env: { CASE2_WEBUI_TEMPLATE: added } }), /refusing EnvironmentFile=/);
});
test("a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses", (t) => {
const h = hub(t);
const token = template(h, BUS_TEMPLATE, "token.in", (s) => s.replace("Type=simple", "Type=simple\nX-Note=@SECRET@"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: token } }), /a token is left after the render: @SECRET@/);
const webuiToken = template(h, WEBUI_TEMPLATE, "wtoken.in", (s) => s.replace("Type=simple", "Type=simple\nX-Note=@SECRET@"));
refuses(unit(h, { env: { CASE2_WEBUI_TEMPLATE: webuiToken } }), /a token other than one @FLAGS@/);
const pso = template(h, BUS_TEMPLATE, "pso.in", (s) => s.replace("Type=simple", "Type=simple\nPermissionsStartOnly=yes"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: pso } }), /refusing PermissionsStartOnly=/);
for (const pre of ["+/srv/mosaic/x", "-+/srv/mosaic/x"]) {
const file = template(h, BUS_TEMPLATE, "pre.in", (s) => s.replace("Type=simple", `Type=simple\nExecStartPre=${pre}`));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: file } }), /an Exec line that runs with full privileges: ExecStartPre=/);
}
});
test("a symlink at either unit path refuses", (t) => {
for (const name of ["[email protected]", "mosaic-webui.service"]) {
const h = hub(t);
const target = join(h.root, "elsewhere");
writeFileSync(target, "");
symlinkSync(target, join(h.units, name));
refuses(unit(h), new RegExp(`${name.replace(".", "\\.")} is a symlink`));
}
});
test("a --node that is relative, not root-owned or under <home> refuses", (t) => {
const h = hub(t);
refuses(unit(h, { node: "bin/node" }), /--node must be an absolute path/);
const mine = script(join(h.root, "opt", "node"), "exit 0");
refuses(unit(h, { node: mine }), /--node: .* is not owned by root/);
const inside = script(join(h.home, "bin", "node"), "exit 0");
refuses(unit(h, { node: inside }), /--node must be outside/);
});
test("a re-install with a different --node or different serve flags refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
// A new node moves PATH too, so the installed unit fails its own Environment= check first.
refuses(unit(h, { node: outsideNode() }), /installed .*: the Environment= lines must be exactly/);
const flags = libraryFlags(h).map((f) => (f === "melanie" ? "someone" : f));
refuses(unit(h, { flags }), /installed .*: ExecStart= must be exactly once/);
// A line check_unit doesn't fix still has to equal the render.
writeFileSync(busFile(h), read(busFile(h)).replace(/^Description=.*$/m, "Description=edited"));
refuses(unit(h), /differs from the render in more than the copy id/);
});
test("an installed pair whose units name different copies refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
writeFileSync(webuiFile(h), read(webuiFile(h)).replaceAll(COPY_A, COPY_B));
refuses(unit(h), new RegExp(`the installed units name different copies, ${COPY_A} and ${COPY_B}`));
});
test("a copy id that is short, uppercase, holds / or .., or names no directory refuses", (t) => {
const h = hub(t);
for (const copy of ["a".repeat(39), "A".repeat(40), `${"a".repeat(19)}/${"a".repeat(20)}`, `..${"a".repeat(38)}`]) {
refuses(unit(h, { copy }), /--copy must be a full 40-character lower-case commit id/);
}
refuses(unit(h, { copy: "c".repeat(40) }), /no copy directory/);
});
test("library refuses a privileged-floor miss, a bad proof pair and every flag outside its set", (t) => {
const h = hub(t);
const base = libraryFlags(h);
const without = (...names) => {
const out = [];
for (let i = 0; i < base.length; i++) {
if (names.includes(base[i])) { if (base[i] !== "--send") i++; continue; }
out.push(base[i]);
}
return out;
};
refuses(unit(h, { flags: base.map((f) => (f === "443" ? "7332" : f)) }), /--port must be 1\.\.1023/);
refuses(unit(h, { flags: without("--proof-header", "--proof-file") }), /serve flag --proof-header is required/);
refuses(unit(h, { flags: base.map((f) => (f === "X-Auth-User" ? "X-Authentik-Uid" : f)) }), /must be an Authentik uid/);
assert.equal(unit(h, { flags: base.map((f) => (f === "X-Auth-User" ? "X-Authentik-Uid" : f === "melanie" ? "0".repeat(64) : f)) }).code, 0);
rmSync(busFile(h));
rmSync(webuiFile(h));
for (const [flag, value, pattern] of [
["--tester-port", "7333", /case2-unit.sh retires tester TCP; use --tester-socket/],
["--tester-socket", null, /the library profile refuses --tester-socket/],
["--checkout", "/srv/x", /refusing --checkout/],
["--board", "http://127.0.0.1:7331", /refusing --board/],
["--sso", null, /refusing --sso/],
["--nonsense", "1", /unknown serve flag: --nonsense/],
]) {
refuses(unit(h, { flags: [...base, flag, ...(value === null ? [] : [value])] }), pattern);
}
refuses(unit(h, { flags: base.map((f) => (f === "acme" ? "other" : f)) }), /serve --business other is not the unit's business acme/);
refuses(unit(h, { extra: ["--pm"] }), /the library profile refuses --pm/);
refuses(unit(h, { extra: ["--claude", TRUE_BIN] }), /the library profile refuses --claude/);
chmodSync(h.proof, 0o644);
refuses(unit(h), /mode 0600 or 0400, not 644/);
chmodSync(h.proof, 0o600);
const link = join(h.secrets, "proof-link");
symlinkSync(h.proof, link);
refuses(unit(h, { flags: base.map((f) => (f === h.proof ? link : f)) }), /must not be a symlink/);
const dataRoot = join(h.home, ".mosaic-dev");
mkdirSync(dataRoot, { recursive: true });
const outside = join(dataRoot, "proof");
writeFileSync(outside, "made-up-proof\n");
chmodSync(outside, 0o600);
refuses(unit(h, { flags: base.map((f) => (f === h.proof ? outside : f)) }), /must be under .*secrets, outside the data root/);
assert.equal(existsSync(busFile(h)), false);
});
// A non-root test can't make a root-owned file under the scratch home, so the
// owner case names another uid in the passwd database instead.
test("library refuses a proof file not owned by <user>'s uid", (t) => {
const h = hub(t, { uid: UID + 4242 });
refuses(unit(h), new RegExp(`proxy proof file must be owned by mosaic \\(uid ${UID + 4242}\\)`));
});
test("factory refuses duplicate socket, a missing --port or --business, and each proxy flag", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), '--tester-socket'] }), /given twice/);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), '--port', '443'] }), /given twice/);
refuses(unit(h, { profile: "factory", flags: ["--business", "acme"] }), /serve flag --port is required/);
refuses(unit(h, { profile: "factory", flags: ["--port", "443"] }), /serve flag --business is required/);
for (const [flag, value] of [["--public-host", "x.example"], ["--auth-header", "X-A"], ["--allow-user", "u"], ["--proof-header", "X-P"], ["--proof-file", h.proof], ["--send", null]]) {
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), new RegExp(`the factory profile refuses ${flag}`));
}
for (const [flag, value, pattern] of [["--checkout", "/srv/x", /refusing --checkout/], ["--board", "http://127.0.0.1:7331", /refusing --board/], ["--sso", null, /refusing --sso/], ["--nonsense", "1", /unknown serve flag/]]) {
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), pattern);
}
});
test("row253 factory refuses retired TCP and a value after the socket marker", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);
refuses(unit(h, { profile: 'factory', flags: [...factoryFlags(), '7333'] }), /unknown serve flag: 7333/);
});
test("factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", extra: ["--claude", "bin/claude"] }), /--claude must be an absolute path/);
const mine = script(join(h.root, "opt", "claude"), "exit 0");
refuses(unit(h, { profile: "factory", extra: ["--claude", mine] }), /--claude: .* is not owned by root/);
const inside = script(join(h.home, "bin", "claude"), "exit 0");
refuses(unit(h, { profile: "factory", extra: ["--claude", inside] }), /--claude must be outside/);
assert.equal(unit(h, { profile: "factory", extra: ["--pm", "--claude", TRUE_BIN] }).code, 0);
refuses(unit(h, { profile: "factory", extra: ["--claude", TRUE_BIN] }), /--pm and --claude must equal the installed ones|ExecStart= must be exactly once/);
refuses(unit(h, { profile: "factory", extra: ["--pm"] }), /Environment= lines must be exactly|differs from the render/);
refuses(unit(h, { profile: "factory", extra: ["--pm", "--claude", USR_NODE] }), /Environment= lines must be exactly|differs from the render/);
});
// ---- case2-precheck.sh ----
// A scratch user for the precheck: a config, a business file, an installed Console unit,
// fakes on PATH and a fake /proc/sys.
function host(t, { roles = {}, vars = {}, keyFile = `ZAI_API_KEY=${FIXTURE_KEY}\n`, port = "443", busClaude = null } = {}) {
const root = tmp(t, "case2-pre-");
const home = join(root, "home");
const configDir = join(home, ".config", "mosaic-dev");
const config = writeJson(join(configDir, "config.json"), {
configVersion: 1,
environment: "production",
dataRoot: join(home, ".mosaic-dev"),
execution: { backend: "docker", provider: "zai", model: "glm-5.3-flash", adapter: "pi" },
});
const doc = businessDoc(root);
doc.roles.cto.vars = { harness: "pi" };
for (const [name, v] of Object.entries(roles)) doc.roles[name].vars = v;
Object.assign(doc.vars, vars);
writeJson(join(configDir, "businesses", "acme.json"), doc);
const secrets = join(configDir, "secrets");
mkdirSync(secrets, { recursive: true, mode: 0o700 });
if (keyFile !== null) {
writeFileSync(join(secrets, "claude-session.env"), keyFile);
chmodSync(join(secrets, "claude-session.env"), 0o600);
}
const units = join(root, "units");
mkdirSync(units);
writeFileSync(join(units, "mosaic-webui.service"), `[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --port ${port} --business acme\n`);
writeFileSync(join(units, "[email protected]"), `[Service]\nEnvironment=PATH=/usr/bin\n${busClaude ? `Environment=MOSAIC_CLAUDE_BIN=${busClaude}\n` : ""}`);
const sys = join(root, "sys");
mkdirSync(join(sys, "net", "ipv4"), { recursive: true });
mkdirSync(join(sys, "kernel"), { recursive: true });
writeFileSync(join(sys, "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
writeFileSync(join(sys, "kernel", "apparmor_restrict_unprivileged_userns"), "1\n");
const bin = join(root, "bin");
const me = spawnSync("id", ["-un"], { encoding: "utf8" }).stdout.trim();
script(join(bin, "getent"), `[ "$1" = passwd ] && [ "$2" = '${me}' ] && echo '${me}:x:${UID}:${GID}::${home}:/bin/bash'`);
script(join(bin, "sudo"), "exit 1");
script(join(bin, "id"), `if [ "$1" = -G ]; then echo "\${CASE2_TEST_GROUPS:-${GID}}"; else exec /usr/bin/id "$@"; fi`);
script(join(bin, "node"), `if [ "$1" = --version ]; then echo "\${CASE2_TEST_NODE:-v24.3.0}"; exit 0; fi\nexec '${process.execPath}' "$@"`);
script(join(bin, "bwrap"), `exit "\${CASE2_TEST_BWRAP:-0}"`);
return { root, home, configDir, config, secrets, units, sys, bin, dataRoot: join(home, ".mosaic-dev") };
}
// A fake claude: replace answers with the marker and a small prompt; append answers with a large one;
// nomarker answers with a small prompt and no marker; text answers with plain text, not JSON.
function fakeClaude(dir, kind) {
return script(join(dir, "claude"), `[ "$1" = --version ] && { echo "9.9.9 (Claude Code, ${kind} fake)"; exit 0; }
file=''
while [ $# -gt 0 ]; do [ "$1" = --system-prompt-file ] && file="$2"; shift; done
marker="$(awk '{ print $NF }' "$file")"
${kind === "nomarker" ? "marker=something-else" : ""}
${kind === "text" ? 'echo "$marker"; exit 0' : ""}
tokens=${kind === "append" ? 9000 : 40}
printf '{"result":"%s","usage":{"input_tokens":%s,"cache_read_input_tokens":0}}\\n' "$marker" "$tokens"`);
}
// This host may have a claude in /usr/bin, so this PATH links every /usr/bin entry but that one.
function pathWithoutClaude(x) {
const usr = join(x.root, "usr-bin");
mkdirSync(usr);
for (const name of readdirSync("/usr/bin")) if (name !== "claude") symlinkSync(join("/usr/bin", name), join(usr, name));
return `${x.bin}:${usr}`;
}
function precheck(x, { profile = "library", env = {}, path } = {}) {
const r = spawnSync(PRECHECK, ["--business", "acme", "--profile", profile], {
encoding: "utf8",
timeout: 60000,
env: {
PATH: path ?? `${x.bin}:/usr/bin:/bin`,
HOME: x.home,
MOSAIC_CONFIG: x.config,
CASE2_UNIT_DIR: x.units,
CASE2_PROC_SYS: x.sys,
CASE2_GETENT: join(x.bin, "getent"),
...env,
},
});
const out = r.stdout + r.stderr;
assert.doesNotMatch(out, new RegExp(FIXTURE_KEY), "the precheck printed the fixture key");
return { code: r.status, out };
}
function blocked(r, pattern) {
assert.equal(r.code, 1, r.out);
assert.match(r.out, pattern);
}
test("the precheck passes on a good host and prints free memory, without the key", (t) => {
const x = host(t);
const r = precheck(x);
assert.equal(r.code, 0, r.out);
assert.match(r.out, /ok {6}config validates/);
assert.match(r.out, /ok {6}bus\.tester is off/);
assert.match(r.out, /ok {6}net\.ipv4\.ip_unprivileged_port_start 1024 is above the Console's port 443/);
assert.match(r.out, /claude-session\.env sets ZAI_API_KEY/);
assert.match(r.out, /free memory: \d+ MiB/);
assert.doesNotMatch(r.out, /claude .*fake|marker/);
});
test("the precheck refuses each failed host check", (t) => {
blocked(precheck(host(t), { env: { CASE2_TEST_NODE: "v22.22.2" } }), /REFUSED node v22\.22\.2 is older than 24/);
blocked(precheck(host(t), { env: { CASE2_TEST_GROUPS: `${GID} 998` } }), /REFUSED id -G prints more than one group/);
const sudo = host(t);
script(join(sudo.bin, "sudo"), "exit 0");
blocked(precheck(sudo), /REFUSED sudo -n true succeeds/);
blocked(precheck(host(t), { env: { XDG_DATA_HOME: "/elsewhere" } }), /REFUSED XDG_DATA_HOME is \/elsewhere/);
const home = host(t);
assert.equal(precheck(home, { env: { XDG_DATA_HOME: `${home.home}/.local/share` } }).code, 0);
blocked(precheck(host(t), { env: { HOME: "/tmp" } }), /REFUSED HOME is \/tmp/);
const runs = host(t);
mkdirSync(join(runs.dataRoot, "launches", "acme"), { recursive: true, mode: 0o755 });
chmodSync(join(runs.dataRoot, "launches", "acme"), 0o755);
blocked(precheck(runs), /REFUSED run root .* is not a 0700 directory \(mode 755\)/);
chmodSync(join(runs.dataRoot, "launches", "acme"), 0o700);
assert.match(precheck(runs).out, /ok {6}run root .* is 0700/);
const bad = host(t);
writeFileSync(bad.config, "{ not json");
blocked(precheck(bad), /REFUSED config:/);
});
test("the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root", (t) => {
const nouser = host(t);
script(join(nouser.bin, "getent"), "exit 2");
blocked(precheck(nouser), /REFUSED user: no .* in the passwd database/);
const unparsed = host(t);
writeFileSync(join(unparsed.configDir, "businesses", "acme.json"), "{ not json");
blocked(precheck(unparsed), /REFUSED business file: cannot read .*acme\.json/);
const gone = host(t);
rmSync(join(gone.configDir, "businesses", "acme.json"));
blocked(precheck(gone), /REFUSED business file: cannot read .*acme\.json: ENOENT/);
const invalid = host(t);
const file = join(invalid.configDir, "businesses", "acme.json");
const doc = JSON.parse(read(file));
doc.notAField = true;
writeJson(file, doc);
const r = precheck(invalid);
blocked(r, /REFUSED business file: /);
assert.doesNotMatch(r.out, /REFUSED business file: cannot read/);
// stat follows a link, so a link to a 0700 directory would read as 0700.
const link = host(t);
const target = join(link.root, "elsewhere");
mkdirSync(target, { mode: 0o700 });
chmodSync(target, 0o700);
mkdirSync(join(link.dataRoot, "launches"), { recursive: true });
symlinkSync(target, join(link.dataRoot, "launches", "acme"));
blocked(precheck(link), /REFUSED run root .* is a symlink/);
});
test("a bwrap that fails refuses and prints the sysctl and where bwrap is", (t) => {
const x = host(t);
blocked(precheck(x, { env: { CASE2_TEST_BWRAP: "1" } }), new RegExp(`REFUSED bwrap .* failed; bwrap: ${join(x.bin, "bwrap")}; kernel\\.apparmor_restrict_unprivileged_userns: 1`));
});
test("an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit", (t) => {
for (const floor of ["443", "80"]) {
const x = host(t);
writeFileSync(join(x.sys, "net", "ipv4", "ip_unprivileged_port_start"), `${floor}\n`);
blocked(precheck(x), new RegExp(`REFUSED net\\.ipv4\\.ip_unprivileged_port_start is ${floor}, at or below the Console's port 443`));
}
const x = host(t);
rmSync(join(x.units, "mosaic-webui.service"));
blocked(precheck(x), /REFUSED no installed Console unit/);
const noPort = host(t);
writeFileSync(join(noPort.units, "mosaic-webui.service"), "[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --business acme\n");
blocked(precheck(noPort), /REFUSED .*mosaic-webui\.service has no --port in its ExecStart/);
const noFloor = host(t);
rmSync(join(noFloor.sys, "net", "ipv4", "ip_unprivileged_port_start"));
blocked(precheck(noFloor), /REFUSED cannot read net\.ipv4\.ip_unprivileged_port_start/);
});
test("bus.tester: library refuses it on, factory needs it on, and factory refuses a library business", (t) => {
blocked(precheck(host(t, { vars: { "bus.tester": true } })), /REFUSED bus\.tester is on; the library profile needs it off/);
blocked(precheck(host(t), { profile: "factory" }), /REFUSED bus\.tester is off; the factory profile needs it on/);
const on = precheck(host(t, { vars: { "bus.tester": true } }), { profile: "factory" });
assert.equal(on.code, 0, on.out);
const lib = host(t, { vars: { "bus.tester": true } });
const file = join(lib.configDir, "businesses", "acme.json");
const doc = JSON.parse(read(file));
doc.projects.stack.kind = "library";
writeJson(file, doc);
blocked(precheck(lib, { profile: "factory" }), /REFUSED profile-library-business: .* projects stack are library projects/);
});
test("the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong", (t) => {
// No claude-code instance and no claude anywhere on PATH: the precheck passes.
const none = host(t);
const r = precheck(none, { path: pathWithoutClaude(none) });
assert.equal(r.code, 0, r.out);
assert.doesNotMatch(r.out, /--system-prompt-file|claude is installed/);
const good = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(good.bin, "replace");
const pass = precheck(good);
assert.equal(pass.code, 0, pass.out);
assert.match(pass.out, /claude .*9\.9\.9 \(Claude Code, replace fake\)/);
assert.match(pass.out, /ok {6}claude replaces its prompt with --system-prompt-file \(40 input tokens\)/);
const bad = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(bad.bin, "append");
blocked(precheck(bad), /REFUSED claude adds to its own prompt with --system-prompt-file: 9000 input tokens/);
const nomarker = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(nomarker.bin, "nomarker");
blocked(precheck(nomarker), /REFUSED claude's reply to the marker prompt lacks the marker \(40 input tokens\)/);
const text = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(text.bin, "text");
blocked(precheck(text), /REFUSED claude gave no JSON reply to the marker prompt/);
const missing = host(t, { roles: { cto: { harness: "claude-code" } } });
blocked(precheck(missing, { path: pathWithoutClaude(missing) }), /REFUSED an instance runs claude-code and no claude is installed/);
});
test("the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH", (t) => {
const pinned = join(tmp(t, "case2-claude-"), "pinned");
const replace = fakeClaude(pinned, "replace");
const x = host(t, { roles: { cto: { harness: "claude-code" } }, busClaude: replace });
fakeClaude(x.bin, "append");
const r = precheck(x);
assert.equal(r.code, 0, r.out);
assert.match(r.out, /replace fake/);
const pinnedBad = join(tmp(t, "case2-claude-"), "pinned");
const append = fakeClaude(pinnedBad, "append");
const y = host(t, { roles: { cto: { harness: "claude-code" } }, busClaude: append });
fakeClaude(y.bin, "replace");
blocked(precheck(y), /REFUSED claude adds to its own prompt/);
});
test("the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line", (t) => {
blocked(precheck(host(t, { keyFile: null })), /REFUSED an instance's provider is zai and .*claude-session\.env is missing/);
const open = host(t);
chmodSync(join(open.secrets, "claude-session.env"), 0o644);
blocked(precheck(open), /REFUSED .*claude-session\.env must be owned by .* at 0600 \(mode 644\)/);
const link = host(t, { keyFile: null });
const real = join(link.root, "real.env");
writeFileSync(real, `ZAI_API_KEY=${FIXTURE_KEY}\n`);
chmodSync(real, 0o600);
symlinkSync(real, join(link.secrets, "claude-session.env"));
blocked(precheck(link), /REFUSED .*claude-session\.env is a symlink/);
blocked(precheck(host(t, { keyFile: "ZAI_API_KEY=\n" })), /REFUSED .*sets no nonempty ZAI_API_KEY/);
blocked(precheck(host(t, { keyFile: `ZAI_API_KEY=${FIXTURE_KEY}\nZAI_API_KEY=\n` })), /REFUSED .*sets no nonempty ZAI_API_KEY/);
const quoted = precheck(host(t, { keyFile: `ZAI_API_KEY=\nZAI_API_KEY="${FIXTURE_KEY}"\n` }));
assert.equal(quoted.code, 0, quoted.out);
});
// ---- the bind test (mos-1100) ----
// A session can't take the Console's port: inside the sandbox a real session gets, a bind below
// 1024 fails with EACCES, and the same bind on a free port of 1024 or above succeeds.
const sandboxSkip = sandboxProblem() ?? false;
const LOW_PORT = 980;
test("inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds", { skip: sandboxSkip, timeout: 30000 }, async (t) => {
const floor = Number(read("/proc/sys/net/ipv4/ip_unprivileged_port_start").trim());
assert.ok(floor > LOW_PORT, `this host's net.ipv4.ip_unprivileged_port_start is ${floor}, at or below the test port ${LOW_PORT}, so the test can't show the sandbox refuses it`);
const root = tmp(t, "c2b-");
const runDir = join(root, "run");
const workspace = join(root, "ws");
mkdirSync(runDir);
mkdirSync(workspace);
mkdirSync(join(runDir, "bundle"));
writeFileSync(join(runDir, "session.json"), "{}");
const adapter = join(root, "adapter.sh");
const brokerSocket = join(root, "broker.sock");
writeFileSync(adapter, "");
writeFileSync(brokerSocket, "");
prepareRun(runDir);
const list = binds({ runDir, workspace, adapter, harnessMounts: [], brokerSocket });
const high = await new Promise((done) => {
const s = createServer().listen(0, "127.0.0.1", () => { const { port } = s.address(); s.close(() => done(port)); });
});
const bindIn = (port) => {
const code = `require("net").createServer().on("error", (e) => { console.log(e.code); process.exit(0); }).listen(${port}, "127.0.0.1", function () { console.log("bound"); this.close(); });`;
const argv = sandboxArgv(list, runDir, [process.execPath, "-e", code], { business: "acme", role: "coder" });
const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf8", timeout: 10000 });
assert.equal(r.status, 0, r.stderr);
return r.stdout.trim();
};
assert.equal(bindIn(LOW_PORT), "EACCES");
assert.equal(bindIn(high), "bound");
});
@@ -0,0 +1,133 @@
# row253-supervisor-begin
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
stat_fields() {
local pid="$1" raw="" rest
# EOF is expected: stat has no NUL. Decide on content, not read's status.
IFS= read -r -d '' raw < "/proc/$pid/stat" 2>/dev/null || :
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
[[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
}
clock_cs() {
local boot spare
read -r boot spare < /proc/uptime || return 1
[[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
}
clean_session() {
local entry pid rc live until=""
while :; do
live=0
for entry in /proc/[0-9]*/stat; do
pid="${entry%/stat}"; pid="${pid##*/}"
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" != 3 ] || continue
[ "$rc" = 0 ] || return 1
[ "$ST_SESSION" = "$child" ] || continue
case "$ST_STATE" in Z|X) continue ;; esac
live=1
if ! kill -KILL "$pid" 2>/dev/null; then
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
fi
done
[ "$live" != 0 ] || return 0
clock_cs || return 1
# Inventory pass 1 is outside the bounded confirmation/retry budget.
[ -n "$until" ] || until=$((CLOCK_CS + 25))
[ "$CLOCK_CS" -lt "$until" ] || return 1
"$sleep_bin" 0.01
done
}
finish() {
local rc="$?"
trap - EXIT HUP INT TERM
if [ "$verified" = 1 ]; then
clean_session || rc=86
elif [ -n "$child" ]; then
kill -KILL "$child" 2>/dev/null || :
fi
[ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
"$setsid_bin" "$@" & child=$!
# Allow session creation the whole inner budget, rather than a 200 ms margin.
while :; do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 85
if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
tester_tools() {
local input target p st magic
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
p="$target"
while :; do
st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
[ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
[ "$p" != / ] || break
p="${p%/*}"; [ -n "$p" ] || p=/
done
magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
case "$input" in
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
/usr/bin/head) TS_HEAD="$target" ;;
esac
done
}
tester_capture_reason() {
case "$1" in
81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
*) echo inner-command ;;
esac
}
tester_capture() (
local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
tester_tools || exit 84
file="$(mktemp)" || exit 88
notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
trap 'exit 88' HUP INT TERM
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
set -o pipefail
maximum="$1"; head_bin="$2"; shift 2
"$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
# Only GNU timeout and the trusted head can write this root-side channel.
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
[ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
[ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
cat -- "$file"
)
+322
View File
@@ -0,0 +1,322 @@
[
{
"id": "M1",
"file": "scripts/case2-unit.sh",
"old": " --tester-port) die \"case2-unit.sh retires tester TCP; use --tester-socket\" ;;",
"new": "",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 factory refuses retired TCP"
},
{
"id": "M2",
"file": "scripts/case2-unit.sh",
"old": "[ \"$PROFILE\" = factory ] || die \"the library profile refuses --tester-socket\"",
"new": ":",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "library refuses a privileged-floor miss"
},
{
"id": "M3",
"file": "scripts/case2-unit.sh",
"old": " [ -z \"${seen[$name]:-}\" ] || die \"serve flag $name given twice\"",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "factory refuses duplicate socket"
},
{
"id": "M4",
"file": "scripts/case2-unit.sh",
"old": " [ \"$rc\" = 0 ] || { case \"$rc\" in 81|85|86|87|88) exit \"$rc\" ;; *) exit 87 ;; esac; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M5",
"file": "scripts/case2-unit.sh",
"old": " [ \"$TS_MARKED\" = true ] || die \"the target does not support --tester-socket\"",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M6",
"file": "scripts/case2-unit.sh",
"old": "case \"$data\" in \"$home\"/*)",
"new": "case \"$data\" in \"$home\"*)",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M7",
"file": "scripts/case2-unit.sh",
"old": "case \"$home/\" in /home/*",
"new": "case /allowed/ in /home/*",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 protected homes"
},
{
"id": "M8",
"file": "scripts/case2-unit.sh",
"old": " [ \"${#TS_SOCKET}\" -le 107 ] || { TS_REASON=too-long; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M9",
"file": "scripts/case2-unit.sh",
"old": " [ -d \"$parent\" ] || { TS_REASON=parent-type; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M10",
"file": "scripts/case2-unit.sh",
"old": " [ \"${st% *}\" = \"$uid\" ] || { TS_REASON=parent-owner; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M11",
"file": "scripts/case2-unit.sh",
"old": " [[ \"${st#* }\" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M12",
"file": "scripts/case1-accept.sh",
"old": " --tester-port) refuse \"case1-accept.sh retires tester TCP; use --tester-socket\" ;;",
"new": "",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "a bad args file refuses"
},
{
"id": "M13",
"file": "scripts/case1-accept.sh",
"old": "[ \"$TESTER_SOCKET\" = 1 ] || refuse \"$ARGS_FILE needs one bare --tester-socket after --\"",
"new": ":",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "a bad args file refuses"
},
{
"id": "M14",
"file": "scripts/case1-accept.sh",
"old": " *) refuse \"unknown serve flag $a\" ;;",
"new": " *) : ;;",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "a bad args file refuses"
},
{
"id": "M15",
"file": "scripts/case1-accept.sh",
"old": " [ \"$rc\" = 0 ] || { case \"$rc\" in 81|85|86|87|88) exit \"$rc\" ;; *) exit 87 ;; esac; }",
"new": " :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 acceptance rejects bad config"
},
{
"id": "M16",
"file": "scripts/case1-accept.sh",
"old": "other=\"$(listeners 7331)$(listeners 7333)\"",
"new": "other=\"$(listeners 7331)\"",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P8 refuses"
},
{
"id": "M17",
"file": "scripts/case1-accept.sh",
"old": " [[ \"$p\" =~ ^$MOSAIC_UID\\ 700\\ directory\\ [0-9]+\\ [0-9]+$ ]] || return 1",
"new": " :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M18",
"file": "scripts/case1-accept.sh",
"old": " [[ \"$s\" =~ ^$MOSAIC_UID\\ 600\\ socket\\ [0-9]+\\ [0-9]+$ ]] || return 1",
"new": " :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M19",
"file": "scripts/case1-accept.sh",
"old": "[ \"$pid\" = \"$CON_PID\" ] && [ \"$uid\" = \"$MOSAIC_UID\" ]",
"new": "true && [ \"$uid\" = \"$MOSAIC_UID\" ]",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M20",
"file": "scripts/case1-accept.sh",
"old": "[ \"$pid\" = \"$CON_PID\" ] && [ \"$uid\" = \"$MOSAIC_UID\" ]",
"new": "[ \"$pid\" = \"$CON_PID\" ] && true",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M21",
"file": "scripts/case1-accept.sh",
"old": "pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})$'",
"new": "pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})'",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M22",
"file": "scripts/case1-accept.sh",
"old": " magic=\"$(od -An -tx1 -N4 -- \"$ROOT$real\" 2>/dev/null | tr -d ' \\n')\"\n [ \"$magic\" = 7f454c46 ] || return 1",
"new": " magic=\"$(od -An -tx1 -N4 -- \"$ROOT$real\" 2>/dev/null | tr -d ' \\n')\"\n :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M23",
"file": "scripts/case1-accept.sh",
"old": " [ \"$before\" = \"$after\" ] || why=changed",
"new": " :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M24",
"file": "scripts/case2-unit.sh",
"old": "sudo -u \"$user\" -H env -i",
"new": "sudo -H env -i",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 factory socket uses actual config"
},
{
"id": "M25",
"file": "scripts/case1-accept.sh",
"old": "tester_capture 2 4 128 \"$S_USER\" \"$S_HOME\"",
"new": "tester_capture 2 4 128 wrong-user \"$S_HOME\"",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 socket observation uses configured root"
},
{
"id": "M26",
"file": "scripts/case1-accept.sh",
"old": " [[ \"$floor\" =~ ^[0-9]{1,5}$ ]] && [ \"$floor\" -gt \"$PORT\" ] || why=floor",
"new": " :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M27",
"file": "scripts/case1-accept.sh",
"old": "p=struct.unpack(\"3i\",s.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))",
"new": "import os; p=(os.getpid(),os.getuid(),os.getgid())",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 socket observation uses configured root"
},
{
"id": "M28",
"file": "scripts/case2-unit.sh",
"old": " [ ! -L \"$parent\" ] || { TS_REASON=symlink-parent; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M29",
"file": "scripts/case2-unit.sh",
"old": " [ ! -L \"$prefix$at\" ] || { TS_REASON=symlink-component; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M30",
"file": "scripts/case2-unit.sh",
"old": " case \"$data/\" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M31",
"file": "scripts/case2-unit.sh",
"old": " [[ \"$1\" =~ $pattern ]] || return 1",
"new": " [[ \"$1\" =~ \\\"dataRoot\\\":\\\"([^\\\"]+)\\\",\\\"socket\\\":(true|false) ]] || return 1",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 config parser is strict"
},
{
"id": "M32",
"file": "scripts/case2-unit.sh",
"old": "[ \"$(wc -c < \"$file\")\" -le \"$maximum\" ]",
"new": "true",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 overflow is output-limit"
},
{
"id": "M33",
"file": "scripts/case2-unit.sh",
"old": "\"$TS_TIMEOUT\" --verbose --signal=KILL \"${outer}s\"",
"new": "\"$TS_TIMEOUT\" --foreground --verbose --signal=KILL \"${outer}s\"",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 query reaps its group"
},
{
"id": "M34",
"file": "scripts/case2-unit.sh",
"old": "seconds=\"$1\" setsid_bin=\"$2\" sleep_bin=\"$3\"; shift 3",
"new": "seconds=\"$1\" setsid_bin=\"$2\" sleep_bin=\"$3\"; shift 3\nexec \"$@\"",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 signals query"
},
{
"id": "M35",
"file": "scripts/case2-unit.sh",
"old": "LC_ALL=C \"$TS_TIMEOUT\" --verbose --signal=KILL \"${outer}s\" \"$TS_BASH\" -c",
"new": "\"$TS_BASH\" -c",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 stalled supervisor"
},
{
"id": "M36",
"file": "scripts/case2-unit.sh",
"old": " [ \"$ST_SESSION\" = \"$child\" ] || continue",
"new": " [ \"$ST_SESSION\" = \"$child\" ] || continue\n [ \"$ST_GROUP\" = \"$child\" ] || continue",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 member query"
},
{
"id": "M37",
"file": "scripts/case2-unit.sh",
"old": " LC_ALL=C \"$TS_TIMEOUT\" --verbose --signal=KILL \"${outer}s\" \"$TS_BASH\" -c '\n set -o pipefail\n maximum=\"$1\"; head_bin=\"$2\"; shift 2\n \"$@\" 2>/dev/null | \"$head_bin\" -c \"$((maximum + 1))\"\n ' -- \"$maximum\" \"$TS_HEAD\" sudo -u \"$user\" -H env -i \"HOME=$home\" \"PATH=$path\" \"$TS_BASH\" -c \"$TESTER_SUPERVISOR\" -- \"$inner\" \"$TS_SETSID\" \"$TS_SLEEP\" \"$@\" > \"$file\" 2> \"$notice\" &\n child=$!\n wait \"$child\" 2>/dev/null || rc=$?\n kill -KILL -- \"-$child\" 2>/dev/null || :\n # Only GNU timeout and the trusted head can write this root-side channel.\n if [ \"$rc\" = 137 ] && [[ \"$(<\"$notice\")\" == *': sending signal KILL to command '* ]]; then exit 82; fi\n [ \"$(wc -c < \"$file\")\" -le \"$maximum\" ] || exit 83\n [ \"$rc\" = 0 ] || { case \"$rc\" in 81|85|86|87|88) exit \"$rc\" ;; *) exit 87 ;; esac; }",
"new": " LC_ALL=C \"$TS_TIMEOUT\" --verbose --signal=KILL \"${outer}s\" \"$TS_BASH\" -c '\n set -o pipefail\n maximum=\"$1\"; head_bin=\"$2\"; shift 2\n \"$@\" 2>/dev/null | cat\n ' -- \"$maximum\" \"$TS_HEAD\" sudo -u \"$user\" -H env -i \"HOME=$home\" \"PATH=$path\" \"$TS_BASH\" -c \"$TESTER_SUPERVISOR\" -- \"$inner\" \"$TS_SETSID\" \"$TS_SLEEP\" \"$@\" > \"$file\" 2> \"$notice\" &\n child=$!\n wait \"$child\" 2>/dev/null || rc=$?\n kill -KILL -- \"-$child\" 2>/dev/null || :\n # Only GNU timeout and the trusted head can write this root-side channel.\n if [ \"$rc\" = 137 ] && [[ \"$(<\"$notice\")\" == *': sending signal KILL to command '* ]]; then exit 82; fi\n :\n [ \"$rc\" = 0 ] || { case \"$rc\" in 81|85|86|87|88) exit \"$rc\" ;; *) exit 87 ;; esac; }",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 overflow is output-limit"
},
{
"id": "M38",
"file": "scripts/case2-unit.sh",
"old": "\"$setsid_bin\" \"$@\" & child=$!",
"new": "\"$@\" & child=$!",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 signals query"
},
{
"id": "M39",
"file": "scripts/case2-unit.sh",
"old": "MOSAIC_TESTER_SUPERVISOR' || :\nset +m",
"new": "MOSAIC_TESTER_SUPERVISOR' || :\n# one-copy drift\nset +m",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 inline supervisor is byte-identical"
},
{
"id": "M40",
"file": "scripts/case2-unit.sh",
"old": "clean_session() {\n local entry pid rc live until=\"\"\n while :; do\n live=0\n for entry in /proc/[0-9]*/stat; do\n pid=\"${entry%/stat}\"; pid=\"${pid##*/}\"\n rc=0; stat_fields \"$pid\" || rc=$?\n [ \"$rc\" != 3 ] || continue\n [ \"$rc\" = 0 ] || return 1\n [ \"$ST_SESSION\" = \"$child\" ] || continue\n case \"$ST_STATE\" in Z|X) continue ;; esac\n live=1\n if ! kill -KILL \"$pid\" 2>/dev/null; then\n rc=0; stat_fields \"$pid\" || rc=$?\n [ \"$rc\" = 3 ] || { [ \"$rc\" = 0 ] && { [ \"$ST_STATE\" = Z ] || [ \"$ST_STATE\" = X ]; }; } || return 1\n fi\n done\n [ \"$live\" != 0 ] || return 0\n clock_cs || return 1\n # Inventory pass 1 is outside the bounded confirmation/retry budget.\n [ -n \"$until\" ] || until=$((CLOCK_CS + 25))\n [ \"$CLOCK_CS\" -lt \"$until\" ] || return 1\n \"$sleep_bin\" 0.01\n done\n}\n",
"new": "clean_session() {\n local entry pid rc live until\n clock_cs || return 1\n until=$((CLOCK_CS + 25))\n while :; do\n live=0\n for entry in /proc/[0-9]*/stat; do\n pid=\"${entry%/stat}\"; pid=\"${pid##*/}\"\n rc=0; stat_fields \"$pid\" || rc=$?\n [ \"$rc\" != 3 ] || continue\n [ \"$rc\" = 0 ] || return 1\n [ \"$ST_SESSION\" = \"$child\" ] || continue\n case \"$ST_STATE\" in Z|X) continue ;; esac\n live=1\n if ! kill -KILL \"$pid\" 2>/dev/null; then\n rc=0; stat_fields \"$pid\" || rc=$?\n [ \"$rc\" = 3 ] || { [ \"$rc\" = 0 ] && { [ \"$ST_STATE\" = Z ] || [ \"$ST_STATE\" = X ]; }; } || return 1\n fi\n done\n [ \"$live\" != 0 ] || return 0\n clock_cs || return 1\n # Inventory pass 1 is outside the bounded confirmation/retry budget.\n :\n [ \"$CLOCK_CS\" -lt \"$until\" ] || return 1\n \"$sleep_bin\" 0.01\n done\n}\n",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 cleanup budget starts after inventory pass one"
}
]
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Run explicit row253 changes only under Sage's grant. --static runs no tests."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import signal
import subprocess
import time
HERE = Path(__file__).resolve().parent
REPO = HERE.parents[3]
def digest(data):
return hashlib.sha256(data).hexdigest()
def busy():
p = subprocess.run(['bash', '/mnt/storage/scratch/sage-busy-now.sh'], text=True, capture_output=True)
if p.returncode or p.stdout.splitlines()[-1:] != ['CLEAR']:
raise RuntimeError('busy refusal: ' + p.stdout + p.stderr)
def run(command, path, seconds=60):
busy()
started = time.monotonic()
command_file = path.with_suffix('.command.json')
command_file.write_text(json.dumps(command))
# Keep command text out of wrapper argv: the busy guard scans every argv.
# launch.py execs the structured command. bash owns a pipefail/tee pipeline.
p = subprocess.Popen(['bash','-o','pipefail','-c',
'python3 "$1" "$2" 2>&1 | tee "$3"', '--',str(HERE/'launch.py'),str(command_file),str(path)],
cwd=REPO,start_new_session=True)
expired = False
try:
code = p.wait(timeout=seconds)
except subprocess.TimeoutExpired:
expired = True
os.killpg(p.pid, signal.SIGKILL)
code = p.wait()
finally:
try:
os.killpg(p.pid, signal.SIGKILL)
except ProcessLookupError:
pass
return dict(code=code, timeout=expired, seconds=round(time.monotonic()-started, 3), log=str(path))
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--static', action='store_true')
ap.add_argument('--grant')
ap.add_argument('--out', type=Path)
args = ap.parse_args()
definitions = json.loads((HERE/'changes.json').read_text())
checked = []
for d in definitions:
original = (REPO/d['file']).read_bytes()
count = original.count(d['old'].encode())
if count != 1:
raise RuntimeError(f"{d['id']}: target occurs {count} times")
changed = original.replace(d['old'].encode(), d['new'].encode(), 1)
checked.append(dict(id=d['id'], file=d['file'], occurrences=count,
before=digest(original), after=digest(changed)))
if args.static:
print(json.dumps(checked, indent=2))
return
if not args.grant or not args.out:
ap.error('execution needs --grant and --out')
if not os.environ.get('TMPDIR', '').startswith('/mnt/storage/scratch/tmp/g'):
raise RuntimeError('TMPDIR must be the granted scratch directory')
status = subprocess.check_output(['git','status','--porcelain','--untracked-files=no'],cwd=REPO)
args.out.mkdir(parents=True, exist_ok=True)
results = dict(grant=args.grant, static=checked, bases={}, changes=[])
failures = 0
for d in definitions:
key = d['killer']+'|'+d['pattern']
command = ['node','--test','--test-reporter=tap','--test-name-pattern',d['pattern'],d['killer']]
if key not in results['bases']:
base = run(command, args.out/(d['id']+'-base.log'))
results['bases'][key] = base
(args.out/'results.json').write_text(json.dumps(results,indent=2)+'\n')
if base['code'] != 0 or base['seconds'] > 30:
raise RuntimeError('base failed or exceeded 30 seconds: '+d['id'])
target = REPO/d['file']
original = target.read_bytes()
try:
target.write_bytes(original.replace(d['old'].encode(),d['new'].encode(),1))
pre = subprocess.run(['bash','-n',str(target)],capture_output=True)
if pre.returncode:
raise RuntimeError('invalid syntax: '+d['id'])
result = run(command,args.out/(d['id']+'.log'))
result.update(id=d['id'], base_seconds=results['bases'][key]['seconds'],
outcome='killed' if result['code'] != 0 else 'survived')
results['changes'].append(result)
if result['timeout']:
raise RuntimeError('timeout requires Sage/Mos classification: '+d['id'])
if result['outcome'] != 'killed':
failures += 1
finally:
target.write_bytes(original)
assert target.read_bytes() == original
assert subprocess.check_output(['git','status','--porcelain','--untracked-files=no'],cwd=REPO) == status
(args.out/'results.json').write_text(json.dumps(results,indent=2)+'\n')
if failures >= 1:
raise RuntimeError('first unexpected failure: end the sage-2297 window')
print(json.dumps(results,indent=2))
if __name__ == '__main__':
main()
@@ -0,0 +1,12 @@
# Row253 integration onto 378a76b85
Authority: sage-2348. New branch gander/row253-378a; gander/row253 remains at 9e916c454. Rebased range c3d88840c..9e916c454 onto 378a76b85; rebase tip before these records is 2f466464cccfe7b4f2522936a7f8409b74af7b5c.
Two conflict resolutions, both documentation:
1. First design commit, BUILD-LOG.md: kept every upstream appended line, then appended the row253 design phase. The integrated log has the complete 378a76b85 blob as its byte-identical prefix. Later row253 phase entries replayed normally. It remains outside the manifest and lands as a doc-only append after code.
2. Runnable repair commit, DEFERRED.md: kept current upstream content and appended the row276 / #1781 F5 entry. That commit had incorporated upstream changes on the old branch; the rebase correctly omits changes already on the new base. The later N1/N2 entry replayed normally.
No runtime or test conflict. All seven final manifest paths, including DEFERRED.md, are byte-identical to 9e916c454. Manifest digest remains 34d52cc500b2de855142daca4e293bbf05ff56d1c98bb94a061edabfc117e3e5. No entry changed. CANDIDATE-SHAS.json also remains valid.
range-diff.json stores the exact command and stdout. patch-ids.json gives every paired commit. Six commits compare equal; design and runnable repair differ only through BUILD-LOG context and DEFERRED upstream incorporation described above. No test or syntax execution ran. No push helper ran during Darkwing's slot. Sage publishes both own branches in the next gap. The whole gate remains held for the manifest review and CLEAR on this integrated candidate.
@@ -0,0 +1,47 @@
{
"digest": "34d52cc500b2de855142daca4e293bbf05ff56d1c98bb94a061edabfc117e3e5",
"entries": [
{
"path": "docs/plans/DEFERRED.md",
"old_sha256": "3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f",
"integrated_sha256": "3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f",
"changed": false
},
{
"path": "packages/cli/tests/case-sudo-fixture.mjs",
"old_sha256": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
"integrated_sha256": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
"changed": false
},
{
"path": "packages/cli/tests/case1-accept.test.mjs",
"old_sha256": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901",
"integrated_sha256": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901",
"changed": false
},
{
"path": "packages/cli/tests/case2-unit.test.mjs",
"old_sha256": "1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44",
"integrated_sha256": "1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44",
"changed": false
},
{
"path": "packages/webui/README.md",
"old_sha256": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9",
"integrated_sha256": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9",
"changed": false
},
{
"path": "scripts/case1-accept.sh",
"old_sha256": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"integrated_sha256": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"changed": false
},
{
"path": "scripts/case2-unit.sh",
"old_sha256": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"integrated_sha256": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"changed": false
}
]
}
@@ -0,0 +1,66 @@
[
{
"old": "d8d7c34a7d10dffe93f3b931b8f4e3e1c4b6d983",
"new": "971681f29a347a3771802b19d1b9a65c88943a79",
"subject": "gander: design tester socket migration for host scripts",
"old_patch_id": "ee1088a4ca06de60f5a844d17d680bf33a37cd6c",
"new_patch_id": "47bdc56d349c8fcce2e5f56f3f5ce4ce7b1d6f14",
"equal": false
},
{
"old": "8a8df41715c016ea5aaf6de7307771ee2dde98f6",
"new": "43f17b9afe60bfe256e924b1317332d6068a8b4f",
"subject": "test: prepare row253 socket fixtures and fix gate collisions",
"old_patch_id": "d597cc7402b568d845a87c1f142bde74ce6ae9da",
"new_patch_id": "d597cc7402b568d845a87c1f142bde74ce6ae9da",
"equal": true
},
{
"old": "968db0bcdf96301f896873a81fa43e13d82f4a64",
"new": "f9b9112b970b634ee7c990447ded04dfe622d8cc",
"subject": "docs: propose row253 C4 session supervisor and draft hop fake",
"old_patch_id": "899327761a9bde7e7b4fefd790c1a523488e8029",
"new_patch_id": "899327761a9bde7e7b4fefd790c1a523488e8029",
"equal": true
},
{
"old": "144a7bf7c7947627b0fd5ed966b8f1a2e5431088",
"new": "70e6bb16159dd28391dbbfe0b4808f0c3dde5225",
"subject": "docs(row253): freeze C4 repair for static review",
"old_patch_id": "38dcf7f0ec2a39bad2696d971a684a729b154747",
"new_patch_id": "38dcf7f0ec2a39bad2696d971a684a729b154747",
"equal": true
},
{
"old": "b64aa316ab4fa911a4b4422dfdf39ad75ec96620",
"new": "bef1bd886b6e90f2a404825cce728e3901328ba3",
"subject": "docs(row253): store repair patch without context whitespace",
"old_patch_id": "1cf1916dab2a8552fadd07ce81246fce5f105b3d",
"new_patch_id": "1cf1916dab2a8552fadd07ce81246fce5f105b3d",
"equal": true
},
{
"old": "aa81ca390b059e36112093758484a564ce197505",
"new": "ecb15d1debdad4fbd2e9bdfedb639143467709c6",
"subject": "fix(row253): supervise socket queries across sudo sessions",
"old_patch_id": "24f6b70a08e4534381b7b4a0d59c6bf0699a3b80",
"new_patch_id": "d104bd53960b75c1e9d9402ec2ea193a55bf8164",
"equal": false
},
{
"old": "761893dac7133671b34aada788cfab91de111e5b",
"new": "39b98a26e9ac00edf3b8d77690cf06bc689a6272",
"subject": "fix(row253): bound cleanup retries after initial inventory",
"old_patch_id": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3",
"new_patch_id": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3",
"equal": true
},
{
"old": "9e916c4543912d2215d81a0a51b7f4751d36ce6d",
"new": "2f466464cccfe7b4f2522936a7f8409b74af7b5c",
"subject": "docs(row253): record static approval and pre-confirmation residual",
"old_patch_id": "d2aecebf222474b0e06c5109b5f22caf768176ce",
"new_patch_id": "d2aecebf222474b0e06c5109b5f22caf768176ce",
"equal": true
}
]
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""Exec a structured command without putting its text in wrapper argv."""
import json
import os
import sys
with open(sys.argv[1]) as source:
command = json.load(source)
os.execvp(command[0], command)
@@ -0,0 +1,69 @@
{
"grant": "sage-2279",
"stopped": "second unexpected fixture failure; no rerun",
"steps": [
{
"name": "focused/installer-red",
"code": 1,
"seconds": 0.265,
"expected_red": true,
"counts": {
"tests": 1,
"pass": 0,
"fail": 1,
"skipped": 0
}
},
{
"name": "focused/acceptance-red",
"code": 1,
"seconds": 0.215,
"expected_red": true,
"counts": {
"tests": 1,
"pass": 0,
"fail": 1,
"skipped": 0
}
},
{
"name": "focused/focused-1",
"code": 1,
"seconds": 17.303,
"expected_red": false,
"counts": {
"tests": 36,
"pass": 35,
"fail": 1,
"skipped": 0
}
},
{
"name": "green/focused-1",
"code": 0,
"seconds": 19.459,
"expected_red": false,
"counts": {
"tests": 36,
"pass": 36,
"fail": 0,
"skipped": 0
}
},
{
"name": "green/focused-2",
"code": 1,
"seconds": 41.962,
"expected_red": false,
"counts": {
"tests": 28,
"pass": 27,
"fail": 1,
"skipped": 0
}
}
],
"sweep_count": 0,
"changes_run": 0,
"whole_suites_run": 0
}
@@ -0,0 +1,133 @@
✖ row253 factory socket uses actual config and target-uid query without widening hardening (24.390664ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 186.876891
✖ failing tests:
test at packages/cli/tests/case2-unit.test.mjs:125:1
✖ row253 factory socket uses actual config and target-uid query without widening hardening (24.390664ms)
AssertionError [ERR_ASSERTION]: case2-unit: unknown serve flag: --tester-socket
1 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:127:45)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
✖ row253 socket observation uses configured root, metadata and the Console peer without sending bytes (23.551007ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 127.494174
✖ failing tests:
test at packages/cli/tests/case1-accept.test.mjs:220:1
✖ row253 socket observation uses configured root, metadata and the Console peer without sending bytes (23.551007ms)
AssertionError [ERR_ASSERTION]: case1-accept: refusing: /etc/mosaic-stack/dev.args lacks a numeric --port or --tester-port after --
2 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:225:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 2,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
✔ row253 factory socket uses actual config and target-uid query without widening hardening (145.450807ms)
✔ row253 installer refuses unsafe location and query facts before unit writes (819.380714ms)
✔ row253 query timeout kills its process group and leaves no running child (5018.363626ms)
✔ row253 protected homes refuse before any filesystem access (14.424895ms)
✔ row253 query reaps its group even when a child closes stdout before the query returns (111.74091ms)
✔ row253 config parser is strict and output capture is bounded (16.596636ms)
✔ a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console (104.769986ms)
✔ PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node (104.638101ms)
✔ a failed bus start refuses with the Console not started (129.922739ms)
✔ the script never links a unit file (0.334047ms)
✔ factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty (288.329982ms)
✔ a re-install over running units says they are still on the old copy and must restart together, never that it started them (484.024882ms)
✔ a re-install to another copy re-renders both units, and an edited installed unit refuses (1069.731966ms)
✔ a template with a namespace, user or system call restriction refuses (273.736385ms)
✔ the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none (226.707075ms)
✔ a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses (166.978341ms)
✔ a symlink at either unit path refuses (179.013605ms)
✔ a --node that is relative, not root-owned or under <home> refuses (20.685359ms)
✔ a re-install with a different --node or different serve flags refuses (515.577009ms)
✔ an installed pair whose units name different copies refuses (290.162793ms)
✔ a copy id that is short, uppercase, holds / or .., or names no directory refuses (35.795128ms)
✖ library refuses a privileged-floor miss, a bad proof pair and every flag outside its set (295.169858ms)
✔ library refuses a proof file not owned by <user>'s uid (18.009672ms)
✔ factory refuses duplicate socket, a missing --port or --business, and each proxy flag (228.440728ms)
✔ row253 factory refuses retired TCP and a value after the socket marker (28.788793ms)
✔ factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude (1087.25876ms)
✔ the precheck passes on a good host and prints free memory, without the key (124.607105ms)
✔ the precheck refuses each failed host check (1066.073149ms)
✔ the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root (587.108114ms)
✔ a bwrap that fails refuses and prints the sysctl and where bwrap is (123.872451ms)
✔ an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit (606.804292ms)
✔ bus.tester: library refuses it on, factory needs it on, and factory refuses a library business (547.428698ms)
✔ the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong (1099.09849ms)
✔ the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH (387.023764ms)
✔ the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line (796.938259ms)
✔ inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds (97.958541ms)
ℹ tests 36
ℹ suites 0
ℹ pass 35
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 17231.019695
✖ failing tests:
test at packages/cli/tests/case2-unit.test.mjs:427:1
✖ library refuses a privileged-floor miss, a bad proof pair and every flag outside its set (295.169858ms)
Error: EEXIST: file already exists, mkdir '/mnt/storage/scratch/tmp/glFUYC2/case2-unit-LimIRr/home/mosaic/.mosaic-dev'
at mkdirSync (node:fs:1734:26)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:465:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
errno: -17,
code: 'EEXIST',
syscall: 'mkdir',
path: '/mnt/storage/scratch/tmp/glFUYC2/case2-unit-LimIRr/home/mosaic/.mosaic-dev'
}
Traceback (most recent call last):
File "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-253/window.py", line 76, in <module>
main()
File "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-253/window.py", line 64, in main
step('focused-'+str(i+1),['node','--test',test])
File "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-253/window.py", line 47, in step
raise RuntimeError('unexpected failure: report before continuing')
RuntimeError: unexpected failure: report before continuing
@@ -0,0 +1,7 @@
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"before": "9f9883cd7258aa56650b14cb19acc683cfd97c0ea3a1b9adc441d1f5749ee9cb",
"after": "93cc7b6a534ba0fff12667f7c8957ec35349055de2428282ac94831390594649",
"cause": "Hub now creates dataRoot; old outside-secrets test mkdir was not idempotent. Assertion unchanged. Product bytes unchanged.",
"harness": "Explicit TAP reporter for machine-readable counts; green-only resume stage preserves RED records."
}
@@ -0,0 +1 @@
["node", "--test", "--test-name-pattern", "row253 socket observation uses configured root", "packages/cli/tests/case1-accept.test.mjs"]
@@ -0,0 +1,31 @@
✖ row253 socket observation uses configured root, metadata and the Console peer without sending bytes (23.551007ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 127.494174
✖ failing tests:
test at packages/cli/tests/case1-accept.test.mjs:220:1
✖ row253 socket observation uses configured root, metadata and the Console peer without sending bytes (23.551007ms)
AssertionError [ERR_ASSERTION]: case1-accept: refusing: /etc/mosaic-stack/dev.args lacks a numeric --port or --tester-port after --
2 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:225:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 2,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1 @@
["node", "--test", "packages/cli/tests/case2-unit.test.mjs"]
@@ -0,0 +1,63 @@
✔ row253 factory socket uses actual config and target-uid query without widening hardening (145.450807ms)
✔ row253 installer refuses unsafe location and query facts before unit writes (819.380714ms)
✔ row253 query timeout kills its process group and leaves no running child (5018.363626ms)
✔ row253 protected homes refuse before any filesystem access (14.424895ms)
✔ row253 query reaps its group even when a child closes stdout before the query returns (111.74091ms)
✔ row253 config parser is strict and output capture is bounded (16.596636ms)
✔ a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console (104.769986ms)
✔ PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node (104.638101ms)
✔ a failed bus start refuses with the Console not started (129.922739ms)
✔ the script never links a unit file (0.334047ms)
✔ factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty (288.329982ms)
✔ a re-install over running units says they are still on the old copy and must restart together, never that it started them (484.024882ms)
✔ a re-install to another copy re-renders both units, and an edited installed unit refuses (1069.731966ms)
✔ a template with a namespace, user or system call restriction refuses (273.736385ms)
✔ the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none (226.707075ms)
✔ a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses (166.978341ms)
✔ a symlink at either unit path refuses (179.013605ms)
✔ a --node that is relative, not root-owned or under <home> refuses (20.685359ms)
✔ a re-install with a different --node or different serve flags refuses (515.577009ms)
✔ an installed pair whose units name different copies refuses (290.162793ms)
✔ a copy id that is short, uppercase, holds / or .., or names no directory refuses (35.795128ms)
✖ library refuses a privileged-floor miss, a bad proof pair and every flag outside its set (295.169858ms)
✔ library refuses a proof file not owned by <user>'s uid (18.009672ms)
✔ factory refuses duplicate socket, a missing --port or --business, and each proxy flag (228.440728ms)
✔ row253 factory refuses retired TCP and a value after the socket marker (28.788793ms)
✔ factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude (1087.25876ms)
✔ the precheck passes on a good host and prints free memory, without the key (124.607105ms)
✔ the precheck refuses each failed host check (1066.073149ms)
✔ the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root (587.108114ms)
✔ a bwrap that fails refuses and prints the sysctl and where bwrap is (123.872451ms)
✔ an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit (606.804292ms)
✔ bus.tester: library refuses it on, factory needs it on, and factory refuses a library business (547.428698ms)
✔ the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong (1099.09849ms)
✔ the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH (387.023764ms)
✔ the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line (796.938259ms)
✔ inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds (97.958541ms)
ℹ tests 36
ℹ suites 0
ℹ pass 35
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 17231.019695
✖ failing tests:
test at packages/cli/tests/case2-unit.test.mjs:427:1
✖ library refuses a privileged-floor miss, a bad proof pair and every flag outside its set (295.169858ms)
Error: EEXIST: file already exists, mkdir '/mnt/storage/scratch/tmp/glFUYC2/case2-unit-LimIRr/home/mosaic/.mosaic-dev'
at mkdirSync (node:fs:1734:26)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:465:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
errno: -17,
code: 'EEXIST',
syscall: 'mkdir',
path: '/mnt/storage/scratch/tmp/glFUYC2/case2-unit-LimIRr/home/mosaic/.mosaic-dev'
}
@@ -0,0 +1 @@
["node", "--test", "--test-name-pattern", "row253 factory socket uses actual config", "packages/cli/tests/case2-unit.test.mjs"]
@@ -0,0 +1,31 @@
✖ row253 factory socket uses actual config and target-uid query without widening hardening (24.390664ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 186.876891
✖ failing tests:
test at packages/cli/tests/case2-unit.test.mjs:125:1
✖ row253 factory socket uses actual config and target-uid query without widening hardening (24.390664ms)
AssertionError [ERR_ASSERTION]: case2-unit: unknown serve flag: --tester-socket
1 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:127:45)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,33 @@
{
"grant": "sage-2279",
"stage": "focused",
"steps": [
{
"code": 1,
"timeout": false,
"seconds": 0.265,
"log": "agents/gander/work/queue-253/run1/focused/installer-red.log",
"name": "installer-red",
"expected_red": true,
"counts": {}
},
{
"code": 1,
"timeout": false,
"seconds": 0.215,
"log": "agents/gander/work/queue-253/run1/focused/acceptance-red.log",
"name": "acceptance-red",
"expected_red": true,
"counts": {}
},
{
"code": 1,
"timeout": false,
"seconds": 17.303,
"log": "agents/gander/work/queue-253/run1/focused/focused-1.log",
"name": "focused-1",
"expected_red": false,
"counts": {}
}
]
}
@@ -0,0 +1,432 @@
TAP version 13
# Subtest: row253 factory socket uses actual config and target-uid query without widening hardening
ok 1 - row253 factory socket uses actual config and target-uid query without widening hardening
---
duration_ms: 216.869128
type: 'test'
...
# Subtest: row253 installer refuses unsafe location and query facts before unit writes
ok 2 - row253 installer refuses unsafe location and query facts before unit writes
---
duration_ms: 1037.060467
type: 'test'
...
# Subtest: row253 query timeout kills its process group and leaves no running child
ok 3 - row253 query timeout kills its process group and leaves no running child
---
duration_ms: 5027.077544
type: 'test'
...
# Subtest: row253 protected homes refuse before any filesystem access
ok 4 - row253 protected homes refuse before any filesystem access
---
duration_ms: 14.602145
type: 'test'
...
# Subtest: row253 query reaps its group even when a child closes stdout before the query returns
ok 5 - row253 query reaps its group even when a child closes stdout before the query returns
---
duration_ms: 152.685734
type: 'test'
...
# Subtest: row253 config parser is strict and output capture is bounded
ok 6 - row253 config parser is strict and output capture is bounded
---
duration_ms: 26.669467
type: 'test'
...
# Subtest: a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console
ok 7 - a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console
---
duration_ms: 141.129141
type: 'test'
...
# Subtest: PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node
ok 8 - PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node
---
duration_ms: 145.189241
type: 'test'
...
# Subtest: a failed bus start refuses with the Console not started
ok 9 - a failed bus start refuses with the Console not started
---
duration_ms: 120.050347
type: 'test'
...
# Subtest: the script never links a unit file
ok 10 - the script never links a unit file
---
duration_ms: 0.293936
type: 'test'
...
# Subtest: factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty
ok 11 - factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty
---
duration_ms: 358.790337
type: 'test'
...
# Subtest: a re-install over running units says they are still on the old copy and must restart together, never that it started them
ok 12 - a re-install over running units says they are still on the old copy and must restart together, never that it started them
---
duration_ms: 583.217218
type: 'test'
...
# Subtest: a re-install to another copy re-renders both units, and an edited installed unit refuses
ok 13 - a re-install to another copy re-renders both units, and an edited installed unit refuses
---
duration_ms: 1346.293893
type: 'test'
...
# Subtest: a template with a namespace, user or system call restriction refuses
ok 14 - a template with a namespace, user or system call restriction refuses
---
duration_ms: 335.885968
type: 'test'
...
# Subtest: the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none
ok 15 - the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none
---
duration_ms: 261.193104
type: 'test'
...
# Subtest: a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses
ok 16 - a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses
---
duration_ms: 217.834215
type: 'test'
...
# Subtest: a symlink at either unit path refuses
ok 17 - a symlink at either unit path refuses
---
duration_ms: 292.144849
type: 'test'
...
# Subtest: a --node that is relative, not root-owned or under <home> refuses
ok 18 - a --node that is relative, not root-owned or under <home> refuses
---
duration_ms: 34.702768
type: 'test'
...
# Subtest: a re-install with a different --node or different serve flags refuses
ok 19 - a re-install with a different --node or different serve flags refuses
---
duration_ms: 862.096949
type: 'test'
...
# Subtest: an installed pair whose units name different copies refuses
ok 20 - an installed pair whose units name different copies refuses
---
duration_ms: 343.53779
type: 'test'
...
# Subtest: a copy id that is short, uppercase, holds / or .., or names no directory refuses
ok 21 - a copy id that is short, uppercase, holds / or .., or names no directory refuses
---
duration_ms: 37.449488
type: 'test'
...
# Subtest: library refuses a privileged-floor miss, a bad proof pair and every flag outside its set
ok 22 - library refuses a privileged-floor miss, a bad proof pair and every flag outside its set
---
duration_ms: 510.984644
type: 'test'
...
# Subtest: library refuses a proof file not owned by <user>'s uid
ok 23 - library refuses a proof file not owned by <user>'s uid
---
duration_ms: 30.321206
type: 'test'
...
# Subtest: factory refuses duplicate socket, a missing --port or --business, and each proxy flag
ok 24 - factory refuses duplicate socket, a missing --port or --business, and each proxy flag
---
duration_ms: 306.036125
type: 'test'
...
# Subtest: row253 factory refuses retired TCP and a value after the socket marker
ok 25 - row253 factory refuses retired TCP and a value after the socket marker
---
duration_ms: 45.290287
type: 'test'
...
# Subtest: factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude
ok 26 - factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude
---
duration_ms: 837.29654
type: 'test'
...
# Subtest: the precheck passes on a good host and prints free memory, without the key
ok 27 - the precheck passes on a good host and prints free memory, without the key
---
duration_ms: 127.505732
type: 'test'
...
# Subtest: the precheck refuses each failed host check
ok 28 - the precheck refuses each failed host check
---
duration_ms: 1209.781622
type: 'test'
...
# Subtest: the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root
ok 29 - the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root
---
duration_ms: 686.996596
type: 'test'
...
# Subtest: a bwrap that fails refuses and prints the sysctl and where bwrap is
ok 30 - a bwrap that fails refuses and prints the sysctl and where bwrap is
---
duration_ms: 147.551012
type: 'test'
...
# Subtest: an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit
ok 31 - an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit
---
duration_ms: 729.482735
type: 'test'
...
# Subtest: bus.tester: library refuses it on, factory needs it on, and factory refuses a library business
ok 32 - bus.tester: library refuses it on, factory needs it on, and factory refuses a library business
---
duration_ms: 570.789238
type: 'test'
...
# Subtest: the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong
ok 33 - the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong
---
duration_ms: 1142.885248
type: 'test'
...
# Subtest: the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH
ok 34 - the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH
---
duration_ms: 383.125949
type: 'test'
...
# Subtest: the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line
ok 35 - the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line
---
duration_ms: 799.735416
type: 'test'
...
# Subtest: inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds
ok 36 - inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds
---
duration_ms: 98.89618
type: 'test'
...
1..36
# tests 36
# suites 0
# pass 36
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 19359.125841
TAP version 13
# Subtest: row253 socket observation uses configured root, metadata and the Console peer without sending bytes
ok 1 - row253 socket observation uses configured root, metadata and the Console peer without sending bytes
---
duration_ms: 457.237158
type: 'test'
...
# Subtest: row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
ok 2 - row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
---
duration_ms: 8825.596429
type: 'test'
...
# Subtest: row253 acceptance rejects bad config and unsupported copies before talk
ok 3 - row253 acceptance rejects bad config and unsupported copies before talk
---
duration_ms: 405.608885
type: 'test'
...
# Subtest: row253 stalled peer observation ends within its bound without leaving a reader
ok 4 - row253 stalled peer observation ends within its bound without leaving a reader
---
duration_ms: 3417.061486
type: 'test'
...
# Subtest: row253 P8 refuses both retired TCP and board listeners
ok 5 - row253 P8 refuses both retired TCP and board listeners
---
duration_ms: 931.562888
type: 'test'
...
# Subtest: row 195 accept: every check passing exits 0, with each P mark and counts that match the lines
ok 6 - row 195 accept: every check passing exits 0, with each P mark and counts that match the lines
---
duration_ms: 423.786569
type: 'test'
...
# Subtest: row 195 accept: exits 1 with each F check failing alone, and its line names it
ok 7 - row 195 accept: exits 1 with each F check failing alone, and its line names it
---
duration_ms: 4820.838663
type: 'test'
...
# Subtest: row 195 accept: F3 fails on exit 0 with no reply line, and its evidence takes talk's error
ok 8 - row 195 accept: F3 fails on exit 0 with no reply line, and its evidence takes talk's error
---
duration_ms: 854.517542
type: 'test'
...
# Subtest: row 195 accept: F1 fails on a unit that isn't active or isn't User=mosaic
ok 9 - row 195 accept: F1 fails on a unit that isn't active or isn't User=mosaic
---
duration_ms: 880.2515
type: 'test'
...
# Subtest: row 195 accept: F2 fails when healthz doesn't answer and when another pid holds the port
ok 10 - row 195 accept: F2 fails when healthz doesn't answer and when another pid holds the port
---
duration_ms: 923.143848
type: 'test'
...
# Subtest: row 195 accept: F5 fails on unpriv_bwrap (enforce), on complain and on a trailing space
ok 11 - row 195 accept: F5 fails on unpriv_bwrap (enforce), on complain and on a trailing space
---
duration_ms: 1611.055796
type: 'test'
...
# Subtest: row 195 accept: F5 fails with no bwrap of mosaic's, and passes on comm alone
ok 12 - row 195 accept: F5 fails with no bwrap of mosaic's, and passes on comm alone
---
duration_ms: 772.495962
type: 'test'
...
# Subtest: row 195 accept: F6 fails on a stub that prints nothing, on a path outside claude-<pin> and on a .. segment
ok 13 - row 195 accept: F6 fails on a stub that prints nothing, on a path outside claude-<pin> and on a .. segment
---
duration_ms: 1208.609458
type: 'test'
...
# Subtest: row 195 accept: F7 fails on two live PM launches, one for another business, and no launch.by
ok 14 - row 195 accept: F7 fails on two live PM launches, one for another business, and no launch.by
---
duration_ms: 1277.66317
type: 'test'
...
# Subtest: row 195 accept: F8 fails on 0640, on another owner and on a symlink, and never prints the credential
ok 15 - row 195 accept: F8 fails on 0640, on another owner and on a symlink, and never prints the credential
---
duration_ms: 1323.209668
type: 'test'
...
# Subtest: row 195 accept: F9 fails on another owner, a group-writable mode and a number for the flag
ok 16 - row 195 accept: F9 fails on another owner, a group-writable mode and a number for the flag
---
duration_ms: 1223.483541
type: 'test'
...
# Subtest: row 195 accept: F9 won't run --node as root off the pin path or with a writable directory above it
ok 17 - row 195 accept: F9 won't run --node as root off the pin path or with a writable directory above it
---
duration_ms: 1110.862644
type: 'test'
...
# Subtest: row 195 accept: F11 fails on one *.json under discord/ and passes on an empty directory
ok 18 - row 195 accept: F11 fails on one *.json under discord/ and passes on an empty directory
---
duration_ms: 916.072105
type: 'test'
...
# Subtest: row 195 accept: each P check reports unnamed on its fault, and the exit stays 0
ok 19 - row 195 accept: each P check reports unnamed on its fault, and the exit stays 0
---
duration_ms: 4105.537374
type: 'test'
...
# Subtest: row 195 accept: P2 is unnamed with a supplementary group, and P5 names docker
ok 20 - row 195 accept: P2 is unnamed with a supplementary group, and P5 names docker
---
duration_ms: 1015.859889
type: 'test'
...
# Subtest: row 195 accept: P5 is closed with no jarvis account, and P2 with an empty authorized_keys
ok 21 - row 195 accept: P5 is closed with no jarvis account, and P2 with an empty authorized_keys
---
duration_ms: 400.087324
type: 'test'
...
# Subtest: row 195 accept: P9 is unnamed once for each item missing
ok 22 - row 195 accept: P9 is unnamed once for each item missing
---
duration_ms: 2710.187272
type: 'test'
...
# Subtest: row253 accept: P10 fails its observation with another peer; P11 with the unit disabled
ok 23 - row253 accept: P10 fails its observation with another peer; P11 with the unit disabled
---
duration_ms: 800.289163
type: 'test'
...
# Subtest: row 195 accept: every P check unnamed together still exits 0
ok 24 - row 195 accept: every P check unnamed together still exits 0
---
duration_ms: 348.312367
type: 'test'
...
# Subtest: row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
ok 25 - row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
---
duration_ms: 101.428549
type: 'test'
...
# Subtest: row 195 accept: usage errors and a run without --root as a user exit 2
ok 26 - row 195 accept: usage errors and a run without --root as a user exit 2
---
duration_ms: 11.252533
type: 'test'
...
# Subtest: row 195 accept: every in-instance call goes through sudo -u mosaic -H env PATH=<node dir>:...
not ok 27 - row 195 accept: every in-instance call goes through sudo -u mosaic -H env PATH=<node dir>:...
---
duration_ms: 448.51281
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:625:1'
failureType: 'testCodeFailure'
error: |-
The input did not match the regular expression /^talk chief case1-accept-[0-9a-f]{12}: .* --business mosaic-stack --tester --wait 300$/. Input:
'talk chief case1-accept-7f454c46: this is the Case 1 acceptance; reply with one short line. --business mosaic-stack --tester --wait 300'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
actual: 'talk chief case1-accept-7f454c46: this is the Case 1 acceptance; reply with one short line. --business mosaic-stack --tester --wait 300'
operator: 'match'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:641:10)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.processPendingSubtests (node:internal/test_runner/test:974:18)
Test.postRun (node:internal/test_runner/test:1542:19)
Test.run (node:internal/test_runner/test:1467:12)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: row 195 accept: the host check runs with --check and never --enforce
ok 28 - row 195 accept: the host check runs with --check and never --enforce
---
duration_ms: 421.488991
type: 'test'
...
1..28
# tests 28
# suites 0
# pass 27
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 41874.061683
Traceback (most recent call last):
File "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-253/window.py", line 78, in <module>
main()
File "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-253/window.py", line 66, in main
step('focused-'+str(i+1),['node','--test','--test-reporter=tap',test])
File "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-253/window.py", line 47, in step
raise RuntimeError('unexpected failure: report before continuing')
RuntimeError: unexpected failure: report before continuing
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "packages/cli/tests/case2-unit.test.mjs"]
@@ -0,0 +1,226 @@
TAP version 13
# Subtest: row253 factory socket uses actual config and target-uid query without widening hardening
ok 1 - row253 factory socket uses actual config and target-uid query without widening hardening
---
duration_ms: 216.869128
type: 'test'
...
# Subtest: row253 installer refuses unsafe location and query facts before unit writes
ok 2 - row253 installer refuses unsafe location and query facts before unit writes
---
duration_ms: 1037.060467
type: 'test'
...
# Subtest: row253 query timeout kills its process group and leaves no running child
ok 3 - row253 query timeout kills its process group and leaves no running child
---
duration_ms: 5027.077544
type: 'test'
...
# Subtest: row253 protected homes refuse before any filesystem access
ok 4 - row253 protected homes refuse before any filesystem access
---
duration_ms: 14.602145
type: 'test'
...
# Subtest: row253 query reaps its group even when a child closes stdout before the query returns
ok 5 - row253 query reaps its group even when a child closes stdout before the query returns
---
duration_ms: 152.685734
type: 'test'
...
# Subtest: row253 config parser is strict and output capture is bounded
ok 6 - row253 config parser is strict and output capture is bounded
---
duration_ms: 26.669467
type: 'test'
...
# Subtest: a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console
ok 7 - a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console
---
duration_ms: 141.129141
type: 'test'
...
# Subtest: PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node
ok 8 - PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node
---
duration_ms: 145.189241
type: 'test'
...
# Subtest: a failed bus start refuses with the Console not started
ok 9 - a failed bus start refuses with the Console not started
---
duration_ms: 120.050347
type: 'test'
...
# Subtest: the script never links a unit file
ok 10 - the script never links a unit file
---
duration_ms: 0.293936
type: 'test'
...
# Subtest: factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty
ok 11 - factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty
---
duration_ms: 358.790337
type: 'test'
...
# Subtest: a re-install over running units says they are still on the old copy and must restart together, never that it started them
ok 12 - a re-install over running units says they are still on the old copy and must restart together, never that it started them
---
duration_ms: 583.217218
type: 'test'
...
# Subtest: a re-install to another copy re-renders both units, and an edited installed unit refuses
ok 13 - a re-install to another copy re-renders both units, and an edited installed unit refuses
---
duration_ms: 1346.293893
type: 'test'
...
# Subtest: a template with a namespace, user or system call restriction refuses
ok 14 - a template with a namespace, user or system call restriction refuses
---
duration_ms: 335.885968
type: 'test'
...
# Subtest: the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none
ok 15 - the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none
---
duration_ms: 261.193104
type: 'test'
...
# Subtest: a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses
ok 16 - a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses
---
duration_ms: 217.834215
type: 'test'
...
# Subtest: a symlink at either unit path refuses
ok 17 - a symlink at either unit path refuses
---
duration_ms: 292.144849
type: 'test'
...
# Subtest: a --node that is relative, not root-owned or under <home> refuses
ok 18 - a --node that is relative, not root-owned or under <home> refuses
---
duration_ms: 34.702768
type: 'test'
...
# Subtest: a re-install with a different --node or different serve flags refuses
ok 19 - a re-install with a different --node or different serve flags refuses
---
duration_ms: 862.096949
type: 'test'
...
# Subtest: an installed pair whose units name different copies refuses
ok 20 - an installed pair whose units name different copies refuses
---
duration_ms: 343.53779
type: 'test'
...
# Subtest: a copy id that is short, uppercase, holds / or .., or names no directory refuses
ok 21 - a copy id that is short, uppercase, holds / or .., or names no directory refuses
---
duration_ms: 37.449488
type: 'test'
...
# Subtest: library refuses a privileged-floor miss, a bad proof pair and every flag outside its set
ok 22 - library refuses a privileged-floor miss, a bad proof pair and every flag outside its set
---
duration_ms: 510.984644
type: 'test'
...
# Subtest: library refuses a proof file not owned by <user>'s uid
ok 23 - library refuses a proof file not owned by <user>'s uid
---
duration_ms: 30.321206
type: 'test'
...
# Subtest: factory refuses duplicate socket, a missing --port or --business, and each proxy flag
ok 24 - factory refuses duplicate socket, a missing --port or --business, and each proxy flag
---
duration_ms: 306.036125
type: 'test'
...
# Subtest: row253 factory refuses retired TCP and a value after the socket marker
ok 25 - row253 factory refuses retired TCP and a value after the socket marker
---
duration_ms: 45.290287
type: 'test'
...
# Subtest: factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude
ok 26 - factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude
---
duration_ms: 837.29654
type: 'test'
...
# Subtest: the precheck passes on a good host and prints free memory, without the key
ok 27 - the precheck passes on a good host and prints free memory, without the key
---
duration_ms: 127.505732
type: 'test'
...
# Subtest: the precheck refuses each failed host check
ok 28 - the precheck refuses each failed host check
---
duration_ms: 1209.781622
type: 'test'
...
# Subtest: the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root
ok 29 - the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root
---
duration_ms: 686.996596
type: 'test'
...
# Subtest: a bwrap that fails refuses and prints the sysctl and where bwrap is
ok 30 - a bwrap that fails refuses and prints the sysctl and where bwrap is
---
duration_ms: 147.551012
type: 'test'
...
# Subtest: an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit
ok 31 - an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit
---
duration_ms: 729.482735
type: 'test'
...
# Subtest: bus.tester: library refuses it on, factory needs it on, and factory refuses a library business
ok 32 - bus.tester: library refuses it on, factory needs it on, and factory refuses a library business
---
duration_ms: 570.789238
type: 'test'
...
# Subtest: the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong
ok 33 - the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong
---
duration_ms: 1142.885248
type: 'test'
...
# Subtest: the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH
ok 34 - the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH
---
duration_ms: 383.125949
type: 'test'
...
# Subtest: the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line
ok 35 - the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line
---
duration_ms: 799.735416
type: 'test'
...
# Subtest: inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds
ok 36 - inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds
---
duration_ms: 98.89618
type: 'test'
...
1..36
# tests 36
# suites 0
# pass 36
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 19359.125841
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "packages/cli/tests/case1-accept.test.mjs"]
@@ -0,0 +1,198 @@
TAP version 13
# Subtest: row253 socket observation uses configured root, metadata and the Console peer without sending bytes
ok 1 - row253 socket observation uses configured root, metadata and the Console peer without sending bytes
---
duration_ms: 457.237158
type: 'test'
...
# Subtest: row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
ok 2 - row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
---
duration_ms: 8825.596429
type: 'test'
...
# Subtest: row253 acceptance rejects bad config and unsupported copies before talk
ok 3 - row253 acceptance rejects bad config and unsupported copies before talk
---
duration_ms: 405.608885
type: 'test'
...
# Subtest: row253 stalled peer observation ends within its bound without leaving a reader
ok 4 - row253 stalled peer observation ends within its bound without leaving a reader
---
duration_ms: 3417.061486
type: 'test'
...
# Subtest: row253 P8 refuses both retired TCP and board listeners
ok 5 - row253 P8 refuses both retired TCP and board listeners
---
duration_ms: 931.562888
type: 'test'
...
# Subtest: row 195 accept: every check passing exits 0, with each P mark and counts that match the lines
ok 6 - row 195 accept: every check passing exits 0, with each P mark and counts that match the lines
---
duration_ms: 423.786569
type: 'test'
...
# Subtest: row 195 accept: exits 1 with each F check failing alone, and its line names it
ok 7 - row 195 accept: exits 1 with each F check failing alone, and its line names it
---
duration_ms: 4820.838663
type: 'test'
...
# Subtest: row 195 accept: F3 fails on exit 0 with no reply line, and its evidence takes talk's error
ok 8 - row 195 accept: F3 fails on exit 0 with no reply line, and its evidence takes talk's error
---
duration_ms: 854.517542
type: 'test'
...
# Subtest: row 195 accept: F1 fails on a unit that isn't active or isn't User=mosaic
ok 9 - row 195 accept: F1 fails on a unit that isn't active or isn't User=mosaic
---
duration_ms: 880.2515
type: 'test'
...
# Subtest: row 195 accept: F2 fails when healthz doesn't answer and when another pid holds the port
ok 10 - row 195 accept: F2 fails when healthz doesn't answer and when another pid holds the port
---
duration_ms: 923.143848
type: 'test'
...
# Subtest: row 195 accept: F5 fails on unpriv_bwrap (enforce), on complain and on a trailing space
ok 11 - row 195 accept: F5 fails on unpriv_bwrap (enforce), on complain and on a trailing space
---
duration_ms: 1611.055796
type: 'test'
...
# Subtest: row 195 accept: F5 fails with no bwrap of mosaic's, and passes on comm alone
ok 12 - row 195 accept: F5 fails with no bwrap of mosaic's, and passes on comm alone
---
duration_ms: 772.495962
type: 'test'
...
# Subtest: row 195 accept: F6 fails on a stub that prints nothing, on a path outside claude-<pin> and on a .. segment
ok 13 - row 195 accept: F6 fails on a stub that prints nothing, on a path outside claude-<pin> and on a .. segment
---
duration_ms: 1208.609458
type: 'test'
...
# Subtest: row 195 accept: F7 fails on two live PM launches, one for another business, and no launch.by
ok 14 - row 195 accept: F7 fails on two live PM launches, one for another business, and no launch.by
---
duration_ms: 1277.66317
type: 'test'
...
# Subtest: row 195 accept: F8 fails on 0640, on another owner and on a symlink, and never prints the credential
ok 15 - row 195 accept: F8 fails on 0640, on another owner and on a symlink, and never prints the credential
---
duration_ms: 1323.209668
type: 'test'
...
# Subtest: row 195 accept: F9 fails on another owner, a group-writable mode and a number for the flag
ok 16 - row 195 accept: F9 fails on another owner, a group-writable mode and a number for the flag
---
duration_ms: 1223.483541
type: 'test'
...
# Subtest: row 195 accept: F9 won't run --node as root off the pin path or with a writable directory above it
ok 17 - row 195 accept: F9 won't run --node as root off the pin path or with a writable directory above it
---
duration_ms: 1110.862644
type: 'test'
...
# Subtest: row 195 accept: F11 fails on one *.json under discord/ and passes on an empty directory
ok 18 - row 195 accept: F11 fails on one *.json under discord/ and passes on an empty directory
---
duration_ms: 916.072105
type: 'test'
...
# Subtest: row 195 accept: each P check reports unnamed on its fault, and the exit stays 0
ok 19 - row 195 accept: each P check reports unnamed on its fault, and the exit stays 0
---
duration_ms: 4105.537374
type: 'test'
...
# Subtest: row 195 accept: P2 is unnamed with a supplementary group, and P5 names docker
ok 20 - row 195 accept: P2 is unnamed with a supplementary group, and P5 names docker
---
duration_ms: 1015.859889
type: 'test'
...
# Subtest: row 195 accept: P5 is closed with no jarvis account, and P2 with an empty authorized_keys
ok 21 - row 195 accept: P5 is closed with no jarvis account, and P2 with an empty authorized_keys
---
duration_ms: 400.087324
type: 'test'
...
# Subtest: row 195 accept: P9 is unnamed once for each item missing
ok 22 - row 195 accept: P9 is unnamed once for each item missing
---
duration_ms: 2710.187272
type: 'test'
...
# Subtest: row253 accept: P10 fails its observation with another peer; P11 with the unit disabled
ok 23 - row253 accept: P10 fails its observation with another peer; P11 with the unit disabled
---
duration_ms: 800.289163
type: 'test'
...
# Subtest: row 195 accept: every P check unnamed together still exits 0
ok 24 - row 195 accept: every P check unnamed together still exits 0
---
duration_ms: 348.312367
type: 'test'
...
# Subtest: row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
ok 25 - row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
---
duration_ms: 101.428549
type: 'test'
...
# Subtest: row 195 accept: usage errors and a run without --root as a user exit 2
ok 26 - row 195 accept: usage errors and a run without --root as a user exit 2
---
duration_ms: 11.252533
type: 'test'
...
# Subtest: row 195 accept: every in-instance call goes through sudo -u mosaic -H env PATH=<node dir>:...
not ok 27 - row 195 accept: every in-instance call goes through sudo -u mosaic -H env PATH=<node dir>:...
---
duration_ms: 448.51281
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:625:1'
failureType: 'testCodeFailure'
error: |-
The input did not match the regular expression /^talk chief case1-accept-[0-9a-f]{12}: .* --business mosaic-stack --tester --wait 300$/. Input:
'talk chief case1-accept-7f454c46: this is the Case 1 acceptance; reply with one short line. --business mosaic-stack --tester --wait 300'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
actual: 'talk chief case1-accept-7f454c46: this is the Case 1 acceptance; reply with one short line. --business mosaic-stack --tester --wait 300'
operator: 'match'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:641:10)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.processPendingSubtests (node:internal/test_runner/test:974:18)
Test.postRun (node:internal/test_runner/test:1542:19)
Test.run (node:internal/test_runner/test:1467:12)
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
...
# Subtest: row 195 accept: the host check runs with --check and never --enforce
ok 28 - row 195 accept: the host check runs with --check and never --enforce
---
duration_ms: 421.488991
type: 'test'
...
1..28
# tests 28
# suites 0
# pass 27
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 41874.061683
@@ -0,0 +1,34 @@
{
"grant": "sage-2279",
"stage": "green",
"steps": [
{
"code": 0,
"timeout": false,
"seconds": 19.459,
"log": "agents/gander/work/queue-253/run1/green/focused-1.log",
"name": "focused-1",
"expected_red": false,
"counts": {
"tests": 36,
"pass": 36,
"fail": 0,
"skipped": 0
}
},
{
"code": 1,
"timeout": false,
"seconds": 41.962,
"log": "agents/gander/work/queue-253/run1/green/focused-2.log",
"name": "focused-2",
"expected_red": false,
"counts": {
"tests": 28,
"pass": 27,
"fail": 1,
"skipped": 0
}
}
]
}
@@ -0,0 +1,6 @@
{
"file": "packages/cli/tests/case1-accept.test.mjs",
"before": "10d2117fc497d5a26b390b0820c5b6b225d294fc6539b4a62d7d2ded9c5c9079",
"after": "21d6471c6ce2b3c0852cc723eae0731d1ced8ad1ddd6269a4f2e5026d556bfb8",
"cause": "Broad od fake intercepted existing random-token generator. Narrowed to -An -tx1 -N4, delegating other calls. Assertion unchanged. No tests run after stop."
}
@@ -0,0 +1,4 @@
{
"scripts/case1-accept.sh": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"scripts/case2-unit.sh": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd"
}
@@ -0,0 +1,5 @@
{
"count": 0,
"processes": [],
"scope": "Own worktree and grant TMPDIR cwd, excluding current ancestors. Both product group-cleanup tests passed."
}
@@ -0,0 +1 @@
/mnt/storage/scratch/tmp/glFUYC2
@@ -0,0 +1,37 @@
# Row253 M1-M33 results
| Check | Outcome | Base seconds | Changed seconds |
|---|---|---:|---:|
| M1 | killed | 0.214 | 0.214 |
| M2 | killed | 0.465 | 0.465 |
| M3 | killed | 0.365 | 0.365 |
| M4 | killed | 0.916 | 1.217 |
| M5 | killed | 0.916 | 0.916 |
| M6 | killed | 0.916 | 0.315 |
| M7 | killed | 0.214 | 0.164 |
| M8 | killed | 0.916 | 0.766 |
| M9 | killed | 0.916 | 0.916 |
| M10 | killed | 0.916 | 1.016 |
| M11 | killed | 0.916 | 1.067 |
| M12 | killed | 0.365 | 0.415 |
| M13 | killed | 0.365 | 0.615 |
| M14 | killed | 0.365 | 0.615 |
| M15 | killed | 0.515 | 0.916 |
| M16 | killed | 0.916 | 0.515 |
| M17 | killed | 6.279 | 0.515 |
| M18 | killed | 6.279 | 1.317 |
| M19 | killed | 6.279 | 2.920 |
| M20 | killed | 6.279 | 2.218 |
| M21 | killed | 6.279 | 3.672 |
| M22 | killed | 6.279 | 3.822 |
| M23 | killed | 6.279 | 5.626 |
| M24 | killed | 0.365 | 0.215 |
| M25 | killed | 0.615 | 0.615 |
| M26 | killed | 6.279 | 5.776 |
| M27 | killed | 0.615 | 0.615 |
| M28 | killed | 0.916 | 0.716 |
| M29 | killed | 0.916 | 0.816 |
| M30 | killed | 0.916 | 0.415 |
| M31 | killed | 0.214 | 0.215 |
| M32 | killed | 0.214 | 0.265 |
| M33 | killed | 0.365 | 0.967 |
@@ -0,0 +1,6 @@
{
"reason": "Static C4 sudo monitor/session gap: cannot establish SIGKILL group containment for a policy using a pty. No failing TAP entries observed before interruption.",
"window_pid": 221862,
"pipeline_group": 612045,
"signal": "SIGTERM"
}
@@ -0,0 +1,3 @@
# Run2 deliberate interruption
The raw Webui/whole-controller traceback says unexpected failure because the generic driver reports any nonzero pipeline status that way. This pipeline received SIGTERM from Gander for the static C4 gap; INTERRUPT.json records the owned group. Sage-2307 item5 rules this an interruption, not a test failure. CLI had completed 755/0 before Webui started; Webui has no completed total, and ten shells never started. Sweep count0. No run followed the stop.
@@ -0,0 +1,4 @@
{
"scripts/case1-accept.sh": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"scripts/case2-unit.sh": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd"
}
@@ -0,0 +1,71 @@
{
"grant": "sage-2297",
"stop": "Deliberate interruption for static C4 gap; sage-2307 says Webui interrupt is not a failure.",
"green": [
{
"code": 0,
"timeout": false,
"seconds": 16.449,
"log": "agents/gander/work/queue-253/run2/green/focused-1.log",
"name": "focused-1",
"expected_red": false,
"counts": {
"tests": 36,
"pass": 36,
"fail": 0,
"skipped": 0
}
},
{
"code": 0,
"timeout": false,
"seconds": 37.55,
"log": "agents/gander/work/queue-253/run2/green/focused-2.log",
"name": "focused-2",
"expected_red": false,
"counts": {
"tests": 28,
"pass": 28,
"fail": 0,
"skipped": 0
}
}
],
"whole": [
{
"code": 0,
"timeout": false,
"seconds": 597.084,
"log": "agents/gander/work/queue-253/run2/whole/cli.log",
"name": "cli",
"expected_red": false,
"counts": {
"tests": 755,
"pass": 755,
"fail": 0,
"skipped": 0
}
},
{
"code": -15,
"timeout": false,
"seconds": 4.736,
"log": "agents/gander/work/queue-253/run2/whole/webui.log",
"name": "webui",
"expected_red": false,
"counts": {}
}
],
"changes": {
"killed": 33,
"survivors": 0,
"timeouts": 0,
"base_seconds": 11.808,
"changed_seconds": 41.202
},
"shells": "NOT RUN",
"sweep": {
"count": 0,
"processes": []
}
}
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 factory refuses retired TCP", "packages/cli/tests/case2-unit.test.mjs"]
@@ -0,0 +1,16 @@
TAP version 13
# Subtest: row253 factory refuses retired TCP and a value after the socket marker
ok 1 - row253 factory refuses retired TCP and a value after the socket marker
---
duration_ms: 29.223639
type: 'test'
...
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 141.584967
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 factory refuses retired TCP", "packages/cli/tests/case2-unit.test.mjs"]
@@ -0,0 +1,37 @@
TAP version 13
# Subtest: row253 factory refuses retired TCP and a value after the socket marker
not ok 1 - row253 factory refuses retired TCP and a value after the socket marker
---
duration_ms: 17.019714
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:494:1'
failureType: 'testCodeFailure'
error: |-
The input did not match the regular expression /case2-unit.sh retires tester TCP; use --tester-socket/. Input:
'case2-unit: unknown serve flag: --tester-port\n'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
actual: |-
case2-unit: unknown serve flag: --tester-port
operator: 'match'
stack: |-
refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:219:10)
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:496:3)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.start (node:internal/test_runner/test:1262:17)
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 127.863578
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 installer refuses unsafe location and query facts", "packages/cli/tests/case2-unit.test.mjs"]
@@ -0,0 +1,36 @@
TAP version 13
# Subtest: row253 installer refuses unsafe location and query facts before unit writes
not ok 1 - row253 installer refuses unsafe location and query facts before unit writes
---
duration_ms: 820.638371
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:134:1'
failureType: 'testCodeFailure'
error: |-
expected a refusal, got 0: case2-unit: wrote /mnt/storage/scratch/tmp/gR3d8dO/case2-unit-eR5it3/units/[email protected] and /mnt/storage/scratch/tmp/gR3d8dO/case2-unit-eR5it3/units/mosaic-webui.service from copy aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; enabled and started [email protected], then mosaic-webui.service
0 !== 1
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 1
actual: 0
operator: 'strictEqual'
stack: |-
refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:218:10)
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:157:39)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.start (node:internal/test_runner/test:1262:17)
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 937.440456
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 installer refuses unsafe location and query facts", "packages/cli/tests/case2-unit.test.mjs"]
@@ -0,0 +1,36 @@
TAP version 13
# Subtest: row253 installer refuses unsafe location and query facts before unit writes
not ok 1 - row253 installer refuses unsafe location and query facts before unit writes
---
duration_ms: 840.318392
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:134:1'
failureType: 'testCodeFailure'
error: |-
expected a refusal, got 0: case2-unit: wrote /mnt/storage/scratch/tmp/gR3d8dO/case2-unit-CGGJnM/units/[email protected] and /mnt/storage/scratch/tmp/gR3d8dO/case2-unit-CGGJnM/units/mosaic-webui.service from copy aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; enabled and started [email protected], then mosaic-webui.service
0 !== 1
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 1
actual: 0
operator: 'strictEqual'
stack: |-
refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:218:10)
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case2-unit.test.mjs:157:39)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.start (node:internal/test_runner/test:1262:17)
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 962.598357
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "a bad args file refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -0,0 +1,16 @@
TAP version 13
# Subtest: row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
ok 1 - row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
---
duration_ms: 160.205676
type: 'test'
...
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 276.702477
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "a bad args file refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -0,0 +1,30 @@
TAP version 13
# Subtest: row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
not ok 1 - row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
---
duration_ms: 141.954375
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:587:1'
failureType: 'testCodeFailure'
error: 'retired TCP'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
actual: 'case1-accept: refusing: unknown serve flag --tester-port'
operator: 'match'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:608:12)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.start (node:internal/test_runner/test:1262:17)
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 298.371364
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "a bad args file refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -0,0 +1,64 @@
TAP version 13
# Subtest: row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
not ok 1 - row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
---
duration_ms: 419.508161
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:587:1'
failureType: 'testCodeFailure'
error: |-
no tester socket: F1 units-user pass [email protected] and mosaic-webui.service are active as mosaic (uid 997)
F2 console-loopback pass healthz answers, and 732 is bound to 127.0.0.1 only, by MainPID 200
F3 tester-reply pass chief replied to case1-accept-511942f73671
F4 precheck pass case2-precheck.sh --profile factory exits 0
F5 bwrap-label pass bwrap 300 reads 'bwrap (enforce)' and its claude descendant 302 reads 'bwrap//&unpriv_bwrap (enforce)'
F6 claude-pin pass /opt/mosaic-stack/claude-2.1.0/node_modules/@anthropic-ai/claude-code/bin/claude.exe prints '2.1.0 (Claude Code)'
F7 pm-live pass one live launch: pm run r-0002
F8 credential-file pass /srv/mosaic/.config/mosaic-dev/secrets/claude-session.env is 'mosaic 600 regular file'
F9 autoupdater-off pass /etc/claude-code/managed-settings.json is root:root 0644 with env.DISABLE_AUTOUPDATER "1"
F10 node-pin pass /opt/mosaic-stack/node-v24.1.0/bin/node prints v24.1.0
F11 no-discord pass /srv/mosaic/.mosaic-dev/discord is absent
P1 mosaic-sudo closed sudo -l: User mosaic is not allowed to run sudo on dev-53.
P2 mosaic-groups-keys closed id -G is '997'; authorized_keys is absent
P3 localadmin-sudo accepted Q183 sudo -l -U localadmin lists 1 NOPASSWD lines
P4 pve-root-pbs accepted Q182+Q183 root on pve over VM 1008, and PBS backups that hold /srv/mosaic with the credential; this script can't see pve
P5 jarvis-reach closed jarvis is in none of sudo, docker and lxd (groups: jarvis)
P6 group-members closed sudo holds only localadmin, and docker and lxd have no members
P7 sudoers-d closed no file under /etc/sudoers.d names mosaic
P8 lan-listeners closed 732 is on 127.0.0.1 only by MainPID 200; no TCP listener on 7331 or 7333; tester uses /srv/mosaic/.mosaic-dev/console/tester.sock
P9 bwrap-userns accepted mos-1096 hostcheck items 1-6 ok, jarvis is denied bwrap, and group mosaic has no other member
P10 tester-socket-squat unnamed boundary=observed path=/srv/mosaic/.mosaic-dev/console/tester.sock parent=0700 owner=997 socket=0600 peer_pid=200 peer_uid=997 floor=1024 port=732; hostile same-uid substitution remains
P11 hostcheck-before-bus closed mosaic-hostcheck.service is enabled, and its Before= names [email protected]
count pass 11
count FAIL 0
count closed 7
count accepted Q182+Q183 1
count accepted Q183 1
count accepted mos-1096 1
count unnamed 1
case1-accept: every F check passes; send the whole output to Duckworth, and each unnamed line to Mos
0 !== 2
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 2
actual: 0
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:606:12)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.start (node:internal/test_runner/test:1262:17)
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 532.619824
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "a bad args file refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -0,0 +1,64 @@
TAP version 13
# Subtest: row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
not ok 1 - row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason
---
duration_ms: 445.63961
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:587:1'
failureType: 'testCodeFailure'
error: |-
socket value: F1 units-user pass [email protected] and mosaic-webui.service are active as mosaic (uid 997)
F2 console-loopback pass healthz answers, and 732 is bound to 127.0.0.1 only, by MainPID 200
F3 tester-reply pass chief replied to case1-accept-fed0bb351858
F4 precheck pass case2-precheck.sh --profile factory exits 0
F5 bwrap-label pass bwrap 300 reads 'bwrap (enforce)' and its claude descendant 302 reads 'bwrap//&unpriv_bwrap (enforce)'
F6 claude-pin pass /opt/mosaic-stack/claude-2.1.0/node_modules/@anthropic-ai/claude-code/bin/claude.exe prints '2.1.0 (Claude Code)'
F7 pm-live pass one live launch: pm run r-0002
F8 credential-file pass /srv/mosaic/.config/mosaic-dev/secrets/claude-session.env is 'mosaic 600 regular file'
F9 autoupdater-off pass /etc/claude-code/managed-settings.json is root:root 0644 with env.DISABLE_AUTOUPDATER "1"
F10 node-pin pass /opt/mosaic-stack/node-v24.1.0/bin/node prints v24.1.0
F11 no-discord pass /srv/mosaic/.mosaic-dev/discord is absent
P1 mosaic-sudo closed sudo -l: User mosaic is not allowed to run sudo on dev-53.
P2 mosaic-groups-keys closed id -G is '997'; authorized_keys is absent
P3 localadmin-sudo accepted Q183 sudo -l -U localadmin lists 1 NOPASSWD lines
P4 pve-root-pbs accepted Q182+Q183 root on pve over VM 1008, and PBS backups that hold /srv/mosaic with the credential; this script can't see pve
P5 jarvis-reach closed jarvis is in none of sudo, docker and lxd (groups: jarvis)
P6 group-members closed sudo holds only localadmin, and docker and lxd have no members
P7 sudoers-d closed no file under /etc/sudoers.d names mosaic
P8 lan-listeners closed 732 is on 127.0.0.1 only by MainPID 200; no TCP listener on 7331 or 7333; tester uses /srv/mosaic/.mosaic-dev/console/tester.sock
P9 bwrap-userns accepted mos-1096 hostcheck items 1-6 ok, jarvis is denied bwrap, and group mosaic has no other member
P10 tester-socket-squat unnamed boundary=observed path=/srv/mosaic/.mosaic-dev/console/tester.sock parent=0700 owner=997 socket=0600 peer_pid=200 peer_uid=997 floor=1024 port=732; hostile same-uid substitution remains
P11 hostcheck-before-bus closed mosaic-hostcheck.service is enabled, and its Before= names [email protected]
count pass 11
count FAIL 0
count closed 7
count accepted Q182+Q183 1
count accepted Q183 1
count accepted mos-1096 1
count unnamed 1
case1-accept: every F check passes; send the whole output to Duckworth, and each unnamed line to Mos
0 !== 2
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 2
actual: 0
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:606:12)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.start (node:internal/test_runner/test:1262:17)
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 550.692311
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 acceptance rejects bad config", "packages/cli/tests/case1-accept.test.mjs"]
@@ -0,0 +1,16 @@
TAP version 13
# Subtest: row253 acceptance rejects bad config and unsupported copies before talk
ok 1 - row253 acceptance rejects bad config and unsupported copies before talk
---
duration_ms: 323.110026
type: 'test'
...
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 423.527216
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 acceptance rejects bad config", "packages/cli/tests/case1-accept.test.mjs"]
@@ -0,0 +1,64 @@
TAP version 13
# Subtest: row253 acceptance rejects bad config and unsupported copies before talk
not ok 1 - row253 acceptance rejects bad config and unsupported copies before talk
---
duration_ms: 687.049369
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:263:1'
failureType: 'testCodeFailure'
error: |-
failed: F1 units-user pass [email protected] and mosaic-webui.service are active as mosaic (uid 997)
F2 console-loopback pass healthz answers, and 732 is bound to 127.0.0.1 only, by MainPID 200
F3 tester-reply pass chief replied to case1-accept-e4b5e2852bf0
F4 precheck pass case2-precheck.sh --profile factory exits 0
F5 bwrap-label pass bwrap 300 reads 'bwrap (enforce)' and its claude descendant 302 reads 'bwrap//&unpriv_bwrap (enforce)'
F6 claude-pin pass /opt/mosaic-stack/claude-2.1.0/node_modules/@anthropic-ai/claude-code/bin/claude.exe prints '2.1.0 (Claude Code)'
F7 pm-live pass one live launch: pm run r-0002
F8 credential-file pass /srv/mosaic/.config/mosaic-dev/secrets/claude-session.env is 'mosaic 600 regular file'
F9 autoupdater-off pass /etc/claude-code/managed-settings.json is root:root 0644 with env.DISABLE_AUTOUPDATER "1"
F10 node-pin pass /opt/mosaic-stack/node-v24.1.0/bin/node prints v24.1.0
F11 no-discord pass /srv/mosaic/.mosaic-dev/discord is absent
P1 mosaic-sudo closed sudo -l: User mosaic is not allowed to run sudo on dev-53.
P2 mosaic-groups-keys closed id -G is '997'; authorized_keys is absent
P3 localadmin-sudo accepted Q183 sudo -l -U localadmin lists 1 NOPASSWD lines
P4 pve-root-pbs accepted Q182+Q183 root on pve over VM 1008, and PBS backups that hold /srv/mosaic with the credential; this script can't see pve
P5 jarvis-reach closed jarvis is in none of sudo, docker and lxd (groups: jarvis)
P6 group-members closed sudo holds only localadmin, and docker and lxd have no members
P7 sudoers-d closed no file under /etc/sudoers.d names mosaic
P8 lan-listeners closed 732 is on 127.0.0.1 only by MainPID 200; no TCP listener on 7331 or 7333; tester uses /srv/mosaic/.mosaic-dev/console/tester.sock
P9 bwrap-userns accepted mos-1096 hostcheck items 1-6 ok, jarvis is denied bwrap, and group mosaic has no other member
P10 tester-socket-squat unnamed boundary=observed path=/srv/mosaic/.mosaic-dev/console/tester.sock parent=0700 owner=997 socket=0600 peer_pid=200 peer_uid=997 floor=1024 port=732; hostile same-uid substitution remains
P11 hostcheck-before-bus closed mosaic-hostcheck.service is enabled, and its Before= names [email protected]
count pass 11
count FAIL 0
count closed 7
count accepted Q182+Q183 1
count accepted Q183 1
count accepted mos-1096 1
count unnamed 1
case1-accept: every F check passes; send the whole output to Duckworth, and each unnamed line to Mos
0 !== 2
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: 2
actual: 0
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:274:12)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.start (node:internal/test_runner/test:1262:17)
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 808.7987
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 P8 refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -0,0 +1,16 @@
TAP version 13
# Subtest: row253 P8 refuses both retired TCP and board listeners
ok 1 - row253 P8 refuses both retired TCP and board listeners
---
duration_ms: 721.532961
type: 'test'
...
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 829.227296
@@ -0,0 +1 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 P8 refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -0,0 +1,40 @@
TAP version 13
# Subtest: row253 P8 refuses both retired TCP and board listeners
not ok 1 - row253 P8 refuses both retired TCP and board listeners
---
duration_ms: 331.920117
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:294:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
[
+ 'closed'
- 'unnamed'
]
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
0: 'unnamed'
actual:
0: 'closed'
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:297:12)
Test.runInAsyncScope (node:async_hooks:226:14)
Test.run (node:internal/test_runner/test:1402:25)
Test.start (node:internal/test_runner/test:1262:17)
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 436.160921

Some files were not shown because too many files have changed in this diff Show More