Compare commits

..
Author SHA1 Message Date
jason.woltje 631a256ba4 row 206 (193b2, #1704): sage-2818 rulings, only a human principal matches a share
#readable returns false for an agent session, so a `*` share no longer shows a user's message
in an agent's trail. New tests: a sharee's session.list refuses (Q1), and an agent bound to the
shared thread, a chief run and the tester match no share (Q2). BUILD.md records the Q1
follow-up, Q2-Q4 and the live --migrate as Mos's go under Q173. README names the agent rule.
2026-10-22 11:53:19 -05:00
jason.woltje 16b9c6df6f row 206 (193b2, #1704): thread shares, schema 3l→3m
thread_shares (row 193 design 5.2) with its index and append-only triggers; the
3l-3m step only adds them. thread.share, thread.unshare and thread.shares for a
thread's owner; #readable holds design 5.4's four rules, #threadRead serves
thread.messages only, and thread.list {shared:true} adds owner. replies and
trail show shared user threads to the owner and readers. COUNTED, LISTED,
timedTables and the clock list take the table. Tests: thread-shares.test.mjs
and the 3l→3m pins. Packet agents/launchpad/work/queue-206/BUILD.md. Not run
yet: node waits for Sage's CLEAR (sage-2789).
2026-10-22 11:46:10 -05:00
jason.woltje aca16404cf queue: revs 3195-3216. Rows 278, 281 and 264a reviews, notes and done moves (sage, gizmoduck, megavolt, gander, webby, dewey, duckworth); row 264 round 1 request posted as jarvis (sage-2693, #1769) 2026-10-22 09:25:48 -05:00
jason.woltje f7e4ec3df5 row 264a (#1769): BUILD.md records the r13 gate on 2cc34aef8 (phases 1-3 green, M2 thrown at the trigger) and the r13p4 rerun at 84f53540d (M1-M4 killed on assertions, M2's four edits and kill rule), and the limits row gets the measured probe time; doc only (mos-1280 item 1, sage-2701) 2026-10-22 09:23:39 -05:00
jason.woltje b789188504 docs: row 264a landing sweep. BUILD-LOG gets Rocko's V502 rerun line and Sage's V502, rows 278/281 and row 264a landing lines; SESSIONS gets Dewey's, Rocko's and Sage's lines; DEFERRED moves the fire-keeps-grants gap to Done (row 281) and opens row 281 N1/N2 and row 264a N1 (actor_run, binds row 269) and N2 (esbuild skip) (sage-2695, #1769) 2026-10-22 09:22:05 -05:00
jason.woltje 84f53540dd row 264a r13 (#1769): BUILD.md writes M2's four edits before phase 4 reruns: the receive filter, delivery_same_session dropped from 3k-3l.sql and schema.sql, and store.mjs's SCHEMA_SHA256 set to the mutated schema; a kill counts only as a receive-test assertion; doc only (mos-1279, sage-2688, sage-2690, duckworth-0143, duckworth-0144) 2026-10-22 09:08:25 -05:00
jason.woltje 2cc34aef87 row 264a r13 (#1769): threads.test :341 takes the 3l trigger's text, 'only to its session's holder' (delivery_same_session, 3k-3l.sql:153); BUILD.md raises the stack-release limit to 1800 s from the survey's 575 s and records the r10 and r11 reds and the r12 survey with M1 in Runs; test only (mos-1278, sage-2682) 2026-10-22 08:36:56 -05:00
jason.woltje 93f4af12bc row 264a r12 (#1769): sessions.test T2's refused() keeps every table but events equal and pins one action.refused {code: not-holder} event per refused call in call order; the old every-table check could never pass since request()'s catch (broker.mjs:4242) writes the refusal through #refused; BUILD.md records Mos's reading of sage-2257; test only (mos-1277, sage-2676) 2026-10-22 08:21:41 -05:00
jason.woltje 75f049ed23 row 264a r11 (#1769): later3l in account-, gitea- and grant-migration names threads_id_not_session, the 3l trigger on threads (3k-3l.sql:104) the sqlite_master compares missed; test only (mos-1276 option A, sage-2672) 2026-10-22 08:14:15 -05:00
jason.woltje 54f65f9abd row 264a r10 (#1769): rebase on landed 281 (e479521d3), all 16 =; broker.mjs overlap read; cli limit 1800 s from 281's 591 s run (sage-2665, sage-2668, sage-2647) 2026-10-22 08:08:28 -05:00
jason.woltje 7451878b66 row 264a r9 (#1769): rebase on landed 278 (385a7194c), all 15 =; webui read check; run limits at 3x the longest recorded run (sage-2647, mos-1275) 2026-10-22 08:07:43 -05:00
jason.woltje 16889a46be row 264a r8 (#1769): build notes correction for the r7 red (account-migration, sqlite_sequence rows from the 3k-3l inserts), the whole-snapshot audit of every migrating test, the r7 run in Runs (sage-2621, duckworth-0129) 2026-10-22 08:07:43 -05:00
jason.woltje c3f43960eb row 264a r8 (#1769): the six migration tests that compare a whole store with sqlite_sequence after the 3k-3l step leave out the sessions and session_defaults sequence rows and assert them exactly, so any other new row still fails; usage-migration reads sqlite_sequence by name (sage-2621, duckworth-0129); unrun 2026-10-22 08:07:43 -05:00
jason.woltje 0431778465 row 264a r7 (#1769): build notes for the rebase onto landed 277 (980cec88c, sage-2608) and the per-session thread.claims; DEFERRED entry binds row 272 to make 274's user reply check session-scoped (sage-2591) 2026-10-22 08:07:43 -05:00
jason.woltje f3e9b6512c row 264a r7 (#1769): thread.claims reads each session's latest claim, so a thread with two held sessions lists both (sage-2591, duckworth-0124 C4); thread-claims.test threads get their default session and its T5 message names it (C5), T6 two held sessions; unrun 2026-10-22 08:07:43 -05:00
jason.woltje b7a05482bb row 264a r6 (#1769): build notes for the rebase onto landed 274 (0165be266, sage-2588): the four user-principal conflicts, users stay at the ruled seven verbs, and why 274's thread-scoped reply check needs no session clause yet 2026-10-22 08:07:43 -05:00
jason.woltje 4beb6433a4 row 264a r5 (#1769): build notes correction, rounds 1-4 never parsed three test files and no run claimed them; the round 5 gate runs every touched suite (sage-2581) 2026-10-22 08:07:43 -05:00
jason.woltje fbfaab3655 row 264a r5 (#1769): build notes for the rebase onto landed 236 (47a35bf43, sage-2579): the 194 conflicts, message-filter.test as the 19th test file, and the escaped quotes in three DEFAULT_SESSION blocks 2026-10-22 08:07:43 -05:00
jason.woltje 9ff668550f row 264a r5 (#1769): build notes name the op rename to legacy-3l and migration-3l by the step's target version (sage-2415) 2026-10-22 08:07:43 -05:00
jason.woltje 5baaaa447b row 264a r5 (#1769): build notes for the 3k-3l step on row 236 (8f51900b5, sage-2412): the renames, the merged pins and lists, gitea-migration reading both steps, and the in-memory dry run on 236's schema; README op value legacy-3l 2026-10-22 08:07:43 -05:00
jason.woltje c4a12de3d8 row 264a r3 (#1769): build notes say the generation fence is equivalent to the holder_run check it replaced, and M1 shows the refusal is tested (duckworth-0108, sage-2270) 2026-10-22 08:07:43 -05:00
jason.woltje 72fc5a330b row 264a r2 (#1769): generation fence for thread runs, a retired generation's approval, delivery and read refuse and write nothing (sage-2257); #route refuses an unknown session, README default-op note, Row 155 comment reworded, mutants M1-M3 in the build notes (duckworth-0107) 2026-10-22 08:07:43 -05:00
jason.woltje d6301dffce row 264a (#1769, T1 item 7): bus schema 3k to 3l, sessions below a thread
sessions and session_defaults are new and append-only. thread_claims, message_threads and
thread_opens are rebuilt in place with a session column, and thread_claims gains a generation
counted from claim rows. Each existing thread gets a default session with the thread's id; new
threads get a random one. Holder, per-run, release and delivery triggers key on the session,
and two id triggers keep session and thread ids apart (Duckworth checks 1-7, duckworth-0106).

The broker binds a thread run to a session (the default when the record names none), and
thread.open, thread.send and thread.messages take an optional session. New verbs session.list
and session.create (idempotent on op). Tests: sessions.test.mjs for T1 bus half, T2 and T7 with
a pinned 3k fixture; count, version and verb pins; hand-made test threads write the legacy
default session.

Built on row 236 (8f51900b5), which takes 3j-3k, so this step is 3k-3l by landing order
(row264 Q2, sage-2412). The op values are legacy-3l and migration-3l. gitea-migration.test reads
its store after both steps. Not run yet: every run waits for Sage's CLEAR.
2026-10-22 08:07:43 -05:00
jason.woltje ae525c4095 gizmoduck: row 264a bus design in the build notes: sessions and session_defaults, thread_claims, message_threads and thread_opens rebuilt with a session (legacy default id = thread id), generation, broker lookups by session, verbs, tests and pinned lists 2026-10-22 08:07:43 -05:00
jason.woltje b5630ca198 gizmoduck: row 264 build notes record Sage's Q1-Q7 rulings (sage-2235, 5f5b51dd6) 2026-10-22 08:07:43 -05:00
jason.woltje 26380e3f0b gizmoduck: row 264 phase A build notes, code at da9555ca2, consumer audit first pass, five proposed slices and Q1-Q7 to Sage (sage-2233, #1769, Vikunja 463) 2026-10-22 08:07:43 -05:00
jason.woltje e479521d3a row 281 build notes: the manifest-round slot on 7979fb41e is green; Runs has the base, anchors, M1 exit check, dewey-0840 files, bus/business/cli whole and the shells with their limits and named skips; Mutants has M1-M22, 22 killed, M22 fails only T14 (sage-2661, dewey-0845, #1786) 2026-10-22 08:00:45 -05:00
jason.woltje 7979fb41eb row 281 build notes: the gate plan for row281-e, with each scripts/test-*.sh under a limit of at least 3x its longest recorded run (sage-2649, dewey-0840, mos-1275, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje 54ebc5d71c row 281 b5: T14 pins that the human's no takes a declined host decision out of the set, after hosts.remove left it with no host (N1); M22 drops the closed check and T14 kills it; test only, not yet run (sage-2653, dewey-0844, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje 5e05e21794 row 281 build notes: the mutant table names section 9's killing test and each mutant's failing tests from its log; every mutant fails its named test (dewey-0841, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje 6b6eb533e8 row 281 b4: T6b pins step 0's four Gitea reads in order and says 'before any Gitea write'; M1-M21 all killed on b4a 3f29375a8 with the M1 exit check, table in the build notes (dewey-0839, sage-2627, sage-2643, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje f9fcf5d7b3 row 281 b4a: the M1 hang was T6c's held web1:add loop, which only the fire's abort ends; setup's t.after clears held and failing, and T6c and T10 release their Gitea gate; test only, no code change (mos-1273, sage-2636, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje c055dddf44 row 281 b3: the first base run's five red tests were test faults, no code change; T4b reads the manual's revoke id from its event, T5b expects gitea-left:<id>,tokens (N10), T6b allows step 0's GET checks, gitea T8 leaves the call log out, cli T10 makes the 0700 store root; rebase onto 980cec88c noted (sage-2605, sage-2615, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje 6d62ef04e0 row 281 build notes: rebase onto 46dedf925 after row 256, range-diff all =, 256's actor threading doesn't change what a delete calls (sage-2597, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje 4982fa0adf row 281 b2: T1-T13 tests (fire-revokes-grants, lifecycle-revoke T1, T8 tester refusals, cli T10 pass) and the test choices in the build notes (#1786) 2026-10-22 07:42:10 -05:00
jason.woltje 7c2c876319 row 281 b1: step G in the broker (fire and delete jobs, the grant set, the lock, boot's interrupted check) and build notes (#1786) 2026-10-22 07:42:10 -05:00
jason.woltje 7d5f16ef95 row 281 build notes: Dewey's d6 approval with N7 (T12's step B cause survives step G) and N8 (no decision-consumed for a within-role fire) (dewey-0829, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje a7b4f6785c row 281 design d6: the job's step C without the chief's claim, fire.failed for a step B or C failure, boot closes interrupted jobs, Q1 lines in the set (dewey-0825, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje 938378455d row 281 design d5: two requests per granted delete with its five conditions, a job id on every answer, event and message (sage-2575, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje 5f4cd2faa6 row 281 design d4: step G detached from the verbs, open decisions and a fired-agent raise check, one step G per agent (dewey-0823, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje 6faa0f1479 row 281 design d3: a grant decision approved before a fire and executed after it, tests T6/T6b/T6c (sage-2574, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje 9867a6f322 row 281 design d2: Q1/Q2 rulings (sage-2573), the finding in the Why, T3b/T4b and M8/M9 (#1786) 2026-10-22 07:42:10 -05:00
jason.woltje 15ab01c0c3 row 281 design d1: a fire or a delete revokes the instance's host and Gitea grants (sage-2568, #1786) 2026-10-22 07:42:10 -05:00
jason.woltje 385a7194c7 docs(row278): record green third gate and final review evidence 2026-10-22 07:32:56 -05:00
jason.woltje 3e4c77ac60 docs(row278): pin Mos-approved fixture and per-script timeout receipts 2026-10-22 07:21:05 -05:00
jason.woltje 9696d1fcb7 test(webui): supply empty thread claims in shell bus fixture 2026-10-22 07:17:46 -05:00
jason.woltje f99c39a3f0 docs(row278): void run2 and record legacy fixture stop 2026-10-22 07:17:12 -05:00
jason.woltje 42abda031f docs(row278): void run1 and pin approved fixture repair 2026-10-22 07:09:57 -05:00
jason.woltje 964b6b1848 test(webui): export bus error in thread claims loader fake 2026-10-22 07:08:42 -05:00
jason.woltje 0011fe357e docs(row278): pin committed source and accepted gate rules 2026-10-22 06:40:47 -05:00
jason.woltje e7d21b91b8 feat(webui): show thread claims separately in Agents 2026-10-22 06:40:05 -05:00
jason.woltje 2c350a4669 docs(row278): freeze thread claims candidate for the gate 2026-10-22 06:39:00 -05:00
rocko 6af85df4be docs: record V502 rerun on 980cec88c 2026-10-22 06:28:40 -05:00
rocko 04a86cf338 docs: record Vikunja 502 focused verification and rebase evidence 2026-10-22 06:24:27 -05:00
rocko 407c8d7be2 test: harden processing failure diagnostics and cleanup 2026-10-22 06:24:27 -05:00
jason.woltje 980cec88c2 queue: revs 3188-3194, row 277 in-review (Morgana 3188, request failed 3189, no token), review request 3190/3191 (Sage, c30538), Duckworth approve 3192 (c30539, posted by Sage as jarvis), landing note and done 3193/3194 (Sage) 2026-10-22 06:23:06 -05:00
jason.woltje c33cba810c docs: row 277 landing log line, Sage's session line, and the M8 resolve-pin DEFERRED entry (Duckworth N1, binds the next tester.test.mjs row) 2026-10-22 06:21:43 -05:00
jason.woltje 01ea1fb11b row 236 build notes: the runs and mutants from the 10-22 slot (2c, the whole gate at bd5f11ded, 40/40 killed), doc-only (#1742) 2026-10-22 06:21:17 -05:00
jason.woltje b831733053 honker: row 256 (#1761) BUILD.md gate results, doc-only after landing f12441d4f: full gate green on 0165be266 (31/0 touched, M1-M4b killed, 18 packages 3757/0, 10 scripts rc=0, 4 skips none touched), Webby approve c30519 2026-10-22 06:21:17 -05:00
jason.woltje 62f53b1e07 morgana: row 277 (#1782) r4: a thread.claims error with no bus code still throws and prints nothing (duckworth-0123 N1, M9); BUILD.md records the rebase onto 0165be266 and N2/N3 2026-10-22 06:01:09 -05:00
jason.woltje bda4ee6138 morgana: row 277 (#1782) r3: a refused thread.claims prints the role lines and the refusal code (N1), cli README agents usage (N2), per sage-2505 2026-10-22 06:01:09 -05:00
jason.woltje 3a5275a9ad morgana: row 277 (#1782) build r2: thread-claims test helper passes a run's capability through, T1 copies store rows before the strict compare (early look sage-2504) 2026-10-22 06:01:09 -05:00
jason.woltje eccd6e276e morgana: row 277 (#1782) build: thread.claims bus verb for human and reader sessions (D1), mosaic agents adds thread lines without --tester or --json (C1/C2), message-filter pin moves to thread.claims (C3), tests T1-T8 2026-10-22 06:01:08 -05:00
jason.woltje 571ca3d912 morgana: row 277 (#1782): design folder moves to queue-277 (sage-2486) 2026-10-22 06:01:08 -05:00
jason.woltje 2f7fc6832f morgana: #1782 design r2 (duckworth-0118): C1 tester path makes one call, C2 --json keeps the agents array, C3 pin moves to thread.claims; N1-N4 taken, M7 added 2026-10-22 06:01:08 -05:00
jason.woltje 1d5d00939a morgana: #1782 design r1 (sage-2481): owner thread claims as a human/reader verb thread.claims, agents unchanged; Console and TUI read c.role, so the briefed mixed list would need them too (Q1, Q2 to Duckworth) 2026-10-22 06:01:08 -05:00
jason.woltje 46dedf9259 queue: revs 3181-3187, row 256 in-review (Honker 3181/3182, request failed, no token), review request 3183/3184 (Sage, c30518), Webby approve 3185 (c30519, posted by Sage as jarvis), landing note and done 3186/3187 (Sage) 2026-10-22 06:00:35 -05:00
jason.woltje cac129e23a docs: row 256 landing log line and Sage's session line (f12441d4f, #1761 closed c30520) 2026-10-22 05:59:17 -05:00
jason.woltje f12441d4fa honker: row 256 (#1761) b2: Webby's N1 (webby-0124): instance-settings.mjs:30 and agents.js:46 comments name the assistant, not the chief of staff; BUILD.md note; comment-only, unrun 2026-10-22 05:33:13 -05:00
jason.woltje e617543b8d honker: row 256 (#1761) b1: the human deletes a creative instance and any other actor is refused organization-creative-human-only; protectedRefusal is the delete check and the listing's per-reader endProtected; actor threaded through planDelete/checkDelete/instances.check; fire unchanged; DEFERRED entry closed for delete; tests T1-T8, unrun 2026-10-22 05:33:13 -05:00
jason.woltje d4dfc79b18 honker: row 256 design draft 2 (webby-0122): T4 uses the last-listed refusal (R1), endProtected keeps its name with comments rewritten (Q1), bus test in creative.test.mjs (Q2), M1 killed by T2 only and the chief's human-required pinned (N1), M4 for the actor threading (N2) 2026-10-22 05:33:13 -05:00
jason.woltje b5f1e09d6c honker: row 256 (#1761) design draft 1: one protectedRefusal predicate for delete and the listing's per-viewer endProtected, actor threaded through planDelete/checkDelete/instances.check, fire unchanged, persona file left in place 2026-10-22 05:33:13 -05:00
jason.woltje 0165be2661 queue: revs 3173-3180, row 281 claim (Megavolt 3173), row 274 in-review (Morgana 3174/3175, request failed, no token), review request 3176/3177 (Sage, c30488), Dewey approve 3178, landing note and done 3179/3180 (Sage) 2026-10-22 05:32:22 -05:00
jason.woltje 6d6cc0f608 docs: row 274 landing log lines, Dewey's 274 and 281 session lines, and the N7 DEFERRED entry (runs suite gates mosaic-task.mjs) 2026-10-22 05:31:01 -05:00
jason.woltje 4c485c63b9 morgana: FED R3 r6 (sage-2576): the collaborator-list test pins USER_VERBS as the ruled seven so M3x and M6x fail it, and its off-list set adds the grants, lifecycle, web and host verb lists; test only, unrun 2026-10-22 05:06:52 -05:00
jason.woltje f58cfc3617 morgana: FED R3 r5 (sage-2566): covers, LEVELS and the all marker move into the business vocabulary so the broker keeps its sandbox import closure; access.mjs re-exports them, the pin test lists covers on the vocabulary line, RUNNER_FILES unchanged, M16 on the pin, the closure walk and the confined load; unrun 2026-10-22 04:48:41 -05:00
jason.woltje cee3f25a37 morgana: FED R3 r4 (sage-2486, sage-2557): halt refuses a granted user's thread.send while thread.messages answers (test), row 194's hire refuses an instance named user at the broker and the writer (S1, one test each), the access argument check moves before the store read (S2, test), M12-M15; unrun 2026-10-22 04:48:22 -05:00
jason.woltje 1313bfb718 morgana: FED R3 r2 packet (sage-2299): rulings 1-5 recorded, brief's 193b1a mutant line void, manifest widens to instance-settings and its test; doc only, unrun 2026-10-22 04:48:22 -05:00
jason.woltje 618fe0f0b3 morgana: FED R3 r2 (dewey-0777): role key user refused at the broker load check and by the instance writer (N1, one test each), #threadFor user clause listed as an equivalent mutant, 193b1a filter mutant not applicable, M10/M11 and the M3x two-walls run in the kit; unrun 2026-10-22 04:48:22 -05:00
jason.woltje d20c92bf65 morgana: FED R3 build (#414, sage-2292): user principal on the bus, bindUser with a per-request grant read, the collaborator verb list and its filters, user threads and the B6 reply path, runner user header; unrun 2026-10-22 04:48:22 -05:00
jason.woltje 47a35bf436 queue: revs 3163-3172, row 256 claim (Honker 3163), row 236 in-review (Megavolt 3164/3165), review request 3166/3167, Dewey approve 3168, landing note and done 3169/3170, row 281 add and briefed 3171/3172 (Sage) 2026-10-22 04:47:17 -05:00
jason.woltje 6b32424adb docs: row 236 landing log lines, and row 281 (#1786): a fire or a delete revokes the instance's grants (mos-1271)
Brief docs/plans/2026-10-22_fire-revokes-grants.md, owner Megavolt, reviewer Dewey, gate owner Sage. DEFERRED entry points at row 281. BUILD-LOG and SESSIONS carry Honker's row 194 lines, Dewey's two row 236 lines and Sage's row 236 landing lines.
2026-10-22 04:43:49 -05:00
jason.woltje bd5f11dedd bus: row 236, a gitea grant raise on a broker with no Gitea path refuses gitea-not-configured (sage-2559, #1742) 2026-10-22 03:59:40 -05:00
jason.woltje e40b74bebc bus: row 236, two out-of-manifest pins: gitea is a known grant method, and the broker imports giteaName and parseGiteaUrl (gate run sage-2554, #1742) 2026-10-22 03:57:48 -05:00
jason.woltje ce74477e14 bus: row 236, grants.status is a read under the kill switch, as hosts.status is (sage-2453, #1742)
Row 191's #processingGate refuses agent verbs off its read list while
processing is halted. grants.status joins hosts.status on that list, per
the kill-switch plan (2026-10-20_kill-switch.md:16): reads still work.
An agent's grants.revoke stays refused while halted, as hosts.revoke is.
2026-10-22 03:42:05 -05:00
jason.woltje 4d9d62ae89 bus: row 236 r3, one N5 test per settle branch; README notes for a left admin token and a running executor (sage-2388, #1742) 2026-10-22 03:42:05 -05:00
jason.woltje 5e5ba94b19 bus: row 236 r3, a revoke during a settle whoami ends revoked in the 401 and token-unknown branches (Dewey N5, #1742) 2026-10-22 03:42:05 -05:00
jason.woltje 97bbe7fd30 bus: row 236 r2, a revoke during step 7 or a settle ends revoked; sweep fire retry checks the user id (Dewey B1, B2, #1742) 2026-10-22 03:42:05 -05:00
jason.woltje 91f7029923 bus: row 236 gap test takes any HTTP status in a grant.result (debug run 2, #1742) 2026-10-22 03:42:05 -05:00
jason.woltje be11a1f7e6 bus: row 236 tests for the static mutant gaps M8, M9, M16, M28, M29, M37 (#1742) 2026-10-22 03:42:05 -05:00
jason.woltje d87483d683 bus, cli: #436b Gitea grant path, schema 3j-3k (row 236, #1742)
Gitea grant executor (steps 0-8, settle, restart, revoke and fire, sweep,
status), the grants.* verbs and mosaic grants, the 3j-3k migration with
gitea_users and gitea_grants, and the section 9 tests with a fake Gitea and
a scratch gitea/gitea:1.27.1 X-G3 test. Build notes in
agents/megavolt/work/queue-236/BUILD.md.
2026-10-22 03:42:05 -05:00
jason.woltje be2b008cc0 queue: revs 3156-3162: row 194 Honker in-review and failed request (3156/3157), Sage review request (3158/3159), Webby verdict (3160), Sage landing note and done (3161/3162) 2026-10-22 03:39:38 -05:00
jason.woltje ab7377bcf1 docs: row 194 landing BUILD-LOG and SESSIONS lines (Sage) 2026-10-22 03:38:16 -05:00
jason.woltje fd6dd327c3 honker: row 194 b15 (b') (sage-2507): T15 drops the exited reviewer stand-in's registry entry by hand, the N6 convention, so the post-resume coder launch isn't refused on an entry with no provider/model; BUILD.md notes for (a) and (b') 2026-10-22 03:06:41 -05:00
jason.woltje 5fd178e44d honker: row 194 b15 (a) (sage-2501): the Broker constructor's harnessOn, testerOn and roleFile type checks move ahead of the store reads, so a null store gets the argument error first (chief-of-staff T8b); 191's lines unchanged 2026-10-22 03:06:41 -05:00
jason.woltje 345f3ace67 honker: row 194 b8 r3 docs: correct N4, only session.ended clears the alive flag today 2026-10-22 03:06:41 -05:00
jason.woltje 54ec93d06f honker: row 194 b8 r3 docs (webby-0100, sage-2290): N1 corrects the N6 clock caveat (the bus clock runs ahead, so the error is over-signal); N2 DEFERRED the 64-stop done cap; N3 and N4 follow-ups; no product change 2026-10-22 03:06:41 -05:00
jason.woltje 4f68485272 honker: row 194 b8 r2 (sage-2289): a fire stop stopRun can't confirm (unknown) reads as lifecycle.end.alive with reason unknown, never killed, and the Console flags it; cli T9a no-runtime-record test, bus T9a reason checks, M56; notes 2026-10-22 03:06:41 -05:00
jason.woltje c48239ec3d honker: row 194 b8 notes: base eb3abba12 (row 191), the commit 1, b4 and b6 conflict resolutions file by file, S6 design notes, the unknown-outcome question, mutants and run list 2026-10-22 03:06:41 -05:00
jason.woltje 5d2c274420 honker: row 194 b8 S6 (sage-2241, sage-2246, sage-2261): fire's kill end goes through row 191's stopRun with the fire's request as owner, killed and alive come back as lifecycle.end.killed and lifecycle.end.alive and the Console flags a run still alive; terminate stays SIGTERM-only (V3); halted, the pass closes launches and regenerates processing-halted and a halt joins a fire's stop; terminate's pass narrows to runs spawned by the end's time (N6); T9a, T9b, T15, N6 2026-10-22 03:06:41 -05:00
jason.woltje 9e5ad7c599 honker: row 194 b7 notes: rebase onto dcd8d4dda (row 254), channel-proof pin, N3 as a behaviour change, routes.set keep and M-next, mutant (a) kill file corrected, N6 ruling for S6 2026-10-22 03:06:41 -05:00
jason.woltje 939356a1c2 honker: row 194 b7 (webby-0095 N1, sage-2248): routes.set refuses a kept instance's route to anyone but the human, as the limits writer does 2026-10-22 03:06:41 -05:00
jason.woltje 3521c23c3e honker: row 194 b6 (webby-0094, sage-2241, sage-2245): every instance-writer verb answers restart (B1) and the CLI and Console show it for veto; fire runs the writer's checks against the file before step A, so an unloaded keep can't strand an instance firing; a fire of a delisted name revokes (N1); a host service makes no token reference (N2); only the human re-points a kept instance's model (N3); DEFERRED a human escape for a stuck fire (N4); S6 waits on row 191's stopRun contract 2026-10-22 03:06:41 -05:00
jason.woltje 2311a50e1c honker: row 194 b4 (sage-2164, sage-2166, sage-2217): a hire and the role's hireable list refuse a creative definition; a library mapping change isn't reloaded (T14); the listing's endProtected hides a Delete the server refuses; DEFERRED creative delete; C1 testerNoSignIn closed; row 213's writer-restart pin follows D3 and T16 uses row 196's tester socket 2026-10-22 03:06:41 -05:00
jason.woltje 29daebf548 honker: row 194 b3 sage-1634: commit() checks the revision before the revoke, so a stale delete or fire refuses revision-conflict with nothing revoked (N1); bus T21; option 2 a follow-up 2026-10-22 03:06:41 -05:00
jason.woltje 2f08fd8f23 honker: row 194 b3 sage-1630: BUILD.md follow-up N1, a revoke that succeeds before a refused write (a stale-revision delete does it every time) 2026-10-22 03:06:41 -05:00
jason.woltje b8c3d00362 honker: row 194 b3 sage-1620: credential-revoke-failed says the business file is unchanged and a retry is safe (CLI and Console), W2 checks the log per case 2026-10-22 03:06:41 -05:00
jason.woltje 14124d96d4 honker: row 194 b3 (sage-1605, sage-1610): fire and delete revoke before the write (T20), a human add takes a tracker bot, a store-only delete names the bot; the tester keeps the credential refusal (row 175 W2); channel-proof and project-runtime pins 2026-10-22 03:06:41 -05:00
jason.woltje e7fb59f225 honker: row 194 on row 199's landing cb5fdb064 (sage-1601): honker/row194-b2 85edab7c7 squashed, five conflicts kept both sides (broker imports, constructor, dispatch, the credential and lifecycle verbs; runtime imports; organization write takes detail and context; cli credentials and veto dispatch; vocabulary comment as row 199 wrote it) 2026-10-22 03:06:41 -05:00
jason.woltje e122aad8e2 queue: revs 3148-3155: row 280 Rocko start, in-review and failed review request (3148-3150), Sage review request (3151/3152), Duckworth verdict (3153), Sage landing note and done (3154/3155) 2026-10-22 03:03:05 -05:00
jason.woltje 4c4b8559cf docs: row 280 landing BUILD-LOG and SESSIONS lines (Sage); Rocko's row 280 BUILD-LOG and SESSIONS lines 2026-10-22 03:00:14 -05:00
rocko b0fd28b6f3 docs: keep a slim row 280 packet with full evidence reference 2026-10-22 02:56:40 -05:00
rocko b765f5cb81 test(bus): prove scratch sshd listener ownership before use 2026-10-22 02:28:09 -05:00
jason.woltje da5d3651d7 queue: revs 3138-3147: row 279 Rocko start and in-review (3138-3140), Sage review request (3141/3142), Dewey verdict (3143), Sage landing note and done (3144/3145); row 280 #1785 bus sshd port race added and briefed (Sage 3146/3147, mos-1270) 2026-10-22 02:14:46 -05:00
jason.woltje 82c8281166 sage: brief for the bus sshd port race row (mos-1270, #1785); 279 landing and 194 b16 third-red BUILD-LOG lines; SESSIONS lines (Sage, Rocko 279, Dewey 279 r1 and carry) 2026-10-22 02:12:09 -05:00
rocko ceadad993c docs: record row 279 regression and mutant evidence 2026-10-22 01:03:26 -05:00
rocko f5611c937e fix(processing): observe run status after stop deadline overshoot 2026-10-22 01:03:26 -05:00
jason.woltje 343b6dd766 queue: revs 3134-3137: row 277 brief re-pin (Sage 3134), Morgana 277 claim (3135); row 279 #1784 stopRun deadline added and briefed (Sage 3136/3137, mos-1269) 2026-10-22 00:54:47 -05:00
jason.woltje f40f2e770e sage: brief for the stopRun deadline row (mos-1269), the 191 follow-up row 194 b15's gate found 2026-10-22 00:51:17 -05:00
jason.woltje 23c3d62183 queue: revs 3130-3133 (Sage): row 277 #1782 owner thread claims added and briefed; row 278 #1783 Console thread claims added and briefed 2026-10-22 00:17:54 -05:00
jason.woltje 70e289ba73 sage: #1782 design D1 ruling (sage-2487) and the Console thread-claims follow-up brief 2026-10-22 00:14:35 -05:00
jason.woltje ab50de9fe3 queue: revs 3114-3129: rows 191, 205, 268 landed and done (Sage 3116/3117, 3119/3120, 3123/3124, 3126-3129); Rocko 191 in-review 3114/3115; Dewey 191 verdict 3118; Morgana 205 in-review 3121/3122; Duckworth 205 verdict 3125 2026-10-22 00:12:00 -05:00
jason.woltje b3665f5ab1 sage: docs for landings 191, 205, 268; #1782 owner thread-claims brief (mos-1262); re-pin 86f95bb27 plan; phase 2 window draft 3; seat SESSIONS and BUILD-LOG lines 2026-10-22 00:10:08 -05:00
jason.woltje c27207f6e6 row 268 (C2-01c, #1773, Vikunja 464): playgrounds at three levels; classify checks prefixes first
checkMapping takes kind absent, hybrid or draft. A draft has a name and one
or more prefixes and no books, and comes back with books []. A prefix inside
or holding another entry's book refuses library-playground-other-universe,
which replaces library-playground-outside-book. A prefix equal to a book or
holding its own refuses library-mapping-invalid, as does a draft source.
classify returns a playground file's entry and the book holding its prefix,
or null at universe level and in a draft.
2026-10-22 00:04:42 -05:00
jason.woltje ce6a96ad61 morgana: row 205 r2 (sage-2430): put() events carry their decision so the decision trail reaches them; pin checks the thread claim via threadsState, owner read is a follow-up row 2026-10-21 23:49:34 -05:00
jason.woltje 59f708ca21 morgana: row 205a build (sage-2288): trail and replies hide other users' thread messages from owner, reader and tester; hidden answers as unknown; #readable owner-only; design d2 rulings 2026-10-21 23:49:34 -05:00
jason.woltje f751505ada morgana: row 205 design d1 (sage-2280): FED R3 unfiled, so split 205a message filter now and 205b viewer verbs after R3; mutant 3 to 193b2; hidden trail subject returns [] 2026-10-21 23:49:34 -05:00
rocko 0fa7bf421f fix: place processing controls within the Console navigation box 2026-10-21 22:42:13 -05:00
rocko 93d211c295 fix: allocate Console viewport space to processing controls 2026-10-21 22:42:13 -05:00
rocko 08c896e3c5 docs: record processing gate failures and timer fixture correction 2026-10-21 22:42:12 -05:00
rocko 03ed2bccce test: attribute processing timer to its owning script 2026-10-21 22:42:12 -05:00
rocko ad6020754d fix: stop processing controls on Console sign-out and preserve layout 2026-10-21 22:42:12 -05:00
rocko 5c8f983dde test: initialize processing control in Discord CLI fixtures 2026-10-21 22:42:12 -05:00
rocko f1267e49af fix: account for processing controls in image inputs and launch fixtures 2026-10-21 22:42:12 -05:00
rocko d2be97c5fc test: use private tester socket for processing controls 2026-10-21 22:42:12 -05:00
rocko c13c68c813 test: strengthen processing cancellation and refusal guards
Bound the halt-abort expectation, check Discord refusal before malformed journal reads, and isolate HTTP origin and valid oversized-body guards. These test changes are committed for the row 191 gate; execution awaits the coordinated slot.
2026-10-21 22:42:12 -05:00
rocko 4bdc7e0e86 processing: inspectable per-run stop seam for lifecycle fire
Unify halt and fire cancellation by verified run identity with durable first-owner receipts and shared deadlines. Integrate the instance control with the current host, library and tester-socket paths. Focused nine-file evidence is 62 passing tests; broad suites, mutants and review remain pending.
2026-10-21 22:42:12 -05:00
jason.woltje 86f95bb27f queue: revs 3109-3113 (darkwing: row 273 in-review round 1 and review request; filbert: row 273 APPROVE recorded (c30308); sage: row 273 landing note and done after 0bbf0f6d1 (#1778)) 2026-10-21 22:41:05 -05:00
jason.woltje 5365adaef6 docs: row 273 TOOLS.md activate usage and F39 rules (Filbert N1), BUILD-LOG for rows 202 and 273
TOOLS.md: activate takes --no-sso and --send; three lines state the F39 rules.
BUILD-LOG: the row 202 landing line held since 02:01Z and the row 273 landing line.
2026-10-21 22:37:16 -05:00
jason.woltje 0bbf0f6d18 row 273: T53 test 6 pins the usage line with --no-sso
The usage line gained --no-sso in the proof and sso bracket. T53 test 6
still matched the old bracket and failed the whole run on 978143bf4.
2026-10-21 22:15:36 -05:00
jason.woltje 95f4bd8fdc row 273 notes: name the changed T57 item 3 assertion for review (sage-2321) 2026-10-21 22:15:36 -05:00
jason.woltje d8034c51bc stack-release: a rollback that drops --sso drops --send; the --no-sso walk refuses an ok line with no activation (row 273, sage-2315)
B1: row 200's rollback to a Console without --sso kept the pointer's --send,
which F39 b forbids once --send can sit beside --sso. send_for_target drops it
whenever the run drops the pointer's --sso, and the plan and steps say so.
N1: an ok line with no activation refuses sso-off-flags instead of being
skipped.
2026-10-21 22:15:36 -05:00
jason.woltje b28baf7ee8 stack-release tests: T58 adds sage-2305 cases a and b (row 273)
(a) --sso on, then two re-pins that carry it, then --no-sso gives back the
arrival's ExecStart byte for byte. (b) every set in the log holds --sso, so
--no-sso refuses sso-off-flags before any unit stops.
2026-10-21 22:15:36 -05:00
jason.woltje 3673463182 stack-release: --no-sso drops --send in one place, send_for_target (row 273) 2026-10-21 22:15:36 -05:00
jason.woltje 7991662dc0 stack-release: activate --send and --no-sso (row 273, #1778)
activate --send turns send on only when the run's Console flags hold
--sso, given or carried (F39 b), and puts it after --sso and before the
tester flags. activate --no-sso turns --sso off with the Console flags
from the set of the activation that turned it on, drops --send (F39 a)
and keeps the pointer's tester. Both are usage errors under --units
external. T58 covers the brief's T1-T4; the external test covers T5.
Design and the set-walk choice: agents/darkwing/work/queue-273/BUILD.md.
2026-10-21 22:15:36 -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
830 changed files with 86030 additions and 32969 deletions

No files matched your search

+118 -4
View File
@@ -5190,10 +5190,124 @@ Before: managed project/thread verbs had no Console conversation area. After: fi
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-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-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-21 row253 implementation prepared, gate pending
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.
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-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-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.
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-20 18:51:10 UTC | Row 191 #1689 design phase | Before: per-business launch revoke and per-binding Discord STOP leave task containers, active sessions and background work without one switch. Proposed per-data-root durable halt with human-only resume, generation-fenced starts, bounded concurrent termination, write-once stop receipts and existing event mirror; no schema migration. DESIGN.md inventories starter/continuation paths and container control-mount restriction. Sent to Dewey with Sage copied; implementation waits independent design approval. No tests, commits or live actions.
2026-10-20 18:57:51 UTC | Row 191 design revision | Dewey r1 changes: interim Console --send absence, reserved processing events and missing-state semantics required correction. DESIGN-r2.md now names CLI interim path, unavailable UI controls, trusted initialization and fail-closed missing leaf, container delete/replace mount tests, reservations and id-bound reconciliation. Small notes addressed including diagnostic seats and aliases. Recommended smaller Docker scope to Sage; design and scope approval pending, no code or test runs.
2026-10-20T19:50:56Z | Rocko | row 191 #1689 implementation stage: from independent brakes to shared root state, generation-fenced launcher and tracked stops, Discord/task guards and control HTTP/UI. Evidence is agents/rocko/work/queue-191/logs/ and PROGRESS.md. Fixture single files only so far; heavy verification and candidate not complete. Scope additions sage-1367 (one read-only CODE file) and sage-1392 (processing-only client socket timeout); no schema, proof baseline, live file/unit, commit or push change.
2026-10-20 22:58:45 UTC | Rocko | row 191 #1689 rebase inspection candidate | Rebuilt uncommitted kill-switch tree from da230ca48 onto landed row 190 base 00d4cc1f6 under new branch rocko/row191-r2-00d4. Hand merges retained instance flags and SSO/sign-out changes; fixture tests exposed and fixed SSO signer compatibility and two unknown-runtime cleanup defects. Final touched singles 249/249 across 26 files; 15 overlapping Client reader files 102/102. Manifest a4f780c2 has 78 paths; packet 185 files verified with scan clear. Mutants and owner gate remain pending; no review-ready or activation claim, no commit/push/live action.
2026-10-21 23:39:21 UTC | Rocko | row 191 #1689 inspection snapshot | Sage-2242 authorized a local inspection commit for Honker's row 194 S6. Created d52b4db1c on new rocko/row191-inspection-20261021, parent 00d4cc1f6, preserving the reviewed-design build and candidate a4f780c2 (78 paths). Static manifest/index/source scan and diff check verified; no tests run, no push credential or push attempt. Existing single-file evidence is historical, 249/249 and overlapping Client readers 102/102; suites/mutants/gate remain pending. Current code has verified runtime signalling and instance halt, but no dedicated per-run stop API yet; Honker and Sage notified of that seam directly. Snapshot is not review-ready or approved for live use.
2026-10-22 00:04:05 UTC | Rocko | row 191 #1689 per-run stop inspection | Implemented agreed Honker-0079/0081 stopRun contract with verified per-signal identity, fixed 5s+2s, durable first-owner join and immutable result plus later-exit observations. Focused CLEAR sage-2250: final 9 files 62/62, no skipped, after correcting host guard placement and peer-discovery fixture timeout isolation; initial failures and failed teardown logs retained, only own failed group stopped. Slot returned with cwd sweep clear. Sage published f7bf7dfec; sage-2265 clean rebase creates 7aa18b0f6 on dcd8d4dda under new branch, explicit Rocko identity, all 79 manifest product blobs identical so 62/62 evidence carried. Direct S6 handoff sent to Honker and Sage. Wider suites, mutants, review and activation remain pending.
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-22 00:24:34 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 full gate on c78f48946 | stopped on second integration failure per sage-2275; singles 269/269, processing-control 61/61, bus 646/646; CLI interrupted with 26 observed failures; no further edits; cwd sweep empty and slot returned to Sage.
2026-10-22 00:48:20 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 eb3abba12 full gate | stopped at Discord 190/192; 32 singles 304/304, processing-control 61/61, bus 646/646, cli 761/761; two uninitialized Discord fixtures reported to Sage; no later run; cwd sweep empty, slot clear.
2026-10-22 00:58:27 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2294 fixture fix | e6800380d on eb3abba12, two test files/four additions, assertions preserved; 338-entry static all-package sweep recorded; no tests; sent Sage for publication and Honker for dependent gate.
2026-10-22 01:30:04 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 e6800380d resume | binding15/15, recover5/5, Discord192/192; stopped WebUI314 passed/4 failed/1 esbuild skip; processing polling violates sign-out/manual redirect and bar changes layout; no fixes/retries, cwd sweep empty, slot returned.
2026-10-22 01:38:08 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2317 Console integration fix | 8e1f55c1d on e6800380d; shared sign-out/manual redirect/interval and desktop frame fixes, assertions retained, six browser cases, 73 mutant sites preflight; syntax only, no tests; sent Sage/Honker.
2026-10-22 02:06:38 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 rebase sage-2329 | eafcbd3e7 on ddfb29af7, new branch, dispatcher retains processing and access; CLI/readme auto-merges checked; 87-path fd0f2740 manifest and 73-site preflight; Sage2333 published, awaiting CLEAR; no tests.
2026-10-22 02:20:51 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 eafcbd3e7 full gate | stopped singles313/6: six new browser cases count app freshness timer as processing timer; diagnosis sent Sage, no fix/retry; no suites/shells/mutants; cwd sweep empty, slot clear.
2026-10-22 02:23:13 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2345 timer fixture | 4db8b5778 on eafcbd3e7, one line attributes timer to processing.js; all assertions retained, no product change or tests; sent Sage and Honker; awaiting publication/CLEAR.
2026-10-22 02:25:29 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2349 record commit | 0f8235277 on 4db8b5778 adds packet BUILD.md cause record; product manifest remains ffb46fbd/87 paths; no amend, product change or tests; Sage/Honker notified.
2026-10-22 02:48:36 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 0f8235277 full gate second red | singles330/330, processing61/61, bus646/646, cli783/783, discord192/192; WebUI323/1/1 host esbuild skip; shell geometry1062vs1000; stopped/no retry, no later suites/shells/mutants; cwd sweep empty, slot returned, Sage escalates Mos.
2026-10-22 02:57:36 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 mos-1248 desktop layout restart | FF 5cd32f4c1 on 0f8235277: desktop grid allocates processing bar, existing shell assertions kept, visibility/no-scroll assertions added; 74 static mutant sites; candidate 2f93ef19; sent Webby/Sage for static review; no runs, awaiting review and CLEAR.
2026-10-22 03:11:09 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 static nav layout, mos-1249/1250 | FF da4334806 on 5cd32f4c1; controls first in nav, non-sticky per sage-2386/webby-0108; candidate-only placement assertion changed, landed shell assertions preserved; 77 mutants statically preflighted; manifest 245dceb1; Webby/Sage handoff sent, no runs, awaiting static look/publish/debug CLEAR.
2026-10-22 03:15:11 UTC | Rocko (d99326cc) | row 191 sage-2394 debug | da4334806 shell5/5 and processing-browser9/9, awk14/0/0; geometry heights reported with read-only distinction; cwd sweep[] and slot returned; full window awaits separate CLEAR.
2026-10-22 02:01:30 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 202 #1700 FED R5 landing | ddfb29af7 on 9c9732983: 6 manifest paths from a74cd9f87 (digests match CANDIDATE.sha256, patch-id 9d89077b equal); Duckworth APPROVE r5 rev 3026; C1 gate by Quackerjack on merge 3b86180b9: cli 771/771 skip 0 (run 2, run 1 red on gate TMPDIR length), 10 test-*.sh rc 0, M1-M13 killed; done rev 3108; landing note refused for length before the done move, so the record is the commit body and #1700 c30265; 191 and 194 rebased onto it (one scripts/mosaic case line)
2026-10-22 03:35:55 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 273 #1778 activate --send/--no-sso landing | 0bbf0f6d1 landed on 378a76b85, a rebase of cac7d0f67 (0042f713d) with all 6 range-diff pairs = and equal patch-ids; 4-path manifest e1031023 unchanged; Filbert APPROVE rev 3111 (c30308) carried merge-scoped under mos-1160, carry comment c30319 names 0bbf0f6d1. Gate on 0bbf0f6d1 by Darkwing: stack-release.test.mjs 218/218 skip 0, stack-release-external.test.mjs 25/25 skip 0, test-release.sh rc 0 (selftest 14/0, no skips), T58 controls c0 11/11 pass and c1 11/11 fail, M1-M7 killed. Rerun because row 202 changed scripts/mosaic, which stack-release.test.mjs copies. Red 1 was 978143bf4 (T53 test 6 usage pin). This doc commit adds the TOOLS.md usage line and F39 rules (Filbert N1).
2026-10-22 04:02:12 UTC | Rocko (d99326cc) | row191 full window0fa7bf421 | touched35files336pass, packages2403pass/1esbuildskip, shells425selftests+156queue node pass; SR01–07killed, SR08timeout,69unrun; stopped/no retry;88blobs restored,cwd sweep[],owned network removed,slot returned Sage; await Mos ruling.
2026-10-22 04:14:56 UTC | Rocko (d99326cc) | row191 control/resume sage-2427 | control12/12;SR08separate timeoutkill;49resumed assertionkills thenKS43survived;total56assertionkills+1timeout,1survivor,19unrun;88blobs restored,cwd sweep[],slot returned,Sage/Mos informed;no retry/edit.
2026-10-22 04:21:30 UTC | Rocko (d99326cc) | row191 KS43+19 retarget run | KS43assertionkill thenKS44invalid mutation syntax;57assertionkills+1timeout,1invalid,18unrun;stopped,no edit/retry;88hashes restored,cwd sweep[],slot returned,Sage/Mos notified.
2026-10-22 04:30:06 UTC | Rocko (d99326cc) | row191 KS44+18 resume | 10assertionkills thenKS54timeout-without-control despite dependency assertion;total67assertionkills+1timeoutkill,1unqualifiedtimeout,8unrun;88hashes restored,cwd sweep[],slot returned,Sage/Mos notified,no retry/edit.
2026-10-22 04:41:46 UTC | Rocko (Codex gpt-6.1-sol,d99326cc) | row191 #1689 builder complete/in-review | published0fa7bf421,manifest0bcbf337;35singles336pass,9packages2403pass/1knownskip,10shells425selftests+156queue nodepass;75assertionkills+2controlledtimeoutkills=77;88hashrestore,cwd sweep[],slotreturned;in-review rev3114,expected no-token failure3115(exit1);Sage/Dewey handoff,no live action.
2026-10-22 05:03:18 UTC | Rocko (Codex gpt-6.1-sol,d99326cc) | Vikunja502 processing test/runner hardening | brief+isolatedrocko-v502;expected wordingred→green,Node27/27,Python4/4,5/5assertionkills(includingSR08andKS54nohang),0skips/timeouts;sweep[],candidate5hashrestored,slotreturned05:01:29Z;candidate968d6ee3,runnerc5bdc764;packetready,no commit/push/liveaction.
2026-10-22 05:07:34 UTC | Rocko (d99326cc) | Vikunja502 committed/review handoff sage-2482 | newbranchrocko/vikunja502-ce6a-20261022,source8e3c6a79b+packet9bf4683c6 on ce6a96ad6;5paths no base drift,range-diff=,patch-id37135ebe unchanged;manifest968d6ee3,packet93889519 verifies39/39;focused runs carry;Sage publish/Dewey review handoff sent,no push/liveaction.
2026-10-22 05:10:00 UTC | Sage (1ef1e4f8) | serial landing 205 then 268 | 205: 3 commits, 4 paths, digest 858463b1, every scripts/test-*.sh green on ce6a96ad6, Duckworth N1-N4 carried (N1/N3 to 205b or R3, N2 to the 193b2 brief, N4 #1782 before any user thread goes live). 268 r4: c27207f6e on 205, 9 paths, digest caa0fa4e, patch-id 2942acc8 equal to r3. Overlap checks clean before both pushes. No live action; LIVE stays 86f95bb27.
2026-10-22 05:58:59 UTC | Rocko | row 279 #1784: before, deadline overshoot skipped final observation and returned unknown; after, one-line for (;;) retains final mapping and unchanged unreachable fallback. Regression red as specified; whole stop-run 13/13 and processing-control 62/62; M1 assertion kill, no skips, restored and swept. Source 1016b8584, packet cad927137; manifest 312ed511d8f1cff826ee2fd547de8368923130c7bfe06236d8e29d8655b80aaa. Pending Sage publication and Dewey review; no live action.
2026-10-22 06:15:54 UTC | Sage (1ef1e4f8) | row 279 #1784 landed (mos-1269) | Before: stopRun returned unknown for a live run when a sleep overshot the deadline. After: LANDED ceadad993 as a carry of cad927137 onto 343b6dd76 (range-diff 1:= 2:=, patch-ids 6d8ae7f4/e484c297 equal, Dewey carry c30410). Sage gate on cad927137: business 209/0/0, cli 794/0/0, discord processing + processing-stop 9/0/0. Correction: the first discord run wrote to a wrong out path and ran nothing; the rerun is the result. Overlap sweep clean (13, 257). Note rev 3144, done rev 3145. No live action; LIVE stays 86f95bb27.
2026-10-22 06:37:43 UTC | Sage (1ef1e4f8) | row 194 b16 third red (mos-1269/mos-1270) | Gate on ceadad993 + 9cdc47d04: business 228/0, cli 826/0 (T9a passes), control-board 124/0, bus 705/1 at grant-executor.test.mjs G13 (timed out 10044 ms); conversation, webui and scripts/test-*.sh never ran. runb8 green 436/0/0, 17/17 mutants. Correction to sage-2524 (mos-1270): the stuck wait is the first until at :733, the holding file after decision.resolve, so the executor never started the install and hosts.revoke at :734 never ran; it is not a revoke-path hang. Ruling: a diagnosis run, 30 base + 30 candidate alternating under cli load, no gate retry.
2026-10-22 07:51:04 UTC | Rocko | row 280 #1785: before, any TCP connect could accept a port our sshd failed to bind; after, attempt-local exact log proof plus liveness and connect, non-ephemeral picker, bounded scratch remote sleep cleanup. Both occupied regressions red on old acceptance; CRLF build failure captured and fixed under sage-2535. Committed source b765f5cb8, packet d6c924764, manifest27806c0579c76d0999d2e6be4180c3867570df0268af9d7e0d7b971af799e6f5. Repeats07:29:03Z–07:48:38Z grant1230/1230,host780/780,CLI1588/1588; final M1/M2 four assertion kills,0skips/timeouts,sweepclear. Sage publishes/gates; Duckworth reviews.
2026-10-22 08:00:02 UTC | Sage (1ef1e4f8) | row 280 #1785 landed | before: the bus tests' scratch sshd helper returned a port on a connect alone, and 194 b16's gate hung at G13 (base hung too, honker-0127). After: b0fd28b6f on refactor, a ff from da5d3651d (b765f5cb8 + Rocko slim packet; full evidence d6c924764 on rocko/row280-da5d-20261022). Duckworth approve rev 3153 (manifest 27806c05). Sage gate bus 656/0/0, ge 41/0/0, hr 26/0/0 (c30431). Rocko repeat 2010/2010 under cli 1588/1588, no timeout; M1/M2 killed 4/4. Two build-time slips recorded in Rocko's packet: a runner root path (no test ran) and the CRLF sshd log line. Overlap 13/257 clean. Note rev 3154, done 3155, #1785 closed c30432, P19 511 comment 623. 194 b17 rebases on b0fd28b6f (sage-2542).
2026-10-22 08:38:11 UTC | Sage (1ef1e4f8) | row 194 #1692 landed | before: 194 b16 9cdc47d04 went red three times (stopRun deadline, then the bus test sshd port race), fixed by rows 279 and 280. After: fd6dd327c on refactor, a ff on e122aad8e. b18 is b17 3f451772d rebased after my doc and queue push blocked a ff (16 =, series patch-id 64303253f8e6, packages equal b17). Webby approve rev 3160 on manifest 29f131ab. Honker gate on fd6dd327c: runb8 503/0/0, mutants 17/17; 18 packages 3668/0, ten scripts rc=0, Docker up, env skips named. The secret scan hit three fake_*_token fixtures in bus tests (false positive). Note 3161, done 3162; #1692 closed c30435; P19 414 comment 625. LIVE still 86f95bb27.
2026-10-22 08:42:04 UTC | Honker (Claude Opus 5.5, ecd17786) | row 194 #1692 agent lifecycle build | before: no Console or CLI lifecycle for agents (create, launch, terminate, regenerate, delete) and no per-agent hire/fire veto. After: 16 commits landed as fd6dd327c (b18, a rebase of b17 3f451772d on e122aad8e: 16 =, series patch-id 64303253f8e6, packages equal b17). Manifest 53 paths, digest 29f131ab. Final run on fd6dd327c: runb8 39 files whole 503/0/0, mutants 17/17 killed, restores matched; gate.sh merge onto e122aad8e, 18 packages 3668/0, ten scripts rc=0, Docker up, four env skips named (webui esbuild pin, queue render outside canonical root, test-task live provider x2); sweep clean. Corrections: b16 went red at G13; my diagnosis (60 runs, base hang on port 38927 sshd bind EADDRINUSE) placed it outside 194, fixed by row 280; the g13diag load kill missed its process group and overran about 8 minutes; one runb8 launch failed rc=126 on a lost exec bit before any test ran; the b17 runb8 was stopped unscored when refactor moved. In-review revs 3156/3157, Webby approve rev 3160.
2026-10-22 09:43:34 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 236 #1742 Gitea grant landing, and row 281 filing | 236 landed bd5f11ded at 09:37:31Z, a fast-forward on be2b008cc: 08840f3c1 rebased past 194 (commit 1 unions only, 2-7 =), e40b74beb for two stale out-of-manifest pins, bd5f11ded for a gitea-not-configured raise test. Dewey approve rev 3168 on manifest 0b00269b (34 paths). Megavolt gate under my slot: 2c bus 131, cli 224, route 28, docker 1; whole bus 765, business 228, cli 832, ten scripts rc=0; mutants 40/40; skips named, env only. Overlap with 13 and 257 clean. Note 3169, done 3170; #1742 closed c30451; P19 436 comment 639. Correction: the first whole run on ce74477e1 went red 762/764 on the two stale pins; that run is void. Follow-up per mos-1271: row 281 (#1786), a fire or delete revokes host and Gitea grants, Megavolt owns, Dewey reviews; brief docs/plans/2026-10-22_fire-revokes-grants.md; DEFERRED entry points at it.
2026-10-22 10:30:51 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 274 #1779 FED R3 broker user principal landing | 274 landed 4c485c63b at 10:28:26Z, a fast-forward on 47a35bf43: six Morgana commits d20c92bf6..4c485c63b. r5 f58cfc361 moved covers, LEVELS and the all marker into business/src/vocabulary.mjs so the broker keeps its sandbox import closure (M16/M16b/M16c). r6 4c485c63b pins USER_VERBS as the ruled seven and widens the R3 off-list set. Dewey approve rev 3178 on manifest 00047b52 (13 paths), comment 30487. Morgana gate under my slot, 10:09-10:22Z: 23 touched files 0 fail, bus 787, harness 151, business 228, cli 832, seat 167, ten scripts rc=0, mutants 22/22; skips named, env only (test-queue 1, test-task 2). Overlap with 13 and 257 clean. Note 3179, done 3180; #1779 closed c30491; P19 414 comment 646. Corrections: the r5 gate on f58cfc361 went red (M3x/M6x survived the R3 off-list set), void; my gate comment 30490 said 24 touched files, the count is 23; the kit comment guessed human-required for the M3x wall, the run code is not-holder.
2026-10-22 10:57:34 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 256 #1761 the human deletes a creative instance, landing | before: row 194's deleteRefusal refused every END_PROTECTED definition for every actor, so nobody could delete a creative instance; the human could only delist one with launch: false. After: 256 landed f12441d4f, a fast-forward on 0165be266 (the 274 landing plus its docs and queue commits): four Honker commits b5f1e09d6..f12441d4f, patch-id b28e63746f8c. One protectedRefusal predicate serves delete and the listing's per-viewer endProtected; any non-human actor gets organization-creative-human-only, fire unchanged. Rebased twice (past 236, then past 274), each range-diff 4/4 =, carries checked by Webby (webby-0126, webby-0128). Webby APPROVE on manifest-r3 01eb75aa (10 paths), comment 30519, posted by Sage as jarvis because Webby has no token; review request c30518 (Sage, revs 3183/3184) after Honker's request failed (no token, revs 3181/3182). Gate by Honker under Sage's slot 10:33Z-10:52Z on merge tree ced6bf5f8: touched files 31/0/0, 18 packages 3757/0 (bus 788, business 231, cli 832, webui 335), ten scripts/test-*.sh rc=0, mutants M1, M2, M3, M4a, M4b killed with blob-checked restores. Skips, named and environment-only: webui editor rebuild byte check (esbuild 0.28.1 installed, pin 0.28.2), test-queue non-canonical root, test-task live provider x2. Overlap with in-review rows 13 and 257 clean. Order change sage-2594: 256 landed before 277 because 277's gate had not run. LIVE is still 86f95bb27. Queue note rev 3186, done rev 3187; #1761 closed c30520; P19 416 comment 649.
2026-10-22 11:19:28 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 277 #1782 the owner sees a thread agent's claims, landing | before: a thread run's role.claim wrote thread_claims only, and the agents verb read role_claims only, so the owner had no way to see a thread agent's claim (PRD :1083-1084). After: 277 landed 62f53b1e0, a fast-forward on 46dedf925 (the 256 landing plus its docs and queue commits): seven Morgana commits 1d5d00939..62f53b1e0, range patch-id 7f0533520bd2. A bus verb thread.claims serves human and reader sessions; mosaic agents adds thread lines without --tester or --json, --json keeps the agents array, the tester path makes one call, a refused thread.claims prints the role lines and the refusal code, and an error with no bus code still throws. Rebased onto 0165be266, then onto 46dedf925 after 256 landed first (sage-2594); both range-diffs all =, manifest e281aa2c unchanged (duckworth-0126). Duckworth APPROVE comment 30539, recorded rev 3192, posted by Sage as jarvis; review request c30538 (revs 3190/3191) after Morgana's request failed (no token, revs 3188/3189). Gate by Morgana under Sage's slot 11:01:23Z-11:13:56Z: 7 touched files 0 fail (thread-claims 5, message-filter 6, user-principal 22, format 4, tester 11, other-tree 4, host 43), bus 793, cli 836, harness 151, business 231, seat 167, ten scripts/test-*.sh rc=0, M1-M9 killed on committed bytes. Skips named, environment only: test-queue non-canonical root, test-task live provider x2. Overlap with in-review 13 and 257 clean. Duckworth N1 (M8 dies on a rethrown error) to DEFERRED; 0123 N3 per-session claims binds 264a r7. Note rev 3193, done rev 3194; #1782 closed c30540; P19 506 comment 652. Also landed as doc-only picks: Honker's 256 BUILD.md gate results (80620a3a0 as b83173305) and Megavolt's 236 BUILD.md runs (9c6e1a165 as 01ea1fb11). LIVE is still 86f95bb27.
2026-10-22T11:37:16Z | Gander row278 #1783 implementation prepared, verification held | Before: owner thread claims were visible through row277 but absent from the Console Agents view. After: prepared a separate four-field table, a Console-owned read adapter, section-local refusal handling and an explicit tester refusal before busRead. Ten tests and six unique check definitions are prepared but unrun. Base moved from980cec88c to6af85df4b under sage-2625 on new gander/row278-b; no candidate path overlapped. No Node process, syntax check, test, browser, queue command, push helper or live action ran. Next: Sage CLEAR for the queue claim and gate; Webby reviews the verified manifest.
2026-10-22T12:09:45.285844+00:00 | Gander row278 run1 void, loader fake repaired | First new-file run stopped3/1/0 because the fake omitted BusReadError imported by base chat.mjs. Sage-2638 places the first-own-test red and permits the assertion-preserving export; test-only commit964b6b184. Runtime bytes unchanged. All run1 results void, no timeout or run leftovers. Run2 must restart every step after busy CLEAR; a second red goes to Sage then Mos before any change. No product acceptance or push claimed.
### 2026-10-22 | Gander | row278 gate run3
Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version skip), all ten shell files425/0, touched files15/0, M1-M6 killed6/6. No timeout; sweep0 run-created leftovers. Runs1/2 void. Seven-path manifest8fa859a0 goes to Webby for final review. No landing or live change.
2026-10-22 11:29:09 UTC | Rocko | V502 carry/rerun on980cec88c: source407c8d7be and packet04a86cf33 match approved8e3c6a79b/9bf4683c6 patch-ids, no conflicts. Dewey0834 carry conditioned on rerun, satisfied under sage2616: stop-run13,processingHTTP6,bustasks9 allgreen (Node28/28),Python4/4,0skips/timeouts. Manifest5758d699,source5/5restored,cwdsweep[]. Doc rerun packet6af85df4b explicitly pushed to rocko/vikunja502-980c-20261022; no live action.
2026-10-22 11:30:17 UTC | Sage | Vikunja 502 (row 191 follow-up, no queue row) LANDED 6af85df4b, a fast-forward from 980cec88c. Before: the processing failure diagnostics and cleanup in stop-run, processing HTTP and bus tasks tests were Dewey-approved on the pre-194 base only (8e3c6a79b/9bf4683c6). After: Rocko's picks 407c8d7be/04a86cf33 carry the same patch-ids (37135ebe/0dbcda54), plus a doc-only rerun record 6af85df4b. The rerun on the new base was green: stop-run 13/13, processing HTTP 6/6, bus tasks 9/9, Python 4/4, 0 skips. Dewey carried the approve (dewey-0836). mosaic-task.mjs is unchanged, so N7 (runs suite) didn't trigger. Overlap with in-review rows 13 and 257 was clean. LIVE is still 86f95bb27.
2026-10-22 13:10 UTC | Sage | Rows 278 and 281 LANDED. Before: refactor at 6af85df4b (V502). Row 278 (#1783, Gander, Webby): the Console Agents view shows thread claims in their own section. It landed as 385a7194c at 12:39Z, ff, 9 commits, manifest 8fa859a0. Run 3 was green: bus 793, webui 345/0/1, ten shells 425/0 under the 3x limits (mos-1275), M1-M6 killed. Runs 1 and 2 were void, on fixture fixes 964b6b184 (placed by Sage) and 9696d1fcb (ruled by Mos, mos-1275). Webby approved (c30588, rev 3200). Done rev 3202, #1783 closed c30589. Row 281 (#1786, Megavolt, Dewey): a fire or a delete revokes the instance's host and Gitea grants. It landed as e479521d3 at 13:06Z, ff, 17 commits, manifest b73d4c81. Slot on 7979fb41e: bus 819, business 231, cli 837, dewey-bus 76 with the Docker gitea-grant test, shells green, M1-M22 killed. The M1 hang was fixed at its cause in b4a (mos-1273). Dewey approved (c30614, rev 3207). Done rev 3209, #1786 closed c30621. Skips on both: test-task's row 244 live-provider pair and test-queue's canonical-root check, plus webui's esbuild rebuild test on 278. After: refactor at e479521d3. DEFERRED moves the grant gap to Done and opens Dewey's N1/N2 text gaps. LIVE is still 86f95bb27.
2026-10-22 14:18:23 UTC | Sage | Row 264a LANDED (#1769, Gizmoduck, Duckworth): sessions per thread, schema 3k-3l. Before: refactor at e479521d3 (row 281). It landed as 84f53540d, ff, 21 commits, manifest 257408e9 (Duckworth approve rev 3214, comment 30640). Gate r13 on 2cc34aef8: 20 touched files, bus 827, cli 837, webui 345/1 named skip, ten shells under 3x limits, mutants M1-M4 killed on assertions. Corrections on the way: the kit stopped on first red, so four reds surfaced one at a time. r10 later3l missed a trigger (mos-1276). T2 had failed since r2 because refusals log action.refused (mos-1277, my sage-2257 wording). threads.test:341 kept the old trigger text (mos-1278). M2 was caught by the store trigger before any assertion, so it now drops the trigger too (mos-1279, four files with the SCHEMA_SHA256 pin). Deferred: actor_run pin binds row 269, esbuild skip to a later gate, session-scoped user reply binds row 272. No live change; LIVE is still 86f95bb27.
+1
View File
@@ -22,6 +22,7 @@ RUN npm ci --ignore-scripts
# runtime adapters.
COPY contracts /opt/mosaic/contracts
COPY src /opt/mosaic/src
COPY packages/processing-control/src/state.mjs packages/processing-control/src/entrypoint.mjs /opt/mosaic/processing-control/
COPY adapters /opt/mosaic/adapters
RUN chmod 0555 /opt/mosaic/contracts /opt/mosaic/contracts/* \
&& chmod 0555 /opt/mosaic/src /opt/mosaic/src/*.sh \
+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.
+113
View File
@@ -0,0 +1,113 @@
# Row 273 (#1778): activate --send and --no-sso
Darkwing, 2026-10-22. Brief: `docs/plans/2026-10-22_activate-send-nosso.md` (blob in f0d0b31e9), approved by
Mos in mos-1246 (F39). Base: origin/refactor 0042f713d. Reviewer: Filbert. Gate owner: Sage.
## What changed
Only `scripts/stack-release.sh` and its two node test files.
- `activate --send` puts `--send` into the run's Console flags after `--sso` and before the tester
flags, where webui-service.sh renders it. It refuses `send-needs-sso` unless those flags hold `--sso`,
given or carried. The plan line is `send: on; --send was given`, or `send: carried` when the pointer
already holds it. `check_target` lets it name the pointer's copy only when the pointer holds no `--send`,
so a second `--send` on the same copy refuses `already-active`.
- `activate --no-sso` refuses `sso-not-on` when the pointer holds no `--sso`. Otherwise it takes the
Console flags from a rollback set (below) and keeps the pointer's tester flags. `send_for_target`
drops `--send`, whether it came from the set or the pointer. It is the only place that does, so one
mutant there (M3) shows the drop is tested. The plan prints `sso: off; Console flags from <set>/<file>; restores <flags>`
and a send line, `send: dropped; --no-sso drops --send (F39)` or `send: off; --no-sso was given and the
pointer holds no --send`. The steps log gets `drop --send; --no-sso was given` and
`turn off --sso; the Console flags come from <set>/<file>`.
- Usage errors: `--send` with `--no-send`; `--no-sso` with `--sso`, `--send`, a proof pair or
`--no-proof`; either flag on any verb but activate; either flag under `--units external`.
`--no-sso --no-send` is allowed, since the brief doesn't forbid it and both drop the same flag.
- The set read that `sso_base` did inline for a rollback to a Console without `--sso` moved into
`set_flags`, which both paths call. Its three messages are unchanged, and T57 still pins them.
- `from_log` has a new mode, `sets`: the activation of each ok activate and rollback line, newest first,
stopping at the adopt line, which took no rollback set.
## The consequential choice: which set --no-sso reads
The brief says the flags come from "the rollback set of the activation that turned `--sso` on", and that
"a set whose Console already ran with `--sso`" refuses. Each set holds the Console unit from before its
run. So the activation that turned `--sso` on is the newest one whose set holds a Console without
`--sso`: every activation after it ran with `--sso`, and so did every set it left.
`no_sso_base` walks the log's sets newest first and stops at the first set whose Console has no `--sso`.
It refuses `sso-off-flags` at the first set it can't read (missing, failing its SHA256SUMS, or with no
argv the script can parse), since it can't tell whether that set was the one. If every set ran with
`--sso`, it refuses with `the Console in <set>/<file> already ran with --sso, and the log names no
earlier activation`. A log with no set at all refuses with `the log names no activation with a rollback
set`.
The tradeoff: a later same-copy re-pin that carried `--sso` (a `--no-send`, or a re-pin onto a newer
copy) doesn't break `--no-sso`, which the phase 2 window needs if a re-pin lands between step 5 and a
rollback. The cost is that "a set that ran with `--sso` refuses" now applies only when no older set
without `--sso` exists. A tampered set on the walk still refuses, even one newer than the set the walk
would have used. Filbert, please rule on this reading.
## Filbert's early look: B1 and N1 (sage-2315)
Filbert accepted the walk. Sage ruled both findings into this row.
- B1: row 200's rollback to a copy whose Console has no `--sso` took the flags from the set and kept
the pointer's `--send`. From an `--sso --send` pointer, which this row's `--send` can now make, that
gave a Console with `--send` and no `--sso`, which F39 b forbids. `send_for_target` now drops `--send`
whenever the run drops the pointer's `--sso`, so it is still the one place that drops it. The plan
line reads `send: dropped; the run drops --sso, and --send needs it (F39)`, and the steps log gets
`drop --send; the run drops --sso`. Row 200's sso line now names only the tester as following the
pointer.
- Changed existing assertion, for the review to approve by name: T57 item 3 (sage-1389), "the
plain case", ends with `assert.deepEqual(pointer(g).console,[...noSend(gbase),...pair(f)])`. It
used to expect `'--send'` at the end. The case seeds a legacy `--send` pointer, turns `--sso` on
(which carries `--send`), then rolls back to B, whose Console has no `--sso`. A carried `--send`
that ends without `--sso` breaks F39 b whatever its source, so the old pin asserted the wrong
outcome (Sage accepted this in sage-2321). Row 200's two sso plan strings also lose "--send and"
(`ROLLBACK_PLAN` and the round 3 named-instance case), since `--send` no longer follows the pointer
on that path.
- N1: the walk skipped an ok line with no activation. Skipping it could land on an older set that
isn't the right one, so it now refuses `sso-off-flags` with `an ok line in the log names no
activation`. The walk reads the log through `mapfile`. That gives no entry for an empty log, so a
log with no set still gets its own message.
## test-release.sh
The window plan says test-release.sh "gets cases for both". The brief lists it only as a file to run
whole. test-release.sh is release.sh's Docker selftest and doesn't call stack-release.sh, so the cases
live in the node tests and test-release.sh runs unchanged.
## Tests (T58 in stack-release.test.mjs)
- T1: `--sso --send` from B onto C1 with a tester port: plan, pointer, unit and steps. `--send` beside a
carried `--sso` re-pins C1 once; a second `--send` refuses `already-active`; C carries it.
- T2: `--send` without `--sso` refuses on another copy and on the pointer's copy, also when the pointer
carries `--send` alone; `--plan` lists the refusal.
- T2 (sage-2315 B1): `--sso --send` onto C1, then a rollback to B, whose Console has no `--sso`. The
pointer and the unit end with neither flag, and the plan and steps name the drop.
- T2 and T4 usage: every forbidden pairing and verb, checked against the full usage text.
- T3: from an `--sso --send` pointer, `--no-sso` on the same copy gives back the pre-`--sso` ExecStart
line byte for byte, without `--send`. A second case walks past a `--no-send` re-pin that ran with
`--sso` and keeps the pointer's tester port (7334), which differs from the set's (7333). A third
(sage-2305 a) turns `--sso` on, then re-pins twice with `--send` and `--no-send`, so the two newest
sets both hold `--sso`. `--no-sso` then gives back the arrival's ExecStart line byte for byte.
- T4 (sage-2305 b): two sets, both edited or written to hold `--sso` with sums intact, refuse
`sso-off-flags` through `refused()`.
- T4: `sso-not-on`; an ok line with no activation; a set that fails its sums, ran with `--sso`, has no
argv or is missing; a log with no set. Each runs through `refused()`, so nothing stops and nothing else changes.
- T5: stack-release-external.test.mjs adds `--send` and `--no-sso` to the row 232 usage list.
- T55 test 5 and the T57 usage test match the new usage line.
## Mutants
| # | Change | Expected killer |
|---|---|---|
| M1 | `set_flags` skips the SHA256SUMS comparison when `--no-sso` is given | T4 tampered set |
| M2 | `no_sso_base` skips the `sso-not-on` refusal | T4 `sso-not-on` |
| M3 | the `--no-sso` branch of `send_for_target` keeps `--send` | T3 |
| M4 | `--send` skips the `send-needs-sso` refusal | T2 |
| M5 | parse_opts drops the `--send`/`--no-send` rule | T2 and T4 usage |
| M6 | a rollback to a Console without `--sso` keeps `--send` (B1) | T2 (sage-2315 B1) |
| M7 | the walk skips an ok line with no activation (N1) | T4 no-activation line |
Runs and results go in the round's notes once Sage clears the slot.
@@ -1,711 +0,0 @@
{
"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
@@ -1,197 +0,0 @@
# 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.
@@ -1,9 +0,0 @@
{
"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
@@ -1,147 +0,0 @@
# 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
@@ -1,248 +0,0 @@
# 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
@@ -1,7 +0,0 @@
# 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.
@@ -1,7 +0,0 @@
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
@@ -1,37 +0,0 @@
# 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.
@@ -1,768 +0,0 @@
{
"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
@@ -1,131 +0,0 @@
# 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
@@ -1,38 +0,0 @@
# 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
@@ -1,50 +0,0 @@
{
"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"
}
@@ -1,233 +0,0 @@
[
{
"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"
}
]
@@ -1,461 +0,0 @@
# 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.
@@ -1,10 +0,0 @@
{
"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
@@ -1,44 +0,0 @@
# 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.
@@ -1,30 +0,0 @@
{
"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
@@ -1,684 +0,0 @@
--- 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.
@@ -1,275 +0,0 @@
[
{
"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"
}
]
@@ -1,12 +0,0 @@
{
"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."
}
@@ -1,142 +0,0 @@
// 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);
}`;
}
@@ -1,707 +0,0 @@
#!/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
@@ -1,580 +0,0 @@
#!/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
@@ -1,717 +0,0 @@
// 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"]);
});
@@ -1,610 +0,0 @@
#!/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"
@@ -1,484 +0,0 @@
#!/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"
@@ -1,850 +0,0 @@
// 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");
});
@@ -1,463 +0,0 @@
# 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
@@ -1,76 +0,0 @@
--- 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.
@@ -1,9 +0,0 @@
{
"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"
}
@@ -1,282 +0,0 @@
[
{
"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"
}
]
@@ -1,142 +0,0 @@
// 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);
}`;
}
@@ -1,706 +0,0 @@
#!/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
@@ -1,717 +0,0 @@
// 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"]);
});
@@ -1,609 +0,0 @@
#!/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"
@@ -1,872 +0,0 @@
// 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");
});
@@ -1,133 +0,0 @@
# 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
@@ -1,322 +0,0 @@
[
{
"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"
}
]
@@ -1,69 +0,0 @@
{
"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
}
@@ -1,133 +0,0 @@
✖ 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
@@ -1,7 +0,0 @@
{
"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."
}
@@ -1 +0,0 @@
["node", "--test", "--test-name-pattern", "row253 socket observation uses configured root", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,31 +0,0 @@
✖ 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'
}
@@ -1 +0,0 @@
["node", "--test", "packages/cli/tests/case2-unit.test.mjs"]
@@ -1,63 +0,0 @@
✔ 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'
}
@@ -1 +0,0 @@
["node", "--test", "--test-name-pattern", "row253 factory socket uses actual config", "packages/cli/tests/case2-unit.test.mjs"]
@@ -1,31 +0,0 @@
✖ 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'
}
@@ -1,33 +0,0 @@
{
"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": {}
}
]
}
@@ -1,432 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "packages/cli/tests/case2-unit.test.mjs"]
@@ -1,226 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,198 +0,0 @@
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
@@ -1,34 +0,0 @@
{
"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
}
}
]
}
@@ -1,6 +0,0 @@
{
"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."
}
@@ -1,4 +0,0 @@
{
"scripts/case1-accept.sh": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"scripts/case2-unit.sh": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd"
}
@@ -1,5 +0,0 @@
{
"count": 0,
"processes": [],
"scope": "Own worktree and grant TMPDIR cwd, excluding current ancestors. Both product group-cleanup tests passed."
}
@@ -1 +0,0 @@
/mnt/storage/scratch/tmp/glFUYC2
@@ -1,37 +0,0 @@
# 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 |
@@ -1,6 +0,0 @@
{
"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"
}
@@ -1,3 +0,0 @@
# 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.
@@ -1,4 +0,0 @@
{
"scripts/case1-accept.sh": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"scripts/case2-unit.sh": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd"
}
@@ -1,71 +0,0 @@
{
"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": []
}
}
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 factory refuses retired TCP", "packages/cli/tests/case2-unit.test.mjs"]
@@ -1,16 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 factory refuses retired TCP", "packages/cli/tests/case2-unit.test.mjs"]
@@ -1,37 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 installer refuses unsafe location and query facts", "packages/cli/tests/case2-unit.test.mjs"]
@@ -1,36 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 installer refuses unsafe location and query facts", "packages/cli/tests/case2-unit.test.mjs"]
@@ -1,36 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "a bad args file refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,16 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "a bad args file refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,30 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "a bad args file refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,64 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "a bad args file refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,64 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 acceptance rejects bad config", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,16 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 acceptance rejects bad config", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,64 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 P8 refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,16 +0,0 @@
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
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 P8 refuses", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 P10 refuses unsafe metadata", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,16 +0,0 @@
TAP version 13
# Subtest: row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
ok 1 - row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
---
duration_ms: 6033.541448
type: 'test'
...
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 6163.295312
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 P10 refuses unsafe metadata", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,61 +0,0 @@
TAP version 13
# Subtest: row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
not ok 1 - row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
---
duration_ms: 365.277582
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:235:1'
failureType: 'testCodeFailure'
error: 'parent-mode'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
actual: |-
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-e2c81d87c9ad
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
operator: 'match'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:258: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 456.731571
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 P10 refuses unsafe metadata", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,61 +0,0 @@
TAP version 13
# Subtest: row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
not ok 1 - row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
---
duration_ms: 1114.429559
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:235:1'
failureType: 'testCodeFailure'
error: 'socket-mode'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
actual: |-
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-3dfa29c347db
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
operator: 'match'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:258: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 1218.086888
@@ -1 +0,0 @@
["node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 P10 refuses unsafe metadata", "packages/cli/tests/case1-accept.test.mjs"]
@@ -1,61 +0,0 @@
TAP version 13
# Subtest: row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
not ok 1 - row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure
---
duration_ms: 2723.404154
type: 'test'
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:235:1'
failureType: 'testCodeFailure'
error: 'peer-pid'
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
actual: |-
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-03e5279e0659
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=555 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
operator: 'match'
stack: |-
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/case1-accept.test.mjs:258: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 2817.344607
Loaded 100 of 830 files, more files were not shown because too many files have changed in this diff. Show more