Compare commits

..
Author SHA1 Message Date
jason.woltje b44152ac64 Untested: record row 288 fresh candidate on landed row 283 (#1793) 2026-10-23 15:40:41 -05:00
jason.woltje 65256f94ae Untested: record row 283 follow-up dependency receipts (#1793) 2026-10-23 15:39:31 -05:00
jason.woltje a3bed36116 Untested: clarify existing blob requirement in library delete tool (#1793) 2026-10-23 15:39:31 -05:00
jason.woltje e5a5c259ea Untested: record row 288 T7 verification and landing boundaries (#1793) 2026-10-23 15:39:31 -05:00
jason.woltje d5852440de Untested: merge row 284 reviewer test follow-up for row 288 2026-10-23 15:39:31 -05:00
jason.woltje 94a57ad822 Untested: add library write tools and served blob integration (#1793) 2026-10-23 15:39:31 -05:00
jason.woltje a011cfda90 Untested: migrate creative tool snapshots to eight or ten names (mos-1523) 2026-10-23 15:39:31 -05:00
jason.woltje d64e082ddd Untested: merge complete row 284 prerequisite for row 288 2026-10-23 15:39:30 -05:00
jason.woltje d03c25d58a row284: cover absent supporting prefix directly (unrun) 2026-10-23 15:39:18 -05:00
jason.woltje 682e0f3b6e Untested: record notice-store failure publication behavior (#1793) 2026-10-23 15:39:18 -05:00
jason.woltje a65e288531 row284: supporting lookup scope and served blob IDs (unrun) 2026-10-23 15:39:18 -05:00
jason.woltje e7dd01f25f Untested: retain library notice after push when publication fails (#1793) 2026-10-23 15:39:18 -05:00
jason.woltje 4621f0f703 row284: migrate playground get tests under mos-1330 (unrun) 2026-10-23 15:39:18 -05:00
jason.woltje 2e25b17caf Untested: use editor login and cover row 288 early review gaps (#1793) 2026-10-23 15:39:18 -05:00
jason.woltje 0e61c51f27 row284: merge row224 intact onto row283 prerequisite (unrun) 2026-10-23 15:39:18 -05:00
jason.woltje 8d90b35cc3 Untested: protect library publication state and add broker bare-repo coverage (#1793) 2026-10-23 15:39:18 -05:00
jason.woltje 37b427ad5e Untested: row 288 library writes, notices, revert and human saves (#1793) 2026-10-23 15:39:18 -05:00
jason.woltje f6c473d2a4 Untested: merge row 286 dependency for row 288 2026-10-23 15:39:18 -05:00
jason.woltje 2abb12ce97 Untested: merge row 285 dependency for row 288 2026-10-23 15:39:18 -05:00
jason.woltje 823085e3be Row 286 C2-03b r2: rebase onto 45dca69f6 (sage-3579). The range-diff is all =. Drift check of the paths the tests read is in BUILD.md. Unrun. 2026-10-23 15:39:03 -05:00
jason.woltje 85933fcb09 Row 286 C2-03b: BUILD.md notes for dewey-0869 and sage-2951 (fired-chief path :4308, marker-gated busy test, T9 scratch roles) 2026-10-23 15:39:03 -05:00
jason.woltje 30b912decd Row 286 C2-03b: fold dewey-0869 and sage-2951 (marker-gated busy test, T9 scratch roles with a chief gitea credential, fired-chief path cited) 2026-10-23 15:39:03 -05:00
jason.woltje 452afb0040 Row 286 C2-03b: cite sage-2947 (T7 real verbs, R1 in this row, 285/286 rebase rule) 2026-10-23 15:39:03 -05:00
jason.woltje 22c76f24d5 Row 286 C2-03b: fold dewey-0864 (T7/T9 through grants.revoke and instance.fire, race fetch takes the sync guard, two-chief code, id -u shim note) 2026-10-23 15:39:03 -05:00
jason.woltje 9c84e47c6c Row 286: library.status shows the credential (webby-0151 R3); fired chief revokes its row as row 281 does; unrun 2026-10-23 15:39:03 -05:00
jason.woltje 7aea171cdb Row 286 C2-03b: library push, per-seat credentials, the chief's fetch credential, askpass fd check (#1791, sage-2922/2925/2928, dewey-0860); unrun 2026-10-23 15:39:03 -05:00
jason.woltje f0d265b6f3 Row 286 C2-03b: sage-2928 rulings (sh helper, chief by roles definition, departure noted) 2026-10-23 15:39:03 -05:00
jason.woltje 09961fd98e Row 286 C2-03b: fold dewey-0860 (Q1 sh fd/inode check, Q2 chief by roles definition, N1-N3) 2026-10-23 15:39:03 -05:00
jason.woltje 6ce6d82b3c row 285: BUILD.md records N1, the creative-only library.write refusal as a follow-up, and the round scope (#1790, sage-3593) 2026-10-23 15:39:02 -05:00
jason.woltje 6ac37b99bf Row 286 C2-03b: sage-2925 Q4 ruling (humanCredential takes her login; editor.user is row 283's) 2026-10-23 15:39:02 -05:00
jason.woltje aa1e59d5dd row 285: library.write joins ACTIONS and ROUTED in the business vocabulary and the broker under PRD 1.16 REQ-WAIT-3 and REQ-LIB-6, with a hard-waits test; BUILD.md records the r2 rebase, range-diff and drift check (#1790, sage-3578) 2026-10-23 15:39:02 -05:00
jason.woltje e8b5c8a2a7 Row 286 C2-03b: sage-2922 files-owned extension; Q4 back to Sage (user is the bot fetch identity) 2026-10-23 15:39:02 -05:00
jason.woltje 2705375041 row 285: mos-1333, withdraw the publish fence; N10 option (a) with an assistant instance; a chief with its gitea ref, identity and publish loads; restore the creative publish test; BUILD.md gate scope (sage-2946) (#1790) 2026-10-23 15:39:02 -05:00
jason.woltje db9ca7af5f Row 286 C2-03b: build plan and four scope questions (BUILD.md only) 2026-10-23 15:39:02 -05:00
jason.woltje 319fb1c112 row 285: webby-0178 early read, drop the stale creative publish comment and name the example's test readers (#1790) 2026-10-23 15:39:02 -05:00
jason.woltje a706c01c73 row 285: a chief-of-staff or creative instance that names publish refuses at load, keeping today's N10 refusal; instance-settings :289 follows the companion's gitea (#1790, mos-1332)
The fence sits near business.mjs:166, clear of row 287's :283-291. Its message
starts with the N10 text. An assistant-instance test keeps the no-gitea branch
covered, and a creative-instance test covers the fence. Nothing here has run.
2026-10-23 15:39:02 -05:00
jason.woltje 9c7a8ad141 row 285: fixtures, example and README give the chief and creative instances their gitea reference (#1790, mos-1330)
Fixture data only, plus two named tests that a chief or creative instance
without credentials.gitea refuses at load. chief-of-staff.test :88 and :155
change expected values and wait on Mos's mos-1272 ruling. Nothing here has run.
2026-10-23 15:39:02 -05:00
jason.woltje 71ecbe4ec8 row 285: C2-08b, creative roles hold library.write and the gitea credential, the chief of staff a read-scoped gitea credential; #giteaRole refuses the chief (#1790, mos-1309 item 5, sage-2915, sage-2921)
The creative role files declare gitea with SEAT_SCOPES, the scopes the Gitea
grant mints. The chief declares read scopes only, so #giteaRole refuses it
with gitea-not-declared until row 287 mints its declared scopes.
library.write is not yet routed: it waits on PRD 1.16 (row #1796), and the
fixture change waits on Mos's mos-1272 rulings. Nothing here has run.
2026-10-23 15:39:02 -05:00
jason.woltje 93694c5185 deferred: row 283 Webby N1, SURVEY-RUN.md:96 shows the scopes line without supporting; bound to the next docs commit on that file (webby-0267, #1788); docs only 2026-10-23 15:38:05 -05:00
jason.woltje 8d2856ee3c row 283 (#1788): BUILD.md: G1 per sage-3713: mos-1528 OKs the mapping.test.mjs:52 draft deepEqual gaining supporting: [] (old :48 at 45dca69f6, new :52), recorded as an append that completes BUILD.md:42; doc only 2026-10-23 15:17:26 -05:00
jason.woltje 252b29bd8c row 283 (#1788): BUILD.md: T2 receipts on c18ad3e12 (sage-3695): base 16+6+6+7 library and 21 business green, 14/14 mutants killed on their named tests, library 98/98 and business 231/231; the mos-1272 first-run red and its test-helper fix; doc only 2026-10-23 15:17:26 -05:00
jason.woltje 54d9e07bb7 row 283 (#1788): mos-1272 first-run red: supporting.test's refuses() labelled each assertion with JSON.stringify(path), which throws on the 1n case added for E3; it now uses util.inspect. Test helper only 2026-10-23 15:17:25 -05:00
jason.woltje de6419f30f row 283 (#1788): sage-2925/sage-3649: optional editor.user (her Gitea login for library-human, read by row 288), checked with USER as the entry user is, kept in the output only when given; T3 accepted and refused cases; README; mutants M6e/M6f; BUILD.md records the departure from DESIGN.md section 1. Untested 2026-10-23 15:17:25 -05:00
jason.woltje 9ed0f3374a row 283 (#1788): webby-0236 E5: the git round-trip test drops GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR and GIT_INDEX_FILE and reads no global or system config (repo convention); BUILD.md records E5. Untested 2026-10-23 15:17:25 -05:00
jason.woltje efcd3f0fc4 row 283 (#1788): E4 per sage-3606: editor values starting or ending with git's crud() set (bytes <= 0x20 and , : ; < > " \ ') refuse library-mapping-invalid; T3 cases per character and key, inner chars pinned, git round-trip test; mutants M6c/M6d; BUILD.md cites crud() and records the claim (rev 3642). Untested 2026-10-23 15:17:25 -05:00
jason.woltje b358427677 row 283 (#1788): E2 per sage-3601: editor values with surrounding whitespace refuse library-mapping-invalid (git trims idents at commit); T3 padded cases per key, mutant M6c; BUILD.md records the departure from DESIGN.md section 1 and the fixture grep (none). Untested 2026-10-23 15:17:25 -05:00
jason.woltje 675813594f row 283 (#1788): webby-0227 E3: supportingPath checks the type before stringifying, so a non-string path refuses library-write-story-path; string case covers 7, null, undefined, 1n and an array; BUILD.md records E1-E3, E2 to Sage. Untested 2026-10-23 15:17:25 -05:00
jason.woltje 3f1aa76b03 row 283 (#1788): BUILD.md: drift check, N3 grep, mos-1330 :73 old and new text, Q2 tradeoff, Q4 handoff to row 288 via #1793 c30738, mutants M1-M8 exact strings, gate plan; doc only 2026-10-23 15:17:25 -05:00
jason.woltje bcd1f34c2d row 283 (#1788): survey.test:73 re-pin under mos-1330 item 4. The scopes line gains only supporting 0; old 'scopes canon 5, playground 1, unmapped 3; indexed 6', new 'scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6' 2026-10-23 15:17:25 -05:00
jason.woltje c87ddc6041 row 283 (#1788): C2-01d supporting folders. supporting lists on every universe kind with the playground's prefix rules, library-supporting-other-universe and the cross-list rules; optional editor {name, email} refusing controls, < and >; classify checks supporting before playground; supportingPath with the eight checks over the listTree array, exported; survey counts supporting (sage-2923); README; tests T1-T8. Untested: runs wait for Sage's CLEAR 2026-10-23 15:17:25 -05:00
jason.woltje ce4040542f row 283 (#1788): design draft 2 folds webby-0169 R1 (editor refuses < and >, T3, M6b) and N1 (check 8 compares NFC and case, NFD case in T6, M4b); Q2-Q4 accepted, Q4 handoff to C2-02 in BUILD.md; doc only 2026-10-23 15:17:25 -05:00
jason.woltje f2f76fd2c3 row 283 (#1788): design draft 1. supporting lists with the playground's prefix rules and library-supporting-other-universe, cross-list rules, the optional editor, supporting first in classify, supportingPath over the listTree array; survey.mjs departure Q1 for Sage, Q2-Q5 for Webby; doc only (sage-2906) 2026-10-23 15:17:25 -05:00
jason.woltje 59226f878f queue: revs 3637-3677: row 226 in-review and failed request (gosalyn 3637-3638), request (sage 3639-3640), approve record (webby 3641), landing note and done (sage 3665-3666); row 283 in-progress (honker 3642), E1 note, in-review and failed request (honker 3671-3673), request (sage 3674-3675, comment 31337), approve record (webby 3677); owner assigns 217 225 298 296 297 288 227 (sage 3643-3649); 225 and 217 after sets, 288 note (sage 3650-3652); row 227 in-progress (gizmoduck 3653); rows 220 and 216 in-progress, in-review and failed requests (researcher 3654-3659), requests (sage 3661-3664, comments 31321 31322); row 298 in-progress (boyd 3660), in-review and failed request (boyd 3667-3668), request (sage 3669-3670, comment 31336); row 319 GitHub consumer added (sage 3676, mos-1525) 2026-10-23 15:16:03 -05:00
jason.woltje f67e5c15c5 docs: brief for the GitHub consumer of row 297's protected githubWithToken callback, honoring expiry and cancellation, post-10-27 (mos-1525, #1803); docs only 2026-10-23 15:10:34 -05:00
jason.woltje e2b8733904 deferred: row 226 Webby N1-N4 (install id regex, first-run refusal leftovers, clone sha on the run sheet, library-status lock wait), webby-0225, #1727 2026-10-23 14:42:22 -05:00
jason.woltje fd3d2a99fe docs: host run sheet section 2 verifies Node's SHASUMS256.txt with the detached SHASUMS256.txt.sig, not the clearsigned .asc (Vikunja 611, ops-0243, mos-1514, mos-1515); docs only 2026-10-23 14:41:39 -05:00
jason.woltje 661890e909 gosalyn: row 226 C2-13b build, case2-install.sh writes the business file and personas through a plan step before any write, case2-precheck.sh adds the D6a mode, persona and Discord checks and --library-status with lastError first and the wider escape, case2-install.test.mjs, run sheet lines; candidate before the gate (webby-0087 N2 N3, sage-2195, sage-2205, #1727) 2026-10-23 14:41:39 -05:00
jason.woltje 3046fd60a3 gosalyn: row 226 C2-13b design d2, release package and activate dropped, precheck split into pre-start checks and --library-status after the bus is up, Webby's N1-N6 taken in, design 10 text change listed (webby-0085, webby-0086, sage-2189, #1727) 2026-10-23 14:41:39 -05:00
jason.woltje b97f9df1ee gosalyn: row 226 C2-13b design d1, the business file as an input, install steps, persona staging before 2b, re-run rules, precheck library checks and tests, Q1-Q3 for review (#1727) 2026-10-23 14:41:39 -05:00
jason.woltje 8352da88c5 agents: register Beakley, Fethry, Boyd, Lena, Violet and Bubba as seats so the queue can name them as row owners (Q245, mos-1506) 2026-10-23 14:18:27 -05:00
jason.woltje 45dca69f6e sessions: Sage 10-23 Vikunja 571 W rows 309-318 filed, 571 done 2026-10-23 13:19:06 -05:00
jason.woltje 293b42fee9 queue: revs 3617-3636: Vikunja 571 W build rows 309-318 added (sage 3617-3626: W1, W2, W3a, W3b, W6, W3, W4b, W5, W7, W8, post-10-27, brief docs/plans/2026-10-21_q195-writing-editor.md) and owners set to unassigned (sage 3627-3636) 2026-10-23 13:16:56 -05:00
jason.woltje 369607e92d sessions: Sage 10-23 rows 308 and 252 landings, 571 Connections design amendment, Vikunja 597; Scrooge row 252 line 2026-10-23 12:51:36 -05:00
jason.woltje 59f013247c queue: revs 3589-3616: row 306 in-progress, in-review and failed request (morgana 3589, 3592-3593), request (sage 3594-3595, comment 31207), approve record (dewey 3596), note and done (sage 3599-3600); row 308 added and briefed (sage 3590-3591), claim, in-review and failed request (gosalyn 3601-3603), request (sage 3604-3605, comment 31223), approve record (webby 3606, comment 31224), note and done (sage 3613-3614, landed 30e6bb680); row 291 note and done (sage 3597-3598); row 252 issue #1753 (sage 3607), in-review and failed request (scrooge 3608-3609), request (sage 3610-3611, comment 31230), approve record (webby 3612, comment 31229), note and done (sage 3615-3616, landed 45ea3fa0d) 2026-10-23 12:51:27 -05:00
jason.woltje e8bb13df84 docs: clarify Connections opening order and current-link filtering 2026-10-23 12:47:20 -05:00
jason.woltje dba2ebdb85 docs: plan required Console Connections view before real import 2026-10-23 12:47:19 -05:00
jason.woltje 45ea3fa0d2 row 252: library-cli and thread-claims bus.mjs stubs gain the throwing consoleCall export cli.mjs now imports (Mos GO mos-1465 under mos-1272, via sage-3497) 2026-10-23 12:25:40 -05:00
jason.woltje f69c962009 row 252: decide-sso J19 killer (a granted user's config says decide false, its decision POSTs stop at owner-only), J16 checks the config that starts without send; BUILD.md rulings, J11c and J18 equivalence, part 2 mutants (#1753) 2026-10-23 12:25:40 -05:00
jason.woltje 5b6c878143 row 252: decide-browser placements (mos-1272): the hostile grant goes in with its INSERT since decisions is append-only, and page() loads about:blank first so a same-URL visit reloads 2026-10-23 12:25:40 -05:00
jason.woltje 8c2c5303cf row 252: access-boot fixture provider gains issuer https://idp.test, so consoleOwners can key the owner (Mos GO mos-1464 under mos-1272, via sage-3494) 2026-10-23 12:25:40 -05:00
jason.woltje 23e3d09fa9 row 252: decide-sso first-run reds placed (mos-1272): a decision transport without --send never starts, and a POST to /api/bus/decision.* meets row 74's read-only refusal; BUILD.md run notes (#1753) 2026-10-23 12:25:40 -05:00
jason.woltje 795a645e65 row 252: console-decide events() keeps the row kind as event, since an answer body's kind shadowed it (first-run red at :180, two vacuous zero counts), and a J7 killer test for the console cap release over the socket (#1753) 2026-10-23 12:25:40 -05:00
jason.woltje 076ca8d6e4 row 252 BUILD.md: rebase onto fe4ee8c7b, commit map and the six conflict resolutions (sage-3482) 2026-10-23 12:25:40 -05:00
jason.woltje 68be863248 row 252: mos-1321, reword the shell.test write-boundary comment so Approve and Deny is the one decision control; BUILD.md records the ruling and its channel-proof review condition 2026-10-23 12:25:40 -05:00
jason.woltje 264fad2e26 row 252 BUILD.md: channel-proof overlap with row 239 (sage-2835) 2026-10-23 12:25:40 -05:00
jason.woltje 24672fd9d6 row 252: Console inbox Approve and Deny for grant requests, human-only through a console owner proof, confirmation then resolve, via console with human.input before the executor (#1753, Webby N1-N5); tests not yet run 2026-10-23 12:25:39 -05:00
jason.woltje 30e6bb6803 gosalyn: PRD 1.17, Case 2 story files are DOCX (row 308, sage-3516, Q235 to Q237, Q239). Applies the approved d9 text at 4607e15b8 word for word to the 1.16 text. Status, the header sentence and a 1.17 source line, the Creative library lead, REQ-LIB-1, -2, -4, -5 and -6 changed as d9 says, new REQ-LIB-8 for the writing store and REQ-LIB-9 for the Console's DOCX editor, the 1.17 notes and four risks. requirements.json goes to 1.17 with 87 ids and the d9 titles, and the tasks and bus test pins follow 2026-10-23 12:20:30 -05:00
jason.woltje 7defb58e97 sessions: Sage 10-23 rows 291 and 306 landings, row 308 launch 2026-10-23 11:37:34 -05:00
jason.woltje 9c8877bdf3 plans: DEFERRED row 306 N1 (revoke Set lost on broker restart) and N2 (pending limit stop answers not-holder first), landed 2d0318a44 (mos-1463) 2026-10-23 11:35:21 -05:00
jason.woltje 2d0318a44a morgana: row 306 build (#1813): #accessRevoked from #revokeLost, #agent answers thread-revoked until endLaunch, runner stops on thread-revoked, READMEs, access-revoke.test 7 7 (mos-1463) 2026-10-23 11:33:17 -05:00
jason.woltje bd3fa2a484 morgana: row 306 red-first tests (#1813): bus revoke-window T1/T2, cli revoke-window T3 forcing test; no product code 2026-10-23 11:33:17 -05:00
jason.woltje ab575ee553 morgana: row 306 design r2 (dewey-0918 C1-C3, sage-3483/mos-1463): T3 asserts no thread-revoked event for A, T2 limit half makes a second call, six-suite gate, access-revoke edit bounded to 7 7 2026-10-23 11:33:17 -05:00
jason.woltje 1676ad0de8 morgana: row 306 design r1 (#1813, sage-3478): #accessRevoked set from #revokeLost, #agent answers thread-revoked until endLaunch, runner maps it to stopped; T1-T3, M1-M3, six access-revoke asserts to Mos via Sage 2026-10-23 11:33:17 -05:00
jason.woltje 4874d1e83a gosalyn: PRD 1.16 routes library.write under REQ-LIB-6 (mos-1328, mos-1329, sage-2919, webby-0164 B2). REQ-WAIT-3 gains the routing paragraph with its bounds, creative roles only and within-role, Markdown under supporting folders a human marks, never a story file, the writing agent's live Gitea grant on the library project, one commit per write by its own account with the trailer, notice and revert. REQ-LIB-6 names the action. Status, the header, a 1.16 source line and a 1.16 note. requirements.json changes its version only, with the test pin 2026-10-23 11:15:49 -05:00
jason.woltje e909ef51a2 plans: PRD 1.17 brief (Gitea #1815, Vikunja 570): Gosalyn builds d9 4607e15b8 onto 1.16 after row 291, Webby reviews, four files, Sage gates 2026-10-23 10:44:58 -05:00
jason.woltje 4607e15b83 sage: PRD 1.17 d9 r3 (02f28dec), Mos's J-b check mos-1470: J-b records N20 as confirmed, REQ-LIB-5's version tag cites Q239 for the Connections view (N21), d9 changes names both 2026-10-23 10:41:08 -05:00
jason.woltje 8b42010551 sage: PRD 1.17 d9 round 2 (b17ce386), Webby round 6 B1: REQ-LIB-5, its note, the Risks line and the A6 line take mos-1443 R3's text as written; J-b in the past tense (N19); the import-waits line cited as Mos's R3 default (N20) 2026-10-23 10:36:16 -05:00
jason.woltje 4863e2caf4 sage: PRD 1.17 draft 9 (7dfc52fd), d8 plus Jason's Q41 O2 (Q239, mos-1467): REQ-LIB-5 requires the Console Connections view for converted chapters, note and Risks line follow, J-b records the answer; Webby round 5 N15-N17 2026-10-23 10:25:37 -05:00
jason.woltje 7b25a60633 agents: webui runs set MOSAIC_REQUIRE_EDITOR_BUILD=1 and linked worktrees npm ci packages/webui (Vikunja 597) 2026-10-23 10:01:25 -05:00
jason.woltje e86df37d76 sessions: Sage 272 and 307 landings, 306 design, 252 rebase check 2026-10-23 09:59:29 -05:00
jason.woltje fe4ee8c7b5 queue: revs 3582-3588: row 307 in-review and failed request (gizmoduck 3582-3583), request (sage 3584-3585, comment 31189), round 1 approve record (duckworth 3586), landing note and done at d243f6b25 (sage 3587-3588); queue only 2026-10-23 09:42:05 -05:00
jason.woltje 0621c439e0 plans: DEFERRED gains row 307 N1 (row 270's store-unreadable doesn't stick; #1814 landed d243f6b25) 2026-10-23 09:40:06 -05:00
jason.woltje d243f6b25f gizmoduck: row 307 run logs: capture.mjs makes each fixture's data root before sessionStore (base session.mjs:78 needs it), BUILD.md expected red adds claude-history test B (duckworth-0287), steps 1-6 results on 1f6db1589 and 1f00d4f91 with skips named and M1-M4 killed (sage-3455, sage-3465) 2026-10-23 09:37:50 -05:00
jason.woltje 869670d1b1 gizmoduck: row 307 BUILD.md: commits, old-test edits against mos-1459, new tests and the empty-session correction to test B, expected red on 1f6db1589, run plan with mos-1275 limits, mutants M1-M4 2026-10-23 09:37:50 -05:00
jason.woltje dbbda25373 gizmoduck: row 307 (#1814, Vikunja 598) harness kind gains compactions: pi's stream scan and Claude-history check move to packages/plugins/harness/pi/compactions.mjs, claude-code answers harness-unsupported, core keeps the ledger and checks each answer, readLedger reads the reason only, the launcher passes the loaded module; four old-test edits under mos-1459 2026-10-23 09:37:50 -05:00
jason.woltje a9da046f83 gizmoduck: row 307 (#1814, Vikunja 598) new tests, red first: row 270's ledgers per reason read the same count and the next collection goes through the harness module, core refuses an answer out of contract, pi answers what base scanPi read, claude-code answers harness-unsupported, a module without compactions refuses missing-export; capture.mjs checks the literals against base 1e629c892 2026-10-23 09:37:49 -05:00
jason.woltje 4a71b56abc gizmoduck: row 307 design r1 notes: seat suite whole and compactions-launch T3 run-not-skip in the gate (Duckworth C1, C2), launcher closure comment on bypassing call() (N1), line refs :301 and :302 (N2) (duckworth-0280) 2026-10-23 09:37:49 -05:00
jason.woltje 5208894196 gizmoduck: row 307 (#1814, Vikunja 598) design: harness export compactions({ store, cursors, budget, streams, first }), interfaceVersion stays 1, core keeps the ledger, readLedger reads the reason, ledger compatibility table, four old-test edits for Mos (sage-3440) 2026-10-23 09:37:49 -05:00
jason.woltje 8dc132bec8 queue: revs 3572-3581: row 272 round 3 approve record (scrooge 3572, comment 31177), row 307 claim (gizmoduck 3573), row 272 round 4 moves (huey 3574-3576), request (sage 3577-3578, comment 31183), round 4 approve record (scrooge 3579, comment 31184), landing note and done at d33989230 (sage 3580-3581); queue only 2026-10-23 08:58:25 -05:00
jason.woltje 08b8831bce plans: DEFERRED gains row 272 N1 (user session verbs) and the row 270 N6 rebound after row 307 (#1777 landed d33989230) 2026-10-23 08:56:47 -05:00
jason.woltje d339892301 row 272: scope unavailable-session waits to the chat area 2026-10-23 08:53:07 -05:00
jason.woltje 0ea3db297b row 272: fix session fixture authority and initial chat render 2026-10-23 08:53:07 -05:00
jason.woltje f859d06d20 row 272: preserve granted-user default chat on denied session catalog 2026-10-23 08:53:07 -05:00
jason.woltje 70651f45d8 row 272: retain session recovery state on invalid selection 2026-10-23 08:53:07 -05:00
jason.woltje 490f45f024 row 272: preserve untested session CLI and Console draft (sage-3145) 2026-10-23 08:53:07 -05:00
jason.woltje 1e629c892d queue: revs 3565-3571: row 272 round 3 moves on 72690cae7 (huey 3565-3567) and request (sage 3569-3570, comment 31174); row 307 added and briefed (#1814, Vikunja 598, mos-1455 item 3; sage 3568, 3571); queue only 2026-10-23 08:08:15 -05:00
jason.woltje 84de3fbca3 plans: row 270 pi compaction reading moves behind the harness kind (#1814, Vikunja 598, mos-1455 item 3, Gizmoduck, reviewer Duckworth); gate template runs all five suites and sets MOSAIC_REQUIRE_EDITOR_BUILD=1 (mos-1455 item 4, mos-1453) 2026-10-23 08:02:15 -05:00
jason.woltje 1e4c1e6e94 queue: revs 3539-3564: rows 300 and 301 round 1 claims and in-review moves (launchpad 3539-3544), round 1 requests (sage 3545-3548), round 2 moves (launchpad 3549-3554), round 2 requests (sage 3555-3558), Duckworth approve records 300 (3559) and 301 (3560), landing notes and done at 07315e651 and 5b846b048 (sage 3561-3564); queue only 2026-10-23 07:33:26 -05:00
jason.woltje 5b846b048a launchpad: row 301 (#1808): library-cli and thread-claims bus.mjs fixtures export throwing busBusiness and userCaller stubs (mos-1451 under mos-1272); BUILD.md repaired with run 1 and the ruling; manifest seven paths; unrun 2026-10-23 07:26:44 -05:00
jason.woltje 59f0e44f47 launchpad: row 301 (#1808): channel-proof re-pins webui bus.mjs at 39fcb27c for the user transport fold, humanCall unchanged (mos-1449 under mos-1272); unrun 2026-10-23 07:26:44 -05:00
jason.woltje bb031ec840 launchpad: row 301 (#1808): R6-N4, user-bus.mjs folded into bus.mjs beside humanCall, unchanged; channel-proof re-pin held for Mos's go; unrun 2026-10-23 07:26:44 -05:00
jason.woltje 07315e6514 launchpad: row 300 (#1807): run 1 gate results in BUILD.md (1/0, webui 375/0, M12 killed) and Duckworth's N1; packet only 2026-10-23 07:26:44 -05:00
jason.woltje fb04cbc431 launchpad: row 300 (#1807): run 1 first-run red placed (mos-1272), the test's call helper resolves headers again as granted-user.test does; manifest re-hashed 2026-10-23 07:26:44 -05:00
jason.woltje 9d929911c8 launchpad: row 300 (#1807): R6-N1, an HTTP-level test of the user-session option swap with the userRoute gate stubbed open by a load hook, and mutant M12; unrun 2026-10-23 07:26:44 -05:00
jason.woltje bcf3c1dacf queue: revs 3531-3538: row 303 landing note and done at e551cd846 (sage 3531/3532); row 304 approve record (dewey 3533), row 305 approve record (filbert 3534); row 304 landing note and done at a863015a5 (sage 3535/3536); row 305 landing note and done at d52d9f1f2 (sage 3537/3538); queue only 2026-10-23 06:54:46 -05:00
jason.woltje d52d9f1f2f row 305 (#1812): gate run 1 evidence (sage-3374): both suites green, mutants a and b killed in both, c a known survivor 2026-10-23 06:49:49 -05:00
jason.woltje 830f3d8bff row 305 (#1812): BUILD.md notes the SIGKILL leak and its cleanup (Filbert N1) 2026-10-23 06:49:49 -05:00
jason.woltje e48d602165 row 305 (#1812): BUILD.md with the trace, change, gate and the three mos-1445 mutants 2026-10-23 06:49:49 -05:00
jason.woltje cabcb81d49 row 305 (#1812): each suite's network check also requires the network present under the run's project before the remove (mos-1445) 2026-10-23 06:49:49 -05:00
jason.woltje 353d65f577 row 305 (#1812, Vikunja 591) draft: per-run COMPOSE_PROJECT_NAME, exit trap removes that one network, one absent-network check per suite; for Mos through Sage before the build (mos-1272) 2026-10-23 06:49:49 -05:00
jason.woltje 2c289f6f0d SESSIONS: filbert row 305 round 1 approve (c31127, rev 3534) 2026-10-23 06:47:32 -05:00
Rocko a863015a51 test: match Claude memory slug normalization under mos-1442 2026-10-23 06:46:02 -05:00
jason.woltje e551cd8466 launchpad: row 303 (#1810): R9 asserts the rehearsal directory is set before the placeholders check joins it, so a null shows the rehearsal output (mos-1422, Vikunja 589) 2026-10-23 06:41:07 -05:00
jason.woltje c13c337e16 queue: revs 3521-3530: rows 304 and 305 claimed (rocko 3521, gizmoduck 3522); row 305 in-review on 7e8218a2e (gizmoduck 3523), request failed without a token (gizmoduck 3524), sage's request posted c31126 (3525/3526); row 304 in-review on 5e8354ebb (rocko 3527), request failed without a token (rocko 3528), sage's request posted c31128 (3529/3530); queue only 2026-10-23 06:27:36 -05:00
jason.woltje 435a7458d3 sage: PRD 1.17 draft 8 (343cff6f), d7 plus Mos's d8 fix list mos-1443 (R1-R14, M1-M45, J-i to J-l) and the mos-1446 choices (M10 default, M15 quotes Q235 in full, M40 both sentences); M15 and M16 cite b99ca86b9. Webby APPROVE round 5 webby-0210, no B items; N15-N17 carry to d9 2026-10-23 06:21:31 -05:00
jason.woltje b99ca86b9c docs: align editor conversion boundary and host restore with PRD d8 2026-10-23 06:08:20 -05:00
jason.woltje 0e057e05eb queue: revs 3511-3520: rows 304, 305 and 306 added and briefed (sage 3511-3516); row 272 round 2 in-review on b44332a93 (huey 3517), request failed without a token (huey 3518), sage's request posted c31120 (3519/3520); queue only (sage) 2026-10-23 06:02:42 -05:00
jason.woltje c16e943b90 briefs: mos-1442 approvals for the 590 and 591 rows (#1811, #1812), the 295 N1 revoke-window row (#1813); DEFERRED 295 N1 (sage) 2026-10-23 05:51:16 -05:00
jason.woltje ac1644e1ce BUILD-LOG and SESSIONS: rows 302, 295 and 219 landings, the Docker pool diagnostic; DEFERRED 219 N1 (U6); brief for the Vikunja 590 and 591 rows (sage) 2026-10-23 05:48:40 -05:00
jason.woltje 90633c8648 queue: revs 3495-3509: row 295 round 2 in-progress and in-review (morgana 3495-3497), sage's request posted (3498/3499), dewey approve record (3500), row 295 landing note and done at 2efd127ce (sage 3501/3502); row 219 in-review on b899095e4 (rocko 3503/3504, request failed without a token), sage's request posted c31107 (3505/3506), dewey approve record c31108 (3507), row 219 landing note and done at 5fdfd7275 (sage 3508/3509); queue only 2026-10-23 05:45:46 -05:00
Rocko 5fdfd7275c docs: preserve row 219 full gate on the post-295 merge 2026-10-23 05:37:44 -05:00
Rocko a236dbc30a docs: preserve row 219 mos-1429 short-run evidence 2026-10-23 05:37:44 -05:00
Rocko 312260db7b test: prepare row 219 admission pins under mos-1429 2026-10-23 05:37:43 -05:00
Rocko d7b4967ed0 docs: record row 219 rebase after granted-user Console 2026-10-23 05:37:43 -05:00
Rocko 90a3c03cfb test: assert creative history in its native session store
Apply only the paths and legacy absence pins approved by mos-1420 and sage-3243. Preserve exact expected history and the stopped run evidence. No product change or rerun.
2026-10-23 05:37:43 -05:00
Rocko 282f5b4e45 test: pin the compaction audit before the persona refusal
Apply the exact two-row fixture assertion approved by mos-1418 and sage-3232. Preserve earlier red logs; no product change and no rerun before review.
2026-10-23 05:37:43 -05:00
Rocko 781fd509c4 test: give row 219 persona fixture its explicit session
Apply mos-1408 and sage-3178 without creating the directory being asserted. Preserve the first red evidence and select TAP explicitly for the next approved run.
2026-10-23 05:37:43 -05:00
Rocko 52a73e83bd docs: rebase row 219 gate packet and record run bounds 2026-10-23 05:37:43 -05:00
Rocko 049214486b wip: publish unverified row 219 inspection candidate
Preserve the draft for inspection under sage-3113. Tests and mutants have not run. Old-test adaptations approved in mos-1316 and mos-1317 are recorded in OLD-TEST-CHANGES.md.
2026-10-23 05:37:43 -05:00
jason.woltje 2efd127ce6 morgana: row 295 r2 gate results on d68b4300d (sage-3338, sage-3340): red first, green, M8 count, whole files, suites, script limits and skips, M1-M16 killed 2026-10-23 05:06:14 -05:00
jason.woltje fdbe95d88d morgana: row 295 r2 narrow alice grant (Dewey F1, sage-3333): aliceAccess(ref) names only the test's project, set after project.create; asserts and bindLaunch lines byte-identical; BUILD.md preservation record updated 2026-10-23 05:06:14 -05:00
jason.woltje cb96220b53 morgana: row 295 r2 old-test setup (mos-1436 option a): message-filter's :117 and governance-pin tests get an alice collaborator access document, spec-fixtures fixture() takes an optional access (null default); asserts and bindLaunch lines byte-identical; BUILD.md gate record, assertion preservation, N1 revoke window 2026-10-23 05:06:14 -05:00
jason.woltje 6408dcec4f morgana: row 295 r2 first-run reds placed (mos-1272): claim rows spread off the store's null prototype in access-revoke and thread-revoke, demo switches the pi harness on for startBroker, Alice binds at her first call in thread-revoke 2026-10-23 05:06:14 -05:00
jason.woltje 0c8f12ea20 morgana: row 295 r2 rebase on 80793d824 (sage-3256): accessFor comment is stamp-cached (dewey-0871 N3); BUILD.md rebase note 2026-10-23 05:06:14 -05:00
jason.woltje 24f8f5d739 morgana: row 295 r2: wrap the launcher's revoked comment (dewey-0874 nit) 2026-10-23 05:06:14 -05:00
jason.woltje 31da9dcb85 morgana: row 295 r2 (dewey-0871): threadsState ends an expired grant's held claim from the cached read by access-expiry (B1, D7, M16, M11 claim row); access.reload scope as D8 (N1); bus README access.reload, revoked and revocation (N2); tests unrun 2026-10-23 05:06:14 -05:00
jason.woltje ff54a15563 morgana: row 295 FED R4 revocation (#1801): 30 s access timer, access.reload, one-transaction claim end on a loss, threadsState revoked, launcher SIGTERM and no start, CLI reload after grant/revoke; tests unrun (no CLEAR yet) 2026-10-23 05:06:14 -05:00
jason.woltje 249d42dfc5 morgana: row 295 plan (FED R4 revocation, #1801): sweep on timer, access.reload and user requests; threadsState revoked; launcher skip and SIGTERM; bindLaunch and role.claim refuse thread-revoked 2026-10-23 05:06:14 -05:00
jason.woltje 07802dc57a queue: revs 3488-3494: row 302 (#1809) in-review on 2a8281a6f (gizmoduck 3488), gizmoduck's request failed without a token (3489), sage's request posted c31091 (3490/3491), dewey approve record c31092 (3492); row 302 landing note and done move at 3ed9fd754 (sage 3493/3494); queue only 2026-10-23 03:53:06 -05:00
jason.woltje 3ed9fd754a gizmoduck: row 302 (#1809) gate results: run 1 stopped on launcher-released.test:89 (d6, mos-1430); run 2 on merge 783602e06 (5b63ea323 onto 1ac655005) red as ruled, suites green, M1-M5 M7-M9 killed; RESULTS.md and logs, packet only 2026-10-23 03:47:38 -05:00
jason.woltje c9fcf9b5af gizmoduck: row 302 (#1809) d6: launcher-released.test:89 counts only a-run's delivery rows and pins the one routed row with holder_run null; #route writes that row for every message (gizmoduck-0388, sage-3294, sage-3296, Mos ruling pending sage-3293); local, not pushed 2026-10-23 03:47:38 -05:00
jason.woltje 2b6111a176 row 302 (#1809, Vikunja 588): a run threadIdle or landRelease released answers launcher-released on every call needing the claim until it ends, role.claim refuses it (Dewey D1), and the runner exits stopped without releasing again 2026-10-23 03:47:38 -05:00
jason.woltje 4a827edb0c row 302 (#1809, Vikunja 588): red first, the final forcing tests and the four pins Mos ruled (mos-1414/1415/1416), over unfixed product files; BUILD.md d3-d5 and ASSERTION-PRESERVATION 2026-10-23 03:47:38 -05:00
jason.woltje 908f92b039 gizmoduck: row 302 design d2: threads.test pins are :392/:394/:395 (sage-3204), land-relaunch grep finds land.test:464 as a third old pin under Q1 2026-10-23 03:47:38 -05:00
jason.woltje 62470a3ec3 gizmoduck: row 302 BUILD.md folds Dewey's d1 approval (dewey-0875): D1 role.claim refuses a released run, landRelease reaches runner.test:742-772 so Q1 moves :771-772, Q2-Q4 and N1-N4 settled 2026-10-23 03:47:38 -05:00
jason.woltje fee08d7ab9 gizmoduck: row 302 (#1809, Vikunja 588) idle-stop race design round d1 for Dewey: idle-released code from the broker's own record of threadIdle's release, runner maps it to stopped/0, forcing cli test holds the window open until a call lands, threads.test:393 is the one changed pin (sage-3200, mos-1411) 2026-10-23 03:47:38 -05:00
jason.woltje 1ac655005e BUILD-LOG and SESSIONS: row 299 gate and landing, row 303 review, 219 and 295 rebases (sage) 2026-10-23 03:10:29 -05:00
jason.woltje 8a35ffadb4 queue: revs 3474-3487: row 303 (#1810) in-progress and in-review on 3c3be5d3 (launchpad 3474/3475), launchpad's request failed without a token (3476), sage's request posted c31082 (3477/3478), duckworth approve record c31079 (3480); row 299 (#1806) claim (gander 3479), in-review on 277ace0b (gander 3481), gander's request failed without a token (3482), sage's request posted c31083 (3483/3484), filbert approve record c31084 (3485); row 299 landing note and done move at ffb2fedfb (sage 3486/3487); queue only 2026-10-23 03:07:32 -05:00
jason.woltje ffb2fedfbc docs(row299): record fresh timing gate and six assertion kills 2026-10-23 03:03:09 -05:00
jason.woltje 11d1e2e0ce docs(row299): record clean integration on 80793d824 2026-10-23 03:03:09 -05:00
jason.woltje ed81f6eba0 docs(row299): record mos-1409 preservation and static candidate 2026-10-23 03:03:09 -05:00
jason.woltje e14164ab49 test(case): observe P10 wait directly under mos-1409 2026-10-23 03:03:09 -05:00
jason.woltje 10113b04f5 docs(row299): propose direct P10 wait timing for mos-1272 review 2026-10-23 03:03:09 -05:00
jason.woltje c1f910b4cc SESSIONS: filbert row 299 manifest round 1 approve (c31084, rev 3485) 2026-10-23 03:00:12 -05:00
jason.woltje 15e688cc01 BUILD-LOG and SESSIONS: row 294 rounds 3-4 and landing, R9 transient and row 303, row 219 edits 2-3 (sage) 2026-10-23 02:11:43 -05:00
jason.woltje 80793d824e queue: revs 3464-3473: row 294 (#1800) round 4 in-progress and in-review on a1ff5d67 (launchpad 3464/3465), launchpad's request without a token failed (3466), sage's request posted c31055 (3467/3468), duckworth approve record c31053 (3469); row 294 landing note and done move at d3059c2c3 (sage 3470/3471); row 303 (R9 guard, #1810, Vikunja 589, mos-1422) added and briefed (sage 3472/3473); queue only 2026-10-23 02:10:05 -05:00
jason.woltje ad6ca79304 docs: brief for the R9 guard row (Vikunja 589, #1810, mos-1422); docs only 2026-10-23 02:07:02 -05:00
jason.woltje d3059c2c3d launchpad: row 294 run 4 (#1800): whole gate green on 8616b9a4f (webui 365, cli 909, bus 873, 10 scripts), M1-M11 killed; M1 record names G4's first failure and G5 green (Duckworth C1); receipts in run4/ 2026-10-23 01:37:03 -05:00
jason.woltje 6f07930679 launchpad: row 294 round 4 (#1800): rehearsal fixture copies carry cli access.mjs and transport.mjs, B1-B3 pin the fixture's tracker warning in full (Mos mos-1419, sage-3238); manifest round 4 (19 paths), run 3 receipts; unrun 2026-10-23 01:37:03 -05:00
jason.woltje b01e1c9ee1 launchpad: row 294 round 3 (#1800): G5's baseline is coalesce(max(seq), 0), since max(seq) is NULL on the fixture's empty events table and run 2 read 0 of 22 refusal events (sage-3212, Mos ruling pending sage-3211); manifest round 3, run 2 receipts; unrun 2026-10-23 01:37:03 -05:00
jason.woltje 8716e72f26 launchpad: row 294 round 2 (#1800): browser.test pin gains /federation.js at index 2 (Mos mos-1412 (a)), G5 pins one action.refused {code: access-refused} per refused verb and every other table unchanged (sage-3194); manifest round 2 (18 paths), ASSERTION-PRESERVATION.json, run 1 receipts; unrun 2026-10-23 01:37:03 -05:00
jason.woltje 9beeeb9955 launchpad: row 294 run limits from recorded durations, 3x or more (sage-3187, mos-1275) 2026-10-23 01:37:03 -05:00
jason.woltje 9e43fefe60 launchpad: row 294 BUILD.md M1 expectation corrected (Duckworth C1), review recorded with N1-N4 dispositions 2026-10-23 01:37:03 -05:00
jason.woltje d0460ed475 launchpad: row 294 round 1 manifest (17 paths from 9450774cb) and notes with the owner-transport exception 2026-10-23 01:37:03 -05:00
jason.woltje 26f93fa746 launchpad: row 294 sage-3157, owner-transport exception written out, userOptionsFor, G9 pins no user value, M10/M11 (unrun, #1800) 2026-10-23 01:37:03 -05:00
jason.woltje c9e67e7d2d launchpad: row 294 BUILD.md M4 kill names the moved test 2026-10-23 01:37:03 -05:00
jason.woltje 52d2a16109 launchpad: row 294 rulings sage-3153 (boot refusal line, owner transport seam G9, C7 follow-up, access.test.mjs unchanged; unrun, #1800) 2026-10-23 01:37:03 -05:00
jason.woltje afef0ea887 launchpad: row 294 FED R6 build and plan, granted-user sign-in (unrun, #1800)
Console granted-user sign-in, a user-cli transport per session, the
per-request grant re-read, the owner-only gate and user page, the notice,
the hub label and peer links, accessContextFor peers, and the bus boot
access wiring (sage-3142). Tests G1-G8 and B1-B3 are written and not yet
run; runs wait for Sage's CLEAR. Choices C1-C9 in
agents/launchpad/work/queue-294/BUILD.md.
2026-10-23 01:37:03 -05:00
jason.woltje 09d0d6fafa queue: revs 3457-3463: row 294 (#1800) round 3 in-progress and in-review on cb1648f3 (launchpad 3457/3458), launchpad's request without a token failed (3459), sage's request posted c31049 (3460/3461), duckworth approve record c31045 (3462); row 219 restart note on b97a3ff97 (rocko 3463); queue only 2026-10-23 00:53:42 -05:00
jason.woltje ffbc0d7a6d BUILD-LOG and SESSIONS: row 292 landing, idle-stop race row 302 and its rulings, rows 300/301, row 294 rounds 2-3 (sage) 2026-10-23 00:47:17 -05:00
jason.woltje 756284e8e9 queue: revs 3448-3456: row 294 (#1800) round 2 in-progress and in-review on 7ec7b1ea (launchpad 3448/3449), launchpad's request without a token failed (3450), sage's request without a token failed (3451/3452), sage's request posted c31042 (3453/3454), duckworth approve record c31029 (3455); row 302 (#1809) in-progress (gizmoduck 3456); queue only 2026-10-23 00:40:17 -05:00
jason.woltje 0e30a85fac queue: revs 3444-3447: row 302 (idle-stop race, #1809, Vikunja 588, mos-1411) added and briefed; row 292 (#1797) landing note and done move at 6aab979e6 (sage); queue only 2026-10-23 00:22:51 -05:00
jason.woltje 6aab979e6c row 292 (#1797) gate results on 0d0021454: r2 old-test red compactions-launch:229 (exit-22 race, Vikunja 588), mos-1411 diagnostic 0/10 with one not-holder exit 22 seen in the clear test, r3 cli rerun 910/910, ten shells green, M1-M9 killed; rebase receipts (sage-3183, sage-3185, sage-3193) 2026-10-23 00:18:02 -05:00
jason.woltje e5ffa76932 row 292 (#1797) packet: targeted mutant kill rules per sage-3152 (pattern, stack lines and text per mutant; anything else is a STOP; a survivor gets one whole-file run), short TMPDIR per stage, run plan after Duckworth's round 2026-10-23 00:18:02 -05:00
jason.woltje bf4ba4280e row 292 (#1797) packet: rebase onto b23783f39 with the 0585b3213 resolution (row 203's console and federation text kept word for word, Gander's text for any other key), range-diff, the three patch-ids, C1/N3, mutants M1-M9 (unrun) and the run plan (sage-3146, sage-3148) 2026-10-23 00:18:02 -05:00
jason.woltje 495c128658 row 292 (#1797) C1 and N3 for duckworth-0229: a pre-270 loader stand-in of its own (C1's loader without the row 270 concat, the list before bce491715) refuses compactionReadBytes and compactionStreams on a full activate and rollback, naming the key; C1's loader reads both; the missing-loader case uses compactionStreams 1024 and checks row 203's console and federation text word for word with no loader 2026-10-23 00:18:02 -05:00
jason.woltje 4b612781b4 docs(release): record static approval and post-compaction review conditions 2026-10-23 00:18:02 -05:00
jason.woltje be43572166 docs(release): record any-key config plan and static review packet 2026-10-23 00:18:02 -05:00
jason.woltje 39bde4fd43 fix(release): validate non-console config keys with target loader 2026-10-23 00:18:01 -05:00
jason.woltje fdedc56e2a docs: idle-stop race brief records the 10-run diagnostic (not-holder seen once, run r846b8d083502); docs only 2026-10-23 00:04:20 -05:00
jason.woltje e3ef1eaf6f docs: brief for the idle-stop race row (Vikunja 588, #1809, mos-1411); docs only 2026-10-23 00:00:21 -05:00
jason.woltje c902a767dc queue: revs 3439-3443: rocko row 219 claim (3439), sage adds rows 300 (R6-N1, #1807) and 301 (R6-N4, #1808) and briefs both; queue only 2026-10-22 23:30:34 -05:00
jason.woltje 86d5ac69bf docs: R6 follow-up briefs for row 294 N1 (#1807) and N4 (#1808); docs only 2026-10-22 23:13:49 -05:00
jason.woltje 4a9c536e86 Row 293 (574) landing e51d11640 and the 3m-3n live window (activation 20261023T034557Z-464010fbe4c6): BUILD-LOG phase line, Darkwing and Sage SESSIONS lines; docs only 2026-10-22 23:01:20 -05:00
jason.woltje 83f1f854de queue: row 293 (574, #1805) landing note and done move (sage); queue only 2026-10-22 23:01:20 -05:00
jason.woltje e51d116402 docs(row293): record green integrated trace and whole suites 2026-10-22 22:55:25 -05:00
jason.woltje 78bbe12233 docs(row293): record integrated run2 and short TMPDIR correction 2026-10-22 22:55:25 -05:00
jason.woltje 92c0ca3db2 docs: preserve row293 first trace run and TMPDIR stop 2026-10-22 22:55:25 -05:00
jason.woltje d0ede15b95 docs(v574): record static approval and trace evidence requirements 2026-10-22 22:55:25 -05:00
jason.woltje 29bb1ccd46 docs(v574): audit fixture comparisons and plan trace proof 2026-10-22 22:55:25 -05:00
jason.woltje 4e22bd49f9 test(git): disable detached fixture maintenance per mos-1369 2026-10-22 22:55:25 -05:00
jason.woltje 464010fbe4 queue: revs 3435-3436. Row 294 (#1800): Duckworth APPROVE c30998 (3435, candidate 0b3f9c9d). Row 272 (#1777): Scrooge CHANGES c31006 (3436, candidate 0c3dc2ed); queue only 2026-10-22 22:39:36 -05:00
jason.woltje e57bfde734 queue: revs 3407-3434. Row 293 (574, #1805): Gander in-review r1 (3407, failed pre-send 3408), Sage request c30981 (3409/3410), Filbert APPROVE c30982 (3411). Row 292 (#1797): assigned gizmoduck (mos-1387), claim 3413, in-review r1 3419 (3420 failed), Sage request c30992 (3425/3426), Duckworth APPROVE c30975 (3433). Row 294 (#1800): Launchpad claim 3418, in-review r1 3422 (3423 failed), Sage request c30993 (3427/3428). Row 295 (#1801): Morgana claim 3414, in-review r1 3416 (3417 failed), Sage request c30991 resolved (3421/3424), Dewey CHANGES c30994 (3431). Row 272 (#1777): Huey claim 3415, in-review r1 3429 (3430 failed), Sage request c30997 resolved (3432/3434); queue only 2026-10-22 22:34:14 -05:00
jason.woltje 319d9f87ea sessions: Filbert row 293 (Vikunja 574) APPROVE c30982 rev 3411, plus re-added S0 re-check and 574 static review lines; docs only 2026-10-22 22:15:06 -05:00
jason.woltje b23783f395 Rows 270+271 landing (7328da8ca), 571 landing, MVP rows 293-299: BUILD-LOG phase line and Sage SESSIONS line; docs only 2026-10-22 21:34:23 -05:00
jason.woltje e18ad27115 docs: define mixed-format chapter ordering for conversion 2026-10-22 21:34:23 -05:00
jason.woltje 12f8c3089c queue: rows 293-299 briefed (revs 3399, 3401-3406, sage) and row 293 claimed by gander (rev 3400, gander-293-start); queue only 2026-10-22 21:34:06 -05:00
jason.woltje 253346e3c3 queue: rows 293-299 added (574 #1805, FED R6 #1800, FED R4 #1801, FED R7 #1802, GitHub grant method #1803, Q228 #1804, 573 #1806; mos-1371/1372), 208 after 294; 270 round 3 request c30929 and Duckworth APPROVE c30930; 270+271 done at 7328da8ca (mos-1351); queue only 2026-10-22 21:19:12 -05:00
jason.woltje 7328da8ca4 gizmoduck: row 270 (264c, #1775) gate results: run 3 red on case1-accept P10 band (Vikunja 573) and run 4 red on the checkout.test fixture race (Vikunja 574), both old tests unchanged in the range; run 5 GREEN on 53406f3e4 under mos-1369 and sage-3106, all phases and 38/38 mutants killed; BUILD.md only 2026-10-22 21:07:51 -05:00
jason.woltje 84002f960a gizmoduck: row 270 (264c, #1775) gate run 2 second red, fixed under mos-1364 and sage-3093: compactions-budget.test F1 (:39 input 8.5 * MiB is a whole number, now MiB + 0.5) and F2 (the 0o000 window in try/finally; the after-hook ran after setup's rm); K M22 :129, M23 :120, M22 and M15 notes; BUILD.md run 2 2026-10-22 21:07:51 -05:00
jason.woltje f89aaf57fe gizmoduck: row 270 (264c, #1775) gate run 1 red placed (mos-1272): compactions-launch.test's host wires row 271's five clear ops, as session-clear.test does; the clear test's successor never started without them; kill lines +5 in mutants.py K; BUILD.md run 1 2026-10-22 21:07:51 -05:00
jason.woltje d3a49f301a gizmoduck: rows 270+271 combined gate (C2) record before the first run (sage-3073, sage-3086, mos-1275, mos-1351): blob table, timeout table, GREEN and RED, order; code is ac44d0de9's 2026-10-22 21:07:51 -05:00
jason.woltje 562b05b43c gizmoduck: row 270 (264c, #1775) round 4 for sage-3073 and sage-3078: rows 270 and 271 rebased onto 25c09f19f (row 203 plus the O2 docs); mosaic-config.mjs keeps 203's federation key and both compaction keys; BUILD.md round 4 with the range-diff, the three merged files and C1 after 203; not run 2026-10-22 21:07:51 -05:00
jason.woltje 8c28ee7315 gizmoduck: row 270 (264c, #1775) round 3 for duckworth-0226 B4 (sage-3038): one collection opens at most the system config's compactionStreams stream files in name order (default 1024, 32 to 65536, a bad value takes the default and logs), not-collected past it with no cursor for an unopened stream; the missing-stream check uses a Set; readStreams, cap and launcher tests; mutants M22-M24, M10 anchor moved; docs/TOOLS.md has both keys and C1 (N4); BUILD.md round 3 with stream counts, the 231 KB bound, C1 and N5; not run 2026-10-22 21:07:51 -05:00
jason.woltje bce4917154 gizmoduck: row 270 (264c, #1775) round 2 for duckworth-0223 B1-B3 (sage-3030, sage-3031, mos-1352, sage-3033): pi streams read in 1 MiB chunks under a budget from the system config's compactionReadBytes (default 64 MiB, 1 MiB to 1 GiB, a bad value takes the default and logs), not-collected past it; Claude history from before the ledger is harness-unsupported for good; launcher, runtime reader, clear and legacy tests; mutants M13-M21; BUILD.md round 2 with sizes, schema change, rollback hazard, N1-N3; not run 2026-10-22 21:07:51 -05:00
jason.woltje 01b57f48bb gizmoduck: row 270 (264c, #1775) round 1 for review (sage-3024): mutants.py M1-M12 with anchors at 7d199f033, each matching once, kills read from the tests; run plan with broker.mjs reruns; launcher and runtime wiring named as untested; not run 2026-10-22 21:07:51 -05:00
jason.woltje 54ec0005e0 row 271 (264d, #1776): BUILD.md r3 gate on 9e18a9aaf (sage-3017): cli and bus session-clear.test whole green (5/5, 19/19), cli suite 874/874, M14 killed by the T6 test's first lines() assertion at :365; no first-run red. Doc only. 2026-10-22 21:07:51 -05:00
jason.woltje 61e262b7c0 row 271 (264d, #1776): r3 packet carries mutants.py, M1-M14 with their anchors, as the gate applies them (duckworth-0212 C1). Doc and harness only; no src or test change. 2026-10-22 21:07:51 -05:00
jason.woltje 540b6f16ba row 271 (264d, #1776): r3, T6 per sage-3001. New cli test: a bus unavailable (broker-exited) at the exit proof, then at retirement, holds the clear with no retirement, successor launch or slot handed on, logs once, and resumes when the bus answers. BUILD.md maps the halt case to session-launch.test.mjs:372 and launcher.mjs:824, binds the Q198 hold to row 257 with duckworth-0210's carry, and names M14 (drop clearPass's try/catch) and its kill. No src change. Not run (no CLEAR). 2026-10-22 21:07:51 -05:00
jason.woltje e6a675ee83 row 271 (264d, #1776): BUILD.md gate results on e8af389e6 (sage-2991): 18 touched files, bus/seat/cli suites and every test-*.sh green, M1-M13 killed by assertions in the named tests; the row gate's T6 coverage open to Sage. Doc only. 2026-10-22 21:07:51 -05:00
jason.woltje e77635cebb row 271 (264d, #1776): BUILD.md run limits for the e567f949c gate (mos-1275, sage-2991); mutant lines moved one down past the B1 comment. Doc only. 2026-10-22 21:07:51 -05:00
jason.woltje 3fc8ccef1a row 271 (264d, #1776): duckworth-0201 B1: #ownerRefusal passes 'session.clear' to #granted, so the project and instance sit in their own slots and a live collaborator grant passes the retirement recheck. N4 setup shared; a second N4 test keeps the grant and expects retired plus the successor; M13 restores the defect. 264e carry noted at the N4 tests. Not run (no CLEAR). 2026-10-22 21:07:51 -05:00
jason.woltje 27d3266fff row 271 (264d, #1776): BUILD.md records mos-1336, Mos's approval under mos-1292 of the later3l, rows() and thread-shares migration test changes on the 3m to 3n rebase, with its conditions 2026-10-22 21:07:51 -05:00
jason.woltje 6bd67225b5 row 271 (264d, #1776): BUILD.md states the post-rebase '3m' grep result as measured 2026-10-22 21:07:51 -05:00
jason.woltje 14715d644a row 271 (264d, #1776): r2 on 0cbe2e9b3 (sage-2968): 3m to 3n migration and 3m engine tests from fixtures/schema-3m.sql; BUILD.md records the label change, the old-test changes (later3l, rows() and the thread-shares migration test outside the mos-1315 list, for Mos) and the mutant lines on the rebased tree 2026-10-22 21:07:51 -05:00
jason.woltje f9a9c9b5a0 row 271 (264d, #1776): BUILD.md defines M1-M12 with their expected kills 2026-10-22 21:07:51 -05:00
jason.woltje 34388223d8 row 271 (264d, #1776): launcher uncertain-path test with test-driven ticks and a stopped runner; start() takes stopTimeoutMs 2026-10-22 21:07:51 -05:00
jason.woltje 3adb391b57 row 271 (264d, #1776): N4 failed-clear test on a user's thread with raw first rows; BUILD.md notes the failed path has no verb route yet 2026-10-22 21:07:51 -05:00
jason.woltje 4da907cd5e row 271 (264d, #1776): T4 tests round 1, not run (slot held). Bus session-clear.test.mjs (15), cli session-clear.test.mjs (3, bwrap) with clear-adapter.mjs; BUILD.md maps each T4 item and names what is still open (failed clear, launcher uncertain path, mutants) 2026-10-22 21:07:51 -05:00
jason.woltje aa8fc490f5 row 271 (264d, #1776): clear context build round 1, not run (slot held). Broker: session.clear, session.cancel and session.operation, the fence on send/open/claim, cancel at arrival for routed messages, the drained receive and the end drain, trusted ops sessionClears and sessionClearExited/Uncertain/Retire/Start. Launcher: clearPass before the threads loop (stop, proof, retire, successor start through check()/start()), waiting on a live row 269 survivor. Old tests per mos-1315, rebased on row 206 (sage-2968): label 3m to 3n, future step 3n to 3o, 3m-3n.sql in the engine copies and file lists, ready lines, later3l and the rows() exclusions gain both tables as 206 did for thread_shares (mos-1292, approved in mos-1336), schema.test counts 25 tables and 96 triggers, COUNTED and T45 LISTED gain both tables after thread_shares (mos-1318, mos-1319), TESTER_VERBS pin (mos-1325), fixtures/schema-3m.sql 2026-10-22 21:07:51 -05:00
jason.woltje a9884dc75c row 271 (264d, #1776): schema step 3m to 3n (rebased on row 206's 3l to 3m, sage-2968), session_ops and message_cancels with 7 triggers; successor id pinned as a lowercase version 8 UUID for Huey (gizmoduck-0274) 2026-10-22 21:07:51 -05:00
jason.woltje 25c2c1a560 gizmoduck: row 271 design: duckworth-0171 C1 (start stages keyed on a report counter, work attempt in its own column) and C2 (cancelled in the stage CHECK at attempt 0 and closing the op, claim and cancels untouched by a pre-retire cancel, post-retire successor reading stated, race and held tests); N1 grep at build 2026-10-22 21:07:51 -05:00
jason.woltje 4f827a5e81 gizmoduck: row 271 design: session.cancel and the cancelled stage built in 271 (sage-2828 re-rules Q3 per D7:263-264), pre- and post-retire paths, cancel tests and the fence-release mutant; N2 refuses session-name-too-long at accept 2026-10-22 21:07:51 -05:00
jason.woltje 7dc8ff1bfa gizmoduck: row 271 design: T45 LISTED gains session_ops and message_cancels to match COUNTED (mos-1319) 2026-10-22 21:07:51 -05:00
jason.woltje 61df3061ed gizmoduck: row 271 design: duckworth-0170 B1-B4 folded (cancel at arrival for routed messages, claim refused while clearing, op namespace both ways, attempt key on start stages) and N1-N8 answered; cancel recorded as a departure from D7:263-264 2026-10-22 21:07:51 -05:00
jason.woltje b212182ed1 gizmoduck: row 271 design: both new tables join COUNTED in stack-release.sh, so T46 passes unchanged; schema.test count pins name the 2 tables and 7 triggers; T45 LISTED question to Mos (mos-1318) 2026-10-22 21:07:51 -05:00
jason.woltje dac64ff311 gizmoduck: row 271 (264d) design draft 2: drained acknowledged by the run's own receive at a turn boundary (a landing turn has unread 0 with a child running), SIGTERM only after it; successor <old>-N bound at request with a deterministic id; no cancel (sage-2826); pre-existing test change list per mos-1315 2026-10-22 21:07:51 -05:00
jason.woltje b329ad69f5 gizmoduck: row 271 (264d) design draft 1 in the packet: the threadIdle fence as the drain, the idle stop as the only signal, a session_ops journal and message_cancels in one schema step; Q1-Q4 for Sage and Mos 2026-10-22 21:07:51 -05:00
jason.woltje 8d4d4ec598 gizmoduck: row 270: the bus legacy test inserts its legacy thread by hand (sessions is append-only) and the test business has the assistant thread.create needs; not run 2026-10-22 21:07:51 -05:00
jason.woltje 5ddc36ca97 gizmoduck: row 270 (264c, #1775): completed Pi compactions counted into a host ledger beside the session store, keyed (entry id, timestamp) store-wide (sage-2806, sage-2814); launcher collects at run start and end under the workspace lock; session.list rows gain compactions through a runtime-injected reader; not run (slot held) 2026-10-22 21:07:51 -05:00
jason.woltje f510d73676 gizmoduck: rows 270 and 271 packets: the 264c count and 264d clear contracts pinned for row 272 (Huey), and the Pi 0.85.1 compaction source contract 2026-10-22 21:07:51 -05:00
jason.woltje 767bb62c7d docs: close remote credential and private review access findings 2026-10-22 21:06:29 -05:00
jason.woltje 6345aae3d8 docs: align editor graph, Markdown transition and archive with PRD d6 2026-10-22 21:06:29 -05:00
jason.woltje ee5f7b9ef8 docs: align O2 chapter conversion and Case 2 access with owner rulings 2026-10-22 21:06:22 -05:00
jason.woltje 0b923a2759 MVP readiness briefs (mos-1371/1372): FED R6 #1800, FED R4 #1801, FED R7 #1802, GitHub grant method #1803, Q228 grant scope #1804, 574 #1805, 573 #1806; docs only 2026-10-22 20:32:37 -05:00
jason.woltje b0a6838ca6 SESSIONS: Filbert Q195 O2 S0 review (#1798, c30914); doc only 2026-10-22 19:05:06 -05:00
jason.woltje 0626464874 Q195 O2 W4 (#1799): log lines for the W4 design and review landing, row 203 and O2 landing; docs only 2026-10-22 18:55:11 -05:00
jason.woltje 66299eeef7 Q195 O2 W4 (#1799): Megavolt's round 4 review of 994c3f7fb (blob 2af460ab7): APPROVE, R3-B1 and R3-B2 closed, R4-N1 (re-migration after a pre-W4b restore) non-blocking for W4b; doc only 2026-10-22 18:54:51 -05:00
jason.woltje 569bb6b177 Q195 O2 W4 (#1799): Megavolt's round 3 review of 54246cb32 (blob 0c4f55826): CHANGES, R2-B1 projection and R2-B2 closed, R3-B1 (reserved notes keyed on negative IDs) and R3-B2 (fence initial state and confirmation scope undefined); doc only 2026-10-22 18:54:51 -05:00
jason.woltje fb404c909d Q195 O2 W4 (#1799): Megavolt's round 2 review of 019a0ea5b (blob 14dfe92d9): CHANGES, B1-B9 and M1 closed, R2-B1 (paragraph-mark revisions, reserved-note leaves) and R2-B2 (release backup/restore keeps O2 text and revives revocations); doc only 2026-10-22 18:54:51 -05:00
jason.woltje 4c0a2c4fd4 Q195 O2 W4 (#1799): Megavolt's review of the DOCX citation design at 15e91538f (blob 662abdb7c): CHANGES, B1-B9 and M1 (Case 2 narrowing in O2-context turns goes to Mos/Jason); doc only 2026-10-22 18:54:51 -05:00
Rocko 2f06aad3f0 docs: define reserved notes and initial O2 restore fence 2026-10-22 18:54:51 -05:00
Rocko 6863d4c852 docs: specify O2 projection marks and restore revocation fence 2026-10-22 18:54:51 -05:00
Rocko 9ef5798acb docs: close W4 locator and access review gaps 2026-10-22 18:54:51 -05:00
Rocko 4897520c31 docs: define committed DOCX citation and access contract 2026-10-22 18:54:51 -05:00
jason.woltje 4ca2703079 queue: revs 3360-3368. Row 203 (#1701): Sage brief re-pin to 8adfb261f (3360, Webby N2), Quackerjack claim, in-review r2 and outcome (3361-3363), Sage request as jarvis (3364-3365), Webby approve record (3366), Sage landing note and done (3367-3368). 2026-10-22 18:54:10 -05:00
jason.woltje 25c09f19fe docs: name save-group settings and enforce adoption gates 2026-10-22 18:51:33 -05:00
jason.woltje a6da8718c4 docs: fix O2 review gates, shipping and save semantics 2026-10-22 18:51:33 -05:00
jason.woltje ee26cfbada docs: redesign Q195 for full WordCraft O2 editor 2026-10-22 18:51:33 -05:00
jason.woltje a4da4e6df4 row 203: gate results on the rebased tip (c7b754d7b): stack-release 219/0, test-config 51/0, mutants 17/17, cli 895/0, ten scripts green with named skips; code tree unchanged from 7309260ae 2026-10-22 18:39:28 -05:00
jason.woltje 7309260aeb row 203: gate record on the rebased tip (c7b754d7b): blob table and timeout table, written before the first run 2026-10-22 18:01:35 -05:00
jason.woltje 7c543fd4a1 row 203 FED R2: cases for missing self, peer id and label (webby-0191 B3)
The pattern test coerces an absent value to the string 'undefined', which
matches, so the typeof halves are the only refusal for a missing value. Adds
a case for each, and M15-M17 in BUILD.md; keeps the config-console-key check
name on purpose. Not run yet.
2026-10-22 17:57:25 -05:00
jason.woltje 284d55cf2c row 203 FED R2: stack-release probe reads federation; a refusal case per federation check
stack-release config_checks refuses a target whose loader refuses a key the
config holds; it looked only for console, so a federation-only config passed
to a pre-203 copy. The probe now reads console or federation. The fixture's
B loader replace follows the new rejectUnknownKeys list, which the federation
key changed.

Adds a test for each federation refusal in test-config.sh (six uncovered,
plus trailing slash and duplicate peer id), and one stack-release test for
the target-loader refusal. Mutants M7-M14 in BUILD.md. Not run yet.
2026-10-22 17:57:25 -05:00
jason.woltje 93a491d732 row 203 FED R2: federation key in the system config (#1701)
The loader validates an optional federation block: self and peers[].id
labels, peers[].origin as an https origin with no path, query or userinfo,
peers[].label non-empty and at most 80 characters, peer ids unique and
distinct from self. Unknown sub-keys refuse. Labels and URLs only; the key
grants no trust. Absent key: config loads with no federation output.

Tests in scripts/test-config.sh cover each refusal and the no-key case.
Notes and mutants M1-M6 in agents/quackerjack/work/queue-203/BUILD.md.
Not run yet: the gate runs on CLEAR.
2026-10-22 17:57:25 -05:00
jason.woltje c7b754d7b1 queue: revs 3339-3359. Row 292 (#1797, Gander) added and briefed (Sage, 3339-3340). Row 291: Gosalyn claim and in-review r1 (3341, 3344-3345), Sage request as jarvis (3353-3354), Webby approve record (3357). Row 207: Megavolt in-review (3342), Sage abandon and request as jarvis, resolved on c30841 (3347, 3349-3350), Duckworth approve record (3356), Sage landing note and done (3358-3359). Row 270: Gizmoduck claim and in-review r3 (3343, 3346), Sage abandon and request as jarvis (3348, 3351-3352), Duckworth approve record (3355). 2026-10-22 17:53:26 -05:00
jason.woltje a94c95522f docs: row 207 landing records (SESSIONS, BUILD-LOG, Megavolt's lines), DEFERRED 207 N1 (duckworth-0230) and 270 N6 bound to row 272 (duckworth-0228) 2026-10-22 17:51:32 -05:00
jason.woltje c4f77373f9 row 207 (#1705): BUILD.md Runs records the gate on 712f79c0a (sage-3041): access.test 31/31, M1-M8 killed on their named tests, cli 894/894, ten scripts green under the mos-1275 limits, and the stopped first run's kill-check correction; doc only 2026-10-22 17:28:04 -05:00
jason.woltje c7e77202d6 row 207 (#1705): BUILD.md records sage-2810 (Q1 no reload, cited to broker.mjs:384; Q2 refuse with user-has-grants) and the usage/help pin sweep: access.test.mjs is the only reader, no hash pin; doc only 2026-10-22 17:28:04 -05:00
jason.woltje a0094633f6 row 207 (#1705): access.test.mjs T1-T9 for user add|remove; BUILD.md pins M7/M8 as edits and records the row 275 path check (bus only, morgana/row275 empty); not yet run, waits for Sage's CLEAR (sage-2790) 2026-10-22 17:28:04 -05:00
jason.woltje 721268b084 row 207 (#1705): mosaic access user add|remove on R5's path (proof, lock, revision, checkAccess, receipt user.add/user.remove); remove refuses user-has-grants; --level viewer already in R5; BUILD.md records Q1-Q5 (no access.reload, R4's) and the tests and mutants; tests held for the row 275 manifest check (sage-2790) 2026-10-22 17:28:04 -05:00
jason.woltje f0e3dcdf41 plans: brief for the config_checks follow-up (row 270 rollback hazard, duckworth-0226, sage-3038) 2026-10-22 17:11:21 -05:00
jason.woltje 01983cd3f1 queue: rev 3338. Row 224 waits on row 216 (Sage, mos-1351: della/row224 carries row 216's commit 4f3fbc25a). 2026-10-22 17:06:42 -05:00
jason.woltje 626c19e78c queue: revs 3325-3337. Row 271: Gizmoduck claim and in-review r3 (3325-3327), Sage review request as jarvis (3328-3329), Duckworth approve record (3330). Row 276: Gander in-review r1 (3331-3332), Sage review request as jarvis (3333-3334), Duckworth approve record (3335), Sage landing note and done (3336-3337). 2026-10-22 17:04:08 -05:00
jason.woltje f32b867f34 docs(row276): record green run5 gate and all seventeen assertion kills 2026-10-22 16:59:44 -05:00
jason.woltje 826e8fd7ff docs(row276): refresh mos-1350 baseline cleanup packet and limits 2026-10-22 16:59:44 -05:00
jason.woltje e06130fc01 test(case): require baseline session cleanup per mos-1350 review 2026-10-22 16:59:44 -05:00
jason.woltje 466562ba03 docs(row276): pin mos-1350 baseline candidate and timing audit 2026-10-22 16:59:44 -05:00
jason.woltje 931fab079a perf(case): remove duplicate head validation per mos-1350 2026-10-22 16:59:44 -05:00
jason.woltje 135bb8fae2 test(case): isolate P10 timing with per-mode baseline per mos-1350 2026-10-22 16:59:44 -05:00
jason.woltje cf228eabc9 docs(row276): record run4 stop and mos-1349 timing pairs 2026-10-22 16:59:44 -05:00
jason.woltje 41f8bd1972 docs(row276): packet stdout spool and M17 per mos-1345 2026-10-22 16:59:44 -05:00
jason.woltje 47906e4fde fix(case): isolate legacy stdout after outer timeout per mos-1345 2026-10-22 16:59:44 -05:00
jason.woltje c2b845c76d docs(row276): preserve run3 whole-file stop for placement 2026-10-22 16:59:44 -05:00
jason.woltje 74635fc608 fix(row276): correct multi-file driver path per mos-1337 review 2026-10-22 16:59:44 -05:00
jason.woltje 257003ba1a test(row276): record mos-1337 N5 delta and both-copy mutant 2026-10-22 16:59:44 -05:00
jason.woltje 537ce2dd4a fix(case): silence vanished stat opens per mos-1337 N5 2026-10-22 16:59:44 -05:00
jason.woltje 3b90428021 docs(row276): record run2 stop for Mos placement 2026-10-22 16:59:44 -05:00
jason.woltje 859ae37083 docs(row276): void setup-red run and pin first fixture correction 2026-10-22 16:59:44 -05:00
jason.woltje 88179d6de4 test(row276): define read helper for new legacy fixtures 2026-10-22 16:59:44 -05:00
jason.woltje ccfa17eefc docs(row276): integrate schema3m base and record dependency drift 2026-10-22 16:59:44 -05:00
jason.woltje b5d9efa1ba docs(row276): pin integration on 94c33a43c and preserve upstream docs 2026-10-22 16:59:44 -05:00
jason.woltje 69979bb8a8 docs(row276): record static approval and upstream deferred drift 2026-10-22 16:59:44 -05:00
jason.woltje 155f643224 docs(row276): pin supervised-call candidate and fifteen mutants 2026-10-22 16:59:44 -05:00
jason.woltje b96b4e303d fix(case1): supervise legacy user calls across sudo sessions 2026-10-22 16:59:44 -05:00
jason.woltje f2735bb6a1 queue: revs 3317-3324. Row 206: Launchpad claim and in-review (3317-3319), Sage review request (3320-3321), Duckworth approve record (3322), Sage landing note and done (3323-3324). 2026-10-22 15:14:46 -05:00
jason.woltje 0cbe2e9b35 row 206 (#1704): BUILD.md Runs, gate GREEN on 9538e0fc8 (bus 850, cli 869, shell suites, M2-M9 and Q2 killed) 2026-10-22 14:40:53 -05:00
jason.woltje 9538e0fc86 row 206 (#1704): thread-shares trail test threads the reply into A's default session, not the thread id (first-run red, mos-1272) 2026-10-22 14:19:00 -05:00
jason.woltje 1a228f395f row 206 r4 (#1704): BUILD.md records the slot rebase onto 17f415792 (269, 282 landed) and the drift sweep 2026-10-22 14:16:21 -05:00
jason.woltje 8295622600 row 206 (#1704): BUILD.md records duckworth-0179/0180, C3 met on the pushed tip 670658dd1 2026-10-22 14:15:48 -05:00
jason.woltje ac0e4dc9bc row 206 r3 (#1704): BUILD.md records the C3 rebase onto 275 at 3410df7c3 on 253's 48e450ce8, and the sweep 2026-10-22 14:15:47 -05:00
jason.woltje c49fdb2692 row 206 (#1704): BUILD.md records duckworth-0175 (round 2 meets C1/C2, APPROVE carries) and C3 2026-10-22 14:15:47 -05:00
jason.woltje cfb86f1bd7 row 206 r2 (#1704): rebased on 275 r3 957a9bfe0; share verbs need collaborator, viewer reads a share (duckworth-0169 C1, C2) 2026-10-22 14:15:47 -05:00
jason.woltje 178eefd5c4 row 206 (#1704): BUILD.md records Duckworth's manifest APPROVE (duckworth-0169), C1/C2 for the 275 round, N1-N4 2026-10-22 14:15:47 -05:00
jason.woltje 8a29f94771 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 14:15:47 -05:00
jason.woltje d78a9cc83c 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 14:15:47 -05:00
jason.woltje 17f4157920 queue: revs 3217-3316. Q226 gap (rows 216-229, 265-267 re-pins; 217 unblocked; 223 blocked; rows 282-290 added), rows 253, 275, 269 and 282 reviews, notes and done moves, row 291 (PRD 1.16, #1796) added, claims and in-review moves for 173, 207, 211, 214, 224, 239, 252, 258, 276 (sage, darkwing, della, dewey, gander, herb, honker, megavolt, scrooge, ludwig, gizmoduck, gosalyn, webby, duckworth, morgana); review requests posted as jarvis by Sage 2026-10-22 14:13:04 -05:00
jason.woltje 647424442f gosalyn: PRD 1.15 round 2 for Webby webby-0158 B1: requireCitation makes every reply carry a citation except the runner's four fixed notices, as the design keeps them passing, and the REQ-LIB-1 rewrap joins organization file to its sentence (N1) 2026-10-22 14:06:25 -05:00
jason.woltje 71fed6281c gosalyn: PRD 1.15 rewrites REQ-LIB-6 to Q223 and Q226 (mos-1320 item 8): agents never write story and write .md files only in supporting folders a human marks, each as a commit by their own Gitea account with a notice she can revert, chat is free text, any citation is checked, and requireCitation is a human-only setting off by default. REQ-LIB-1 and REQ-LIB-2 gain supporting folders and the canon-cite rule. The 1.12 note's residual becomes the supporting-doc residual, and the 1.15 note records the old texts and lists REQ-LIB-3, 4, 5 and 7 as unchanged. requirements.json version 1.15 with three titles changed, and the test's version pin. The id count stays 85 2026-10-22 14:06:25 -05:00
jason.woltje 86a665bdf0 docs: PRD 1.16 brief, library.write routes under REQ-LIB-6 (#1796, mos-1328) 2026-10-22 13:51:55 -05:00
jason.woltje efcc9ea8dd row 269 (264b, #1774): BUILD.md M11 kill line is the survivor log assertion at session-launch.test.mjs:309, not the outcome assertion (mutant run 17:49Z, duckworth-0181) 2026-10-22 13:37:34 -05:00
jason.woltje f39508c24f row 269 (264b, #1774): the M6 halt test halts after mayLaunch answers
A halt inside the op reaches the bus's own fence first (runtime.mjs:564-566,
processingWork at :352-356), so the launcher refused limit-check-failed
(launcher.mjs:389-391) before its recheck admission() at :397. First-run red of
a new test on 6d9ab3503 early signal, placed under mos-1272.
2026-10-22 13:37:34 -05:00
jason.woltje d20ef317fc row 269 (264b, #1774): BUILD.md names the sessioned-survivor residual in Q2 and records N1's mos-1292 allowance (duckworth-0159 N3, N4) 2026-10-22 13:37:34 -05:00
jason.woltje 6e50bd6304 row 269 (264b, #1774): BUILD.md Q2 notes that a survivor's released claim doesn't prove its run has exited, and the per-leftover 5 s wait (duckworth-0158 N1, N2) 2026-10-22 13:37:34 -05:00
jason.woltje 6c80b12904 row 269 (264b, #1774): BUILD.md answers sage-2750 on the recover() change; the survivor log names its pids
The section "recover() change, for a design read" covers three things, with
file:line against aca16404c. Every leftover is still bound and ended as
host-lost, and a survivor only refuses legacy-run-live while it lives. The
5 s wait applies only in the bwrap kill branch. No old test's expected value
changes. The fallback is to revert to the base recover() and leave the
refusal untested. The survivor log line now names the outer and runner pids
so an operator can clear it, and the new test's log assertion matches.
2026-10-22 13:37:34 -05:00
jason.woltje 42fd3edd45 row 269 (264b, #1774): legacy-run-live gets its test (duckworth-0156)
recover() kept a survivor only when a bwrap outer pid outlived SIGKILL, so
the Q2 refusal (BUILD.md:177-179, t1-sessions.md:109) had no test. A leftover
whose runner still ran after its outer pid was gone was ended and dropped.
Now recover() waits on the outer pid and the runner, keeps any leftover where
either still runs, and logs that it still runs. check() reads only the
survivors that still run.

session-launch.test.mjs adds a test where a sessionless leftover's runner is
a sleep child and its outer pid is gone. It asserts the legacy-run-live
refusal with no launch and no session workspace, then one launch once the
child exits. BUILD.md adds mutants M10-M12 and replaces the "no test" note.
2026-10-22 13:37:34 -05:00
jason.woltje dc5836c7df row 269 (264b, #1774): refusedOnce in sessions.test.mjs pins actor_run, closing Duckworth N1 (DEFERRED.md:474-480)
Allowed by Mos in mos-1292 (copy mos-1294) on mos-1285's terms. The SELECT
reads actor_run, the expected row gets the run-name literal, and the three
callers pass `${name}-run`. Nothing else in the test changes. The old
expected row was:

  [['action.refused', { code: 'not-holder' }]]

DEFERRED.md gets a closing line on the entry naming row 269.
2026-10-22 13:37:34 -05:00
jason.woltje 2133c5a0c6 row 269 (264b, #1774): per-session store, workspace and single-writer lock
threadsState gives one row per session with `session` and `legacy`; threadIdle
idles a run against its bound session. Each session gets its own store
(threads/<b>/<t>/sessions/<s>/, or the thread's old store for a legacy-3l
session) and its own workspace (session-workspaces/<b>/<i>/<s>/), held by an
O_EXCL <ws>.lock from before the clones to the run's proven exit. A legacy
session's workspace binds in-sandbox at the old instance path. sessions.json
is v2 and reads v1. New tests: cli session-launch.test.mjs, seat
session-workspace.test.mjs. Not yet run: tests wait for Sage's CLEAR.

Two old assertions change as exact updates:
- mos-1285: end-launch.test.mjs threadsState deepEqual. The old row shape was
  { thread: 'th-1', instance: 'coder', pending: 0, unread: 0, lastMessage: null,
  lastOpen: '2026-10-15T00:00:01.000Z', lastClaim: null, holder: null }; the
  row keeps every key and value and adds session: 'th-1', legacy: true.
- mos-1289: thread-launch.test.mjs:182, old line
  `const store = join(f.dataRoot, "threads", "acme", A);`
  now reads A's default session id from threadsState's `session` key; :183-185
  unchanged; one added assert that threads/acme/<A>/session is absent.
2026-10-22 13:37:34 -05:00
jason.woltje 194dec83d6 row 269 (264b, #1774): BUILD.md lists thread-launch.test.mjs:182 as a second old assertion that changes. A thread.create thread's default session isn't legacy-3l, so its store takes D2's layout (t1-sessions.md:100, :294). Proposed as an exact path update, for Mos through Sage 2026-10-22 13:37:34 -05:00
jason.woltje 84b32e3903 row 269 (264b, #1774): BUILD.md drops the planned bindLaunch session-running refusal. Two old tests bind a second unended run to one session and expect the claim refusal (threads.test.mjs:88-89, sessions.test.mjs:119-120), so the broker half of the fence is the existing per-session claim, thread-already-held (broker.mjs:4815) 2026-10-22 13:37:34 -05:00
jason.woltje 9d241fdcc2 row 269 (264b, #1774): design notes for Duckworth's early look (duckworth-0150). C1 adds the Console and R-guard sessions.json readers, the 264a refusals a v2 file causes, the /proc live check and the Console restart; C2 pins M6 to a generation change after check() with all eight start() admission calls named; C3 gives the realpath compare its own code and a symlinked workspaces root as M4's input; C4 lists the old threadsState assertions, end-launch.test:193 the only change, under mos-1285 2026-10-22 13:37:34 -05:00
jason.woltje 76a6524cfe row 269 (264b, #1774): design notes for sage-2717. Rollback names the four steps past the 264a exit-3 refusal and the re-pin plan section that carries them; new M8 binds a non-default session at the legacy path, old M8 becomes M9; Duckworth's legacy-bind check 2026-10-22 13:37:34 -05:00
jason.woltje 693f7e745e row 269 (264b, #1774): design notes. D9 audit of every sessions.json reader at 84f53540d and of threadsState's consumers; sessions.json goes to v2 because recover() rebinds without session; pi 0.85.1 resumes by cwd, so the legacy session keeps its in-sandbox path; mutants M1-M8 and run limits (sage-2707) 2026-10-22 13:37:34 -05:00
jason.woltje 94c33a43c0 docs: row 275 landing line; Researcher session line 2026-10-22 13:13:47 -05:00
jason.woltje a870fd85bb docs: seat log lines for rows 281, 224, 219, 214, 211 and 258, the row 253 landing line, Sage's SESSIONS line, and DEFERRED for the 224 residual and the 206 sharee list 2026-10-22 13:10:27 -05:00
jason.woltje 5b1f1dc885 docs: DEFERRED gains row 253 N1 (Vikunja 532, mos-1297), row 198 N2 (/api/seen 400, sage-2867) and the stale row 203 text in access.mjs (Quackerjack) 2026-10-22 13:10:27 -05:00
jason.woltje 8c638b6f66 agents: register Della, Scrooge, Huey, Louie and Ludwig as seats so the queue can name them as row owners (Q220, mos-1301) 2026-10-22 13:10:27 -05:00
jason.woltje a700992cd4 docs: 193b2 brief, the live --migrate window needs Mos's go under Q173, not Jason (mos-1310 item 4) 2026-10-22 13:10:27 -05:00
jason.woltje d30f35e292 docs: SESSIONS line for Gizmoduck, rows 264a and 268 after-landing docs and the row 269 plan 2026-10-22 13:10:27 -05:00
jason.woltje e5b7d1d083 row 268 (#1773): BUILD.md Runs records the r2 gate on ecd9b35eb and the r3 and r4 carry reruns with every count and skip; doc only (sage-2455) 2026-10-22 13:10:27 -05:00
jason.woltje 836b2f25ca docs: preserve email hard-wait approval and tune parser limits 2026-10-22 13:10:27 -05:00
jason.woltje 73516da853 docs: apply Q217 and Q222 to email source settings 2026-10-22 13:10:27 -05:00
jason.woltje 95e3f71661 docs: address Q195 editor design review findings 2026-10-22 13:10:26 -05:00
jason.woltje cde0cd0832 docs: Q195 writing editor design and Q196 options 2026-10-22 13:10:26 -05:00
jason.woltje 100f018af3 gosalyn: row 229 design round 2 for Webby webby-0148: the settings.json allowlist is three keys a fresh /login writes, written in Mosaic code with httpProxy and the path and command keys refused by name and a sha256 bump test on pi's setter names, models-store.json and keybindings.json now refuse, auth.json.lock is the only allowed dir, pi-login runs --offline and --no-session in an empty temporary cwd with a 229b allowlist test, the contents check runs before preflight, and creativeHumanOnly covers the fallback (R1, R2, N1-N5) 2026-10-22 13:10:26 -05:00
jason.woltje 4a8215e3a1 gosalyn: row 229 design draft 1 after mos-1306: Chet's route has no fallback by default with a human-only fallback still possible through routes, and the dates note records that the live check gates every build row, not the design 2026-10-22 13:10:26 -05:00
jason.woltje 669e680368 gosalyn: row 229 design draft 1, a per-instance pi agent dir for Chet's Codex login: the host-made 0700 store per instance, a same-path rw bind with PI_CODING_AGENT_DIR through the pi module env, a contents allowlist since pi reads models.json and settings.json there, a refusal for roles with bash, a no-refresh auth check preflight, no fallback for Chet, a fresh-grant mosaic pi-login, the live check that gates the build, and rows 229a and 229b (#428, #1730) 2026-10-22 13:10:26 -05:00
jason.woltje c06ccc9ba4 gosalyn: Case 2 C2-03b round 3 for Webby webby-0156 R4: the fetch fallback reads the append-only events table for any GRANT_EVIDENCE event with method gitea and agent the chief, never gitea_grants, whose row a revoke and a grant-lost delete. It names no project, since grant.applied and grant.revoked carry none, so a chief grant on any project ends the fallback and fails safe. The revoked and fired tests go through grants.revoke and instance.fire and assert the row is gone before the fetch 2026-10-22 13:10:26 -05:00
jason.woltje 136e5c1ca3 gosalyn: Case 2 Q226 row briefs with Webby's round webby-0151, Mos's mos-1320 and Sage's sage-2833 folded in. C2-06 rewritten and taking in C2-07: an unreadable business file resolves requireCitation to true (R2), a send over 64 KiB gets the one retry and then a fourth fixed text (R1). C2-03b falls back to slot library-gitea only while no chief grant record has ever existed and refuses library-fetch-no-grant after (R3), with library-human-missing for her save and revert. C2-02 after C2-05c, revert and save by her editor (N1). C2-21 cites mos-1320 item 2 and keeps the host-creative-role refusals, and records two Gitea facts that don't gate it (N4, item 5). C2-08b names what the chief's credential needs (N5). C2-05b restored to the brief row 224 built, which lands as built, and new row C2-05c carries the supporting scope and blob. A PRD row brief for REQ-LIB-6 (mos-1320 item 8). New briefs C2-01d, 05c, 08b, 03b, 21, 02, 11b and 12b 2026-10-22 13:10:26 -05:00
jason.woltje cf17ab387b gosalyn: C2-06 brief Q222 round 2 for Webby webby-0144: M11 compares against the on-disk human and replyLimits on document and written and refuses agent actor shapes, an unreadable business file checks at the defaults, keys read by Object.hasOwn and Number.isInteger, the verb refuses unknown keys, the new tests, the residual line, cites at aca16404c, and the hold moves to Q30 (mos-1305, design f164dcef8) 2026-10-22 13:10:26 -05:00
jason.woltje 09948da187 gosalyn: C2-06 brief makes every M5 limit a replyLimits setting with Q219's numbers as defaults, the human's library.limits.set and mosaic limits replies, the M11 writer check, read at check time with per-key fallback to the default, the setup sheet's settings line, the settings and M11 tests, After C2-04, and the build held for Q29 (Q222, mos-1302, design df29550c9) 2026-10-22 13:10:26 -05:00
jason.woltje 35615b01b7 gosalyn: C2-06 brief cites the setup sheet's reply limits paragraph at :115-118 (webby-0140 N4) 2026-10-22 13:10:26 -05:00
jason.woltje 3f1f4e44fa gosalyn: C2-06 brief owns the setup sheet's reply limits paragraph, which says 3 ideas of up to 120 characters and drops the lower-limits offer, and the tests paragraph is rewrapped (webby-0139 N1, N2, Q219) 2026-10-22 13:10:26 -05:00
jason.woltje 65b8c8a9bd gosalyn: C2-06 brief takes Jason's idea caps, 3 items of 120 characters per reply, tested one past each boundary, and the idea-text residual line reads 360 characters closed in Q219 (mos-1299, design 2852348d2) 2026-10-22 13:10:26 -05:00
jason.woltje 271cef205c gosalyn: Case 2 design round 3 for Webby webby-0156 R4: the fetch fallback reads GRANT_EVIDENCE events with method gitea and agent the chief in the append-only events table, never gitea_grants, and names no project, so it survives a revoke, a grant-lost and a fire and fails safe. The ruling paragraph, 7.2 and section 10 steps 4 and 4b say so, cited at aca16404c 2026-10-22 13:10:26 -05:00
jason.woltje 3e5db651b7 gosalyn: Case 2 design ruling after r11, agents never write story (Q223, Q226, mos-1309), with Webby's round webby-0151 and Mos's mos-1320 folded in. Supporting prefix lists agents may write .md files under through library.doc.write and library.doc.delete, fenced by M12 supportingPath, plumbing commits pushed fast-forward only with a Mosaic-Agent trailer, a notice and a human revert authored by her editor on slot library-human. Chat is free text with optional citations, replyLimits keys requireCitation and citationsPerReply, and an unreadable business file resolves requireCitation to true (R2). A send over 64 KiB gets the one retry and then a fixed text (R1). The fetch falls back to slot library-gitea only while no chief grant record has ever existed, and Jason deletes the slot once status shows the chief's grant (R3). A missing editor or slot refuses before any git call (N1), runner and tools cites fixed at aca16404c (N2, N3), two record-only Gitea facts at C2-21 (N4, mos-1320 item 5), mos-1163 narrowed to gitea library projects (mos-1320 item 2), M3 at eight or ten (mos-1320 item 4). C2-05b is row 224 as built and C2-05c carries the supporting scope and blob (sage-2833). New rows C2-01d, C2-05c, C2-08b, C2-12b and C2-21 2026-10-22 13:10:26 -05:00
jason.woltje e1c9c09935 gosalyn: Case 2 Q222 round 2 for Webby webby-0144: the M11 fence compares against the on-disk human and refuses agent actor shapes, checks document and written as M9 does, a failed business file read checks at the defaults and never skips, the fallback reads keys by Object.hasOwn and Number.isInteger with booleans, arrays and objects named, the verb refuses unknown keys, the ceilings residual line, and cites at aca16404c (R1-R3, N1-N3, N5, N6) 2026-10-22 13:10:26 -05:00
jason.woltje aaaf45576a gosalyn: Case 2 design ruling after r11, reply limits are settings: every M5 limit is a replyLimits key in the business file with Q219's numbers as defaults, changed only by the human's library.limits.set and fenced in organizationWriter.write as M11, read at check time, with a missing or invalid value using its default. C2-06 carries it and waits on Q29 (Q222, mos-1302) 2026-10-22 13:10:26 -05:00
jason.woltje cf6abca9c0 gosalyn: Case 2 design ruling after r11, idea items capped at 3 of 120 characters: M5, the B5 note, the 5.1 residual (now 360 characters per reply) and 10 step 8 closed by Jason; the rest of M5 unchanged, C2-06 builds and tests the new boundaries (Q219, mos-1299, Vikunja 465, ludwig-0014 Q4) 2026-10-22 13:10:26 -05:00
jason.woltje 3410df7c3b morgana: row 275 r3 (duckworth-0166): B1 bob asks create only, open and send on alice's own thread; N1 thread B holds a message and the viewer read asserts its id 2026-10-22 12:46:09 -05:00
jason.woltje 1e3db53436 morgana: row 275 r2 (mos-1311): downgraded viewer keeps send and open refusals with nothing stored in the R3 test; BUILD.md lists each changed assertion, old and new, with design 4.3 lines 2026-10-22 12:46:09 -05:00
jason.woltje 4dd85c6716 morgana: row 275 (193b1b, #1780): viewer verbs and board refusals. VIEWER_VERBS at any grant level, collaborator for thread.create/open/send per verb, BOARD_VERBS exported and refused to users, user-side governance pin, mutants 6 and 10; FED R3 viewer-refused assertions reversed per design 4.3 2026-10-22 12:46:09 -05:00
jason.woltje 23816cf285 row224: pace failed inline sync attempts (unrun) 2026-10-22 11:50:19 -05:00
jason.woltje 38e322ea01 row224: draft broker library lookups and typed tools (unrun) 2026-10-22 11:45:58 -05:00
jason.woltje 48e450ce8f docs(row253): pin mos1298 session assertion candidate 2026-10-22 10:53:50 -05:00
jason.woltje 14e4600617 test(row253): assert direct query PID equals SID (mos-1298) 2026-10-22 10:53:16 -05:00
jason.woltje a6c71e6d99 docs(row253): preserve fresh gate stop at M38 survivor 2026-10-22 10:49:46 -05:00
jason.woltje e1bc766d87 docs(row253): pin mos1297 fixture repair for review 2026-10-22 10:34:06 -05:00
jason.woltje 362ec674c9 test(row253): expose peer failures and pin reasons (mos-1297)
The peer-output fixture execs isolated system Python to write its exact bytes
and exit with peer-status. This avoids masking parser/status faults behind the
supervisor session check. All17 faults assert an exact reason with a delimiter;
every existing assertion remains. No product change or timing delay.

Old packages/cli/tests/case1-accept.test.mjs💯
const PYTHON_STUB = `[ -f "$HC/peer-output" ] && { cat "$HC/peer-output"; exit "$(cat "$HC/peer-status")"; }

Diagnostic under sage-2752 found probe-inner-session for malformed output and
nonzero exit. Mos confirmed the fixture mask and authorized this repair.
No tests ran after the repair; Duckworth review and fresh whole gate follow.
2026-10-22 10:32:29 -05:00
jason.woltje f5ef1c33a3 docs(row253): preserve single P10 diagnostic and reason mismatches 2026-10-22 10:27:24 -05:00
jason.woltje 2bef9124c4 docs(row253): preserve M21 survivor stop and static call trace 2026-10-22 10:10:42 -05:00
jason.woltje 34401e7c90 docs(row253): record green 27-file run and measured timeout basis 2026-10-22 09:46:35 -05:00
jason.woltje 4f3fbc25a0 row 216 (C2-05a, #1717): library index cache and lookup queries find, get, search, manuscript, universeInfo; get pages links, manuscript takes no chapter, search marks a too-long line, PRD 1.14 levels (rulings da9555ca2); unrun 2026-10-22 09:42:51 -05:00
jason.woltje 0604d91fcf docs(row253): pin final aca1 integration and focused file bounds 2026-10-22 09:28:15 -05:00
jason.woltje ed3bb376cd docs(row253): preserve doc sweep appends on corrected interim base 2026-10-22 09:26:31 -05:00
jason.woltje cd269e7337 docs(row253): record schema3l integration before final base correction 2026-10-22 09:26:31 -05:00
jason.woltje e75ef1b19f docs(row253): integrate 385a base and record dependency drift and bounds 2026-10-22 09:26:31 -05:00
jason.woltje 72b17562d9 docs(row253): integrate row191 base and refresh manifest 2026-10-22 09:26:31 -05:00
jason.woltje ad67d95955 docs(row253): record integration onto refactor 378a76b85 2026-10-22 09:26:31 -05:00
jason.woltje 3b142c9c69 docs(row253): record static approval and pre-confirmation residual 2026-10-22 09:26:31 -05:00
jason.woltje 950cf47f3e fix(row253): bound cleanup retries after initial inventory 2026-10-22 09:26:31 -05:00
jason.woltje cca9367168 fix(row253): supervise socket queries across sudo sessions 2026-10-22 09:26:31 -05:00
jason.woltje c8cca3a688 docs(row253): store repair patch without context whitespace 2026-10-22 09:26:31 -05:00
jason.woltje 62de848820 docs(row253): freeze C4 repair for static review 2026-10-22 09:26:31 -05:00
jason.woltje 6f320b4b0c docs: propose row253 C4 session supervisor and draft hop fake 2026-10-22 09:26:31 -05:00
jason.woltje 643d16528b test: prepare row253 socket fixtures and fix gate collisions 2026-10-22 09:26:31 -05:00
jason.woltje 730a78aa91 gander: design tester socket migration for host scripts 2026-10-22 09:26:31 -05:00
1790 changed files with 448667 additions and 1796 deletions

No files matched your search

+104
View File
@@ -5311,3 +5311,107 @@ Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version
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.
2026-10-21 21:24:47 UTC | Gander row253 #1758 design r1 | Investigation began21:12Z under sage-2162; recorded now. Before: case2 installer still renders tester TCP and case1 acceptance requires it. After: proposed design covers socket marker migration, actual configured dataRoot within unchanged hardening, target-uid metadata queries, kernel-peer acceptance observation and explicit same-uid residual. Initial inventory saved; no passing final static-check claim, product edit, test, host or live action. Duckworth design review precedes code.
### 2026-10-21 row253 implementation prepared, gate pending
Gander: after Duckworth-0100 design approval with C1-C5, prepared the two host-script socket migrations, fake fixtures, updated retirement docs and 33 unique change checks. Static syntax/diff and source inventory checks pass; behavioral tests have not run. Sage-2191 queues the requested full scope and will send CLEAR. No host, unit or live config changed.
2026-10-22 02:13:30 UTC | Gander row253 C4 committed candidate, gate pending | Sage-2335 explicitly directs committing runnable source and tests before the whole gate. The repair implements the approved after-uid supervisor with separate inner/outer bounds; the static packet is b64aa316a. Current repaired bytes have run no syntax check, test, probe or host experiment. Earlier static syntax results apply only to the old build. Results from 8a8df4171 are historical and do not carry. Duckworth static verdict and Sage CLEAR precede all gate execution. F5 is row276 / #1781 in DEFERRED.md.
2026-10-22 14:45 UTC | Gander row253 focused27 on finalaca16404c base | Fresh0604d91fc / manifest573c51cf:27 whole files604/0/0, no timeout/retry, case134/0 in99.281s, case243/0 in66.418s, stack-release includingT46221/0 in583.453s. N9 sandbox bind ran and passed, no focused skips. Seven source blobs unchanged; sweep0 run-created leftovers, pre-existing infrastructure receipts retained. Whole gate and M1-M40 await separate Sage CLEAR after Duckworth rebase check. No live action.
2026-10-22 14:46 UTC | Gander correction to the row253 focused27 entry immediately above | stack-release whole was218/0/0, not221/0. The27-file sum604/0/0 and duration583.453s are unchanged; raw files/results.json and SUMMARY.json give the exact per-file totals.
2026-10-21T16:04:23Z | Q195/Q196 writing editor design, #1752 / Vikunja #448 | Before: row 154 edits Markdown in a separate Docs view, with no selected-word thesaurus or document pins. After: design draft on gyro/editor-design compares three editor approaches, records WordNet source/license/size, folds the pane/pin model and specifies guarded context/proposal APIs, DesignCraft handoff and seven proposed briefs. Q196 changes the basis to a Word-like interface. Verification so far: editor/provenance checks 10 pass, 1 explicit rebuild skip (pinned esbuild absent); brief fields, unique headings and 11-line checklist checked. Product, queue and live state unchanged. WordCraft scratch evaluation is being incorporated before publication. Review and Jason choices remain open.
2026-10-21T16:22:36Z | Q195/Q196 design verification, #1752 / Vikunja #448 | Folded Fenton 7caf8008b and Louie 880ef19e0. Recommend bounded O3: Word-like DOM editor plus isolated headless DOCX export, with explicit feature cut and semantic loss checks. WordCraft web 44,179,239 bytes, identical twice; measured comment/footnote conversion limits recorded. Seven briefs and 12-line checklist validated. Fresh editor/provenance: 10 pass, 0 fail, 1 explicit rebuild skip (esbuild 0.28.2 absent). CI queue guard identity gyro/purpose push returned documented degraded-pass (exit 0), CANNOT_ASSERT credential-unresolvable, scratch audit gyro-q195-ci-queue-audit.jsonl; no fallback or CI-green claim. Documentation-only candidate for Mos/Fenton review, no source, queue or live changes.
2026-10-22T16:06:34Z | Q195/Q196 design review fixes, #1752 / Vikunja #448, mos-1300 / fenton-0004 | Before: draft 105811400 had owner-only thread wording, stale source positions, two mismatched seat/model assignments, an assumed library save boundary and 12 owner questions. After: grant-aware pin validation and stale/revoked access redaction; references refreshed against aca16404c, including public/bus.js and README offsets; Sage assigns model-only seats; unimplemented library.file.save/C2-11b brief blocks book editing; tabs explicitly cover 62-76rem; J7 stricter context choice recorded; pronouns removed; six owner questions. Static docs/source-anchor checks only, no builds, product suites, MVP gate slot or live action. Fenton re-check pending; O3 and both companion folds retained.
2026-10-22T16:09:56Z | Della mos-1303 / Vikunja 460 Q217/Q222 revision | Before: Q1/Q2 open and Q3-Q7 phrased as Jason questions. After: Jason-created InMotion/woltje.com ingress recorded; per-source mailboxScope defaults none with human-only CLI then Console writer and named default/change/agent-refusal tests. Q3-Q7 settings/defaults/owners documented; no new external decision required for default development, conditional provider/spend/exposure list retained. PRD/grant/data.delete/live gates preserved. Docs only, no product suite or live operation. Earlier uncommitted append-only PRD review session receipts are preserved in this docs commit. Ludwig reviews before Sage landing.
2026-10-22T16:12:26Z | Della ludwig-0035 / mos-1303 / Vikunja 460 focused corrections | Verified REQ-WAIT-2/3 at db1e7732a: Jason approval is required to route mail.access out of hard waits. Restored explicit Jason-approved PRD gate; no Q211 waiver inferred. Named tunable parser/attachment settings, defaults and schema/test review ownership of resource bounds; E04 checks changed values and bound refusals. Clarified Q8 extraction stays Jason choice. Docs-only checks, no product/live action; focused review before Sage handoff.
2026-10-22 13:06 UTC | Megavolt | Row 281 (#1786) build. Before: a fire or delete left host_grants/gitea_grants rows, the host line and the Gitea token live. After: step G in broker.mjs revokes them in a detached job per agent (the host line first, then Gitea local-first with an admin preflight); a refusal is grant.revoke.refused plus a human message; open grant decisions are in the set; a raise for a fired agent refuses instance-fired. Tests T1-T14 (bus fire-revokes-grants, lifecycle-revoke, grant-executor, gitea-grant; cli lifecycle). Gate on 7979fb41e: base 121/121 + 24/24, 29 anchors, M1 exit check rc1/17, M1-M22 22/22 killed, dewey-0840 files 76+14+23, bus 819, business 231, cli 837, ten shells rc 0 under the mos-1275 limits; skips: test-task row 244 live-provider pair, test-queue canonical-root check. Corrections: the first base run on c174f37ac went red on five first-run test defects (b3, test only); the first mutant run on 06ee5ed6b hung on M1 because T6c's held loop never ended (b4a fixed the teardown, and the runner now has timeout -k 10 180 and an exit check without --test-force-exit).
2026-10-22T16:36:30Z | Della row224 C2-05b #1725 preparation | Before: C2-05a queries exist at 4f3fbc25a but no broker lookup verbs or typed tools. Scope: broker/tools plus Sage-authorized runtime/bundle integration, tests and docs. Existing human-only library reads retained; five agent queries require a live holder and trusted configured source. Code/docs only under row253 gate; no Node/test/push/claim. Runtime ownership coordinated with Rocko219/Huey272.
2026-10-22T16:52:55Z | Rocko | Row219 prepared under sage2793/2807; explicit creative thread/default-session selection, narrow metadata route/picker U1–U5,22 static mutant sites; unrun while253 holds.
2026-10-22T17:42:16Z | Sage | Row 253 (#1758, Gander, Duckworth) LANDED 48e450ce8. Before: refactor at aca16404c; case-sudo calls had no supervised session check and the M21 survivor hid behind a fast-exit fixture (mos-1297). After: a fast-forward of 22 commits; manifest 591020a1 rechecked with 0 mismatches; run 5 whole gate green with M1-M40 killed, the M38 PID equals SID assertion added under mos-1298; Duckworth APPROVE rev 3252, note rev 3253, done rev 3254; #1758 comment 30701 and closed; P19 416 comment 719. N1 (fast child reported as exit 85) is DEFERRED as Vikunja 532.
2026-10-22T18:11:57Z | Sage | Row 275 (#1780, Morgana, Duckworth) LANDED 3410df7c3. Before: refactor at 48e450ce8; viewers had no verb list of their own and board reads were not refused to users. After: a fast-forward of 3 commits; manifest 39847b15 (4 paths) blobs match; gate 17:52:12Z-18:05:00Z green (whole files 7/22/6, bus 834, cli 861, harness 151, business 231, seat 167, 10 scripts with 3 named skips), M6 and M10 killed; Duckworth APPROVE rev 3260 (comment 30717); note and done by sage-2897; #1780 comment 30728 and closed. N1 (board refusal test as a loop over BOARD_VERBS) is non-blocking.
2026-10-22 18:11:58 UTC | Gander row276 #1781 prepared, gate pending | Before: row253 left5legacy Case1 AS_USER calls unsupervised across sudo pty sessions. After: candidate uses its unchanged supervisor with per-call inner/outer budgets, separate streams, strict display-only failure trailer, fake-only paired process-model cases and15unique mutants. Duckworth0182 B1 corrected the plan before product code;0183 cleared build. Parse-only Bash checks pass; no Node, behavioral test, host, unit or config action. Runtime gate and publication await Sage.
2026-10-22T22:51:32Z | Megavolt | Row 207 (193c, #1705): rebased onto 01983cd3f as 712f79c0a; gate green under sage-3041 (access.test 31/31, M1-M8 killed on named tests, cli 894/894, ten scripts under mos-1275 limits). Correction: the first run's kill check grepped TAP "not ok" against node's spec output and was stopped and rerun. Runs note in 9bb406c6c.
2026-10-22T22:51:32Z | Sage | Row 207 (#1705, Megavolt, Duckworth) LANDED c4f77373f. Before: refactor at f0e3dcdf4; mosaic access had no user verbs. After: fast-forward of 4 commits, a cherry-pick of the gated 01983cd3f..9bb406c6c (range-diff 4/4 equal, patch-ids equal, the 4 manifest blobs equal; the base moved only by the #1797 brief, which no test reads; no open branch touches the manifest). Duckworth APPROVE c30849 rev 3356; done rev after the note. N1 (any-business refusal untested) DEFERRED. No live change. Correction: my first 207 review request ran under timeout 120, which killed it while it wrote the outcome; the comment (c30841) had posted, so I removed the dead lock and resolved it at rev 3350.
2026-10-22T23:03:39Z | Gyro Q195 O2 rewrite, #1752 / Vikunja #448, mos-1356 | Before: landed O3 design 95e3f7166/blob 4cb59999. After: Q231/Q233 full WordCraft O2 design on gyro/editor-o2 from refactor c4f77373f; canonical git DOCX, original Markdown/wiki preservation, human cadence default 0 with instant accepted-save commits, full rich-feature first slice. Ten briefs include independent source/dependency security clearance before any build queue entry, exact editor-only CSP approval gate, restricted source/build/bridge, separate reviewed DOCX citation gate and flags-only agent access. Existing Case 2 semantics unchanged; any integration requirement change goes through Mos to Jason. Static checks: ten briefs/seven fields, independent reviewers, unique anchors, current source/base/blob pins, docs scope and append-only logs pass. No builds, browser/worker runs, code, queue or live changes. Fenton review pending; CSP and implementation gates remain closed.
2026-10-22T23:03:53Z | Gyro Q195 O2 push guard | CI queue guard identity gyro / purpose push returned exit 0, documented degraded-pass: CANNOT_ASSERT credential-unresolvable. Audit: /mnt/storage/scratch/gyro-q195-ci-queue-audit.jsonl. No credential fallback or CI-green claim.
2026-10-22T23:09:38Z | Gyro Q195 O2 fenton-0006 fixes | Before: e012bab0b left stale-source ambiguity, incomplete migration/shipping ownership, undefined typing transactions and an unmeasured CSP gate. After: D12 explicitly routes superseded-source/C2 reply notices to Mos/Jason and refuses adoption before W4b/notice path; Q2 remains open. F2-F6 add schema-edge placeholders and restore-accounting files, shipped runtime data/writing closure with no sandbox store mount, per-chapter DOCX and 2s-pause/15s-span save groups, volume/latency/growth gates, split-run clickable wikilinks and historical Obsidian graph warning, and S0 isolated existing-artifact CSP measurement before exact-header approval. Static ten-brief/ownership/settings/checklist checks pass. No build/browser run, product/queue/live change. Independent re-check pending.
2026-10-22T23:09:50Z | Gyro fenton-0006 push guard | identity gyro, purpose push, exit 0 documented degraded-pass CANNOT_ASSERT credential-unresolvable; audit gyro-q195-ci-queue-audit.jsonl. No fallback credentials or CI-green claim.
2026-10-22T23:10:14Z | Gyro mos-1358 ruling recorded | Q36 / Vikunja 562 keeps C2 superseded notices OPEN with Jason. Markdown adoption refusal until W4b PASS is now a separate settled Mos build-order ruling, not a Jason question. D12 and Q2 distinguish them; no notice build authorized.
2026-10-22T23:13:27Z | Gyro Q195 O2 fenton-0007 F7/F8 | Named human-only editor.saveGroupPauseMs=2000 and editor.saveGroupMaxMs=15000 with integer/range/cross-field validation and W3a UI/CLI refusal tests. W3b owns production adoption refusal until W4b; isolated fixture conversion remains testable without request-flag bypass. W4b owns adoption module/API tests and reviewed lifting only with committed-reader/citation readiness and, if Q36 approves, the separate notice row PASS. Q36 unresolved stays closed. Static ten-brief, setting-key/bounds, refusal/lift ownership and two-question checks pass. Docs only; no build, browser/worker run, product/queue/live change. Fenton checks F7/F8 on next push.
2026-10-22T23:13:48Z | Gyro mos-1359 / r3 push guard | D13 records save-group timing as Mos's accepted reading of Q226 F41, not Jason's ruling; both defaults remain human-only settings. Push guard identity gyro/purpose push: exit 0 documented degraded-pass, CANNOT_ASSERT credential-unresolvable; audit gyro-q195-ci-queue-audit.jsonl, no credential fallback or CI-green claim.
2026-10-22T23:23:42Z | Rocko, W4 #1799 | Before, D24-D26 had no separate exact DOCX citation/access contract. After, 15e91538f proposed a versioned schema, parser/story/view/refusal rules, live authorization/sharing and generic/git/session bypass closure, compatibility gates and 22 acceptance rows. Two JSON examples parse, diff check clean; no code or implementation acceptance. Megavolt review pending.
2026-10-22T23:55:11Z | Sage, W4 #1799 docs landing | Megavolt reviewed four rounds (CHANGES 15e91538f, 019a0ea5b, 54246cb32; APPROVE 994c3f7fb, #1799 c30868/c30870/c30876/c30884). Landed here as cherry-picks: docs/plans/2026-10-22_q195-docx-citations.md blob 2af460ab7 and docs/reviews/2026-10-22_q195-docx-citations.md blob bedde1716, both new files. 32 acceptance rows go to W4b; R4-N1 (re-migration after a pre-W4b restore keeps the rotated fence and needs owner confirmation) goes into W4b's brief and A32. W4 is design-approved only: its gate still needs Sage's W4b implementation brief, which waits for S0 (#1798) PASS. Q36 open, adoption closed. Docs only, no test reads these files.
2026-10-23T00:52:58Z | Gyro Q235-Q237 policy rewrite started, Vikunja 571, mos-1366/1367 | Before: approved O2 e36bce352 keeps Markdown originals at their paths and Q36 notices open; landed W4 2af460ab7 has the same boundary. Imported that byte-identical W4 baseline as 1ac76a417, from refactor 062646487, to make the policy-only revision diff explicit. Prepared P1-P6/B1-B4: human one-way per-story import, human-set source-repo archive, DOCX current reader, scoped format map/archived non-canon reads, W7 conversion publication after W4b/approved PRD/10-27 and Jason first-real-book go. Q234 exact CSP, mos-1363 and S0 M1-M14 retained. W4 locator/extractor, shares and restore-fence blocks compare byte-identical to the approved baseline. Scope/source review ongoing; Mos impact map must be folded before Fenton handoff. No build/browser/worker run, code, queue, private-book or live action.
2026-10-23T01:37:07Z | Gyro Vikunja571 final impact-map / mos-1372/1375 fold | Prepared parent/W4 rewrite against brain-redux d183246 impact map at stack 062646487. P1-P6/B1-B4 and engineering Q1-Q16 are mapped to eleven briefs. D9 replaces the earlier private O2-only context/per-document/share model: normal Case 2 access in any authorized thread/room, supporting writes intact, changed policy awaits PRD 1.17 approval. New DOCX story creation has the same opening gate as human per-story import. Host-only push to human-set off-host remote replaces the no-remote ban; remoteURL defaults unset and same-host bare is disallowed. W7 gates on Hermes #423 step5, W4b, PRD approval, post-10-27 and first-real-book go; owns conversion events COUNTED/--events/pre-conversion restore reconciliation, manifest order/next_chapter, creative role cite wording, archive/old-path edits and format/link/citation routing. Store+off-host acknowledgment precedes archive commit/push, then catalogue publication; post-push failures remain resumable pending with pinned source. Current checks pass: complete brief fields, policy/map coverage, exact Q234 CSP, locator/projection mechanics byte-equivalent except explicit authority wording, A01-A35 IDs, immutable map pin, four-doc scope and append-only logs. W4 authority/remote/recovery sections reopened for Fenton; older APPROVE not reused. No product/build/browser/worker/private-book/queue/live action. Earlier entry describing preserved shares/fence applies only to the pre-D9 intermediate draft; D9 now withdraws those unimplemented structures.
2026-10-23T01:37:55Z | Gyro 571 push guard | identity gyro/purpose push: exit 0 documented degraded-pass CANNOT_ASSERT credential-unresolvable, scratch audit gyro-q195-ci-queue-audit.jsonl; no credential fallback or CI-green claim.
2026-10-23T01:41:59Z | Gyro sage-3121 A1-A3 PRD alignment | Before: 8d209016b adds a current Connections graph, excludes new Markdown stories in converted books and defaults archiveFolder. After: parent/W4 align with PRD1.17 d6 sha256 886b93c571f87bfbb2f27ee3113b6800a32d8f89196d95ace33566031bee380a: clickable editor links only; graph/tests later pending J-b; new different-path Markdown in a converted book remains canon until import, only recreated converted old paths are unmapped; archive folder is human-set in library mapping with no default, missing folder refuses conversion. Static A1-A3/11-brief checks pass; W4 locator/extractor/access/remote/recovery blocks unchanged. Local commit only, push held for Sage's named gap while Gizmoduck holds 270+271 run5. No product/build/browser/worker/queue/live action. Fenton gets local SHA through shared object store.
2026-10-23T01:49:08Z | Gyro fenton-0012 B1/B2 and N1-N6, mos-1379 | B1: off-host remote must deny the real shared jason.woltje seat-helper credential as well as seat identities until #431; private visibility/dummy-token denial is insufficient; W3b/A16/A26 require that future fixture proof. B2 follows mos-1379: human-only editor.agentReadComments and editor.agentReadTrackedDeletions both default false, comment/original/markup reads/resolves/index/context refuse when off; final access remains D9 and no role allowlist/per-document grant. N1 staging-ref allowlist and retained private refused/published audit refs; N2 URLs literal with fidelity-report loss and confirmation; N3 future paths marked new or predecessor row; N4 rename/sharing wording fixed; N5 historical thread text not refreshed; N6 protected VM1012 backup cited in P5. Static checks pass, locator JSON/package/projection mechanics retained. Local commit only; HTTPS push still held for Sage's named gap. No credentials tested/read/printed, no product/build/browser/worker/queue/live action. Fenton gets exact local SHA/blobs.
2026-10-23T01:53:25Z | Gyro fenton-0013 N7 mixed-format chapter order | Fenton approved local 0f4fa4f93. D10 now explicitly orders every chapter ID regardless of Markdown/DOCX format, inserts new canon Markdown by row216 or human committed order, and preserves ID/position at import. W7 tests cover mixed traversal/insertion/import stability. Static diff/append-only checks and docs hook pass. Local commit only, narrow re-check requested, HTTPS push still held for Sage gap. No runtime, queue or live change.
2026-10-23T02:21:06Z | Sage, rows 270+271 landing, 571 landing, MVP rows | Before: 270 (#1775) and 271 (#1776) were approved on gizmoduck/row270-271-r4 49cee846e and 2b964a082 after combined gate C2 run 5 GREEN on 53406f3e4 (bus 873, seat 202, cli 905, ten scripts, 38/38 mutants). After: 33 commits cherry-picked onto 767bb62c7 as 7328da8ca, range-diff all "=", 40 paths identical to the candidate; 270 manifest f4174458 matches, 271 manifest 3ab1f5c8 matches on 29 paths, the other 4 moved under 270 and match its manifest. Done revs 3396/3398, #1775 c30962 and #1776 c30964 closed, P19 463 c861. 571 (Gyro, Fenton APPROVE) picked as ee5f7b9ef, 6345aae3d, 767bb62c7 (blobs 9382f85b, 7ec0897b), 1ac76a417 left out; N7 911dad2aa (Fenton-0014) picked after. Rows 293-299 added (574, FED R6, R4, R7, GitHub grant, Q228, 573), queue commit 253346e3c. Correction: the push sync step checked out O..S paths over the canonical worktree without testing for uncommitted edits, so any pending SESSIONS or BUILD-LOG line in the canonical checkout at 02:06Z would have been overwritten; the sync now skips edited paths. Schema 3n is not live (LIVE 0cbe2e9b3 at 3m).
2026-10-23T03:59:52Z | Sage, row 293 (Vikunja 574) landing and 3m-3n live window | Before: live 0cbe2e9b3 at schema 3m; 574 approved by Filbert (c30982, cdda41f5) on gander/v574-r2 a9cae2bd6 after run 2 GREEN on 6bfab273b (four files 153/0, seat 202/0, cli 905/0, 312 flagged commits with 0 maintenance children). After: Darkwing's window put live on 464010fbe at 3n (activation 20261023T034557Z-464010fbe4c6, 26/97/5/28, Console 7332 200, W9b pm ra38826f3c6e2 left live, STATE darkwing/window-migrate-3n 8708927e5); 574's six commits cherry-picked onto 464010fbe as e51d11640, range-diff all =, patch-ids equal, only docs differ from a9cae2bd6. #1805 closed c31013, P19 386 c887 and 574 c888. Residual: queue store.mjs, discord git.mjs, harness tools.mjs commit without the maintenance flags; no whole-root test covers them yet.
2026-10-23T05:47:17Z | Sage, row 292 (#1797) landing, idle-stop race row 302, rows 300/301, row 294 rounds 2-3 | Before: refactor 4a9c536e8; 292's gate on 0d0021454 hit one red at compactions-launch.test.mjs:229, ['session-invalid',22,null] where ['stopped',0,null] was expected, product and test unchanged. Gizmoduck read it as a race: threadIdle releases the claim, then the launcher sends SIGTERM, and a runner call in between gets not-holder and exits 22. Mos (mos-1411) rejected loosening :229, let 292 count one whole unedited green cli rerun, and allowed one rerun for that exact signature on other gates until the race row lands (hits logged on Vikunja 588). The 10-run diagnostic passed 10/10, but one runner log (r846b8d083502) showed not-holder then exit 22. After: the cli rerun was 910/910. 292 LANDED 6aab979e6 05:18Z (7 cherry-picks, range-diff all =, patch-ids equal), done rev 3447, #1797 closed c31038, Vikunja 588 c891. Briefs 86d5ac69b (R6-N1 #1807, R6-N4 #1808, rows 300/301) and e3ef1eaf6/fdedc56e2 (race, #1809, row 302). Row 302 design d1/d2 approved by Dewey with D1 (role.claim refuses a released run), c31044. Mos ruled landRelease folded in, code launcher-released, and four old-test lines moved to exact values (mos-1414/1415/1416): threads.test:394, runner.test:771-772, land.test:464. Row 294 round 2 stopped at webui G5 granted-user.test:269: max(seq) over an empty events table is NULL. Round 3 is one line, coalesce(max(seq),0) (mos-1417), Duckworth APPROVE c31045 rev 3462, gate pending. Queue commit 756284e8e (revs 3448-3456). Correction: two review-request attempts this window ran without MOSAIC_GITEA_CREDENTIAL_FILE exported and failed (3450 Launchpad, 3451/3452 Sage); the posted request was 3453/3454.
2026-10-23T07:11:43Z | Sage, row 294 (#1800) rounds 3-4 and landing, row 219 edits 2-3, R9 transient and row 303 | Before: refactor 09d0d6faf; 294 run 3 on cb1648f3 stopped with 20 cli reds from two causes. A: the stack-release rehearsal fixture copies lacked cli access.mjs and transport.mjs, which 294's config.mjs now imports. B: the B1-B3 boot tests read warnings that now carry the fixture tracker line. Mos ruled A as two copy-list entries and B as a full pin, no filter: a TRACKER const and deepEqual (mos-1419). After: round 4 8616b9a4f, manifest a1ff5d67, Duckworth APPROVE c31053 rev 3469 with C1/C2. Run 4 from npm ci green: webui 365, cli 909, bus 873, 10 scripts, M1-M11 killed. Merged tree d3059c2c3 (11 cherry-picks, range-diff all =, patch-id 8371405a equal): stack-release-external 25/25, stack-upgrade 21/21. stack-release 223/224 on the first run, R9 at :2178: TypeError on join(r.dir) with r.dir null, so rehearse stopped before its directory and the output was lost. R9 passed 6/6 run alone on d3059c2c3, 09d0d6faf and 24b745447. Mos (mos-1422) allowed one unedited whole rerun: 224/224. 294 LANDED d3059c2c3 07:04Z, done rev 3471, #1800 closed c31068, P19 269 c895. Row 303 (#1810, Vikunja 589, brief ad6ca7930) adds an r.dir guard in R9 only. Queue commit 80793d824 (revs 3464-3473). Row 219: Rocko's second edit 7cbd839d0 (mos-1418, Dewey dewey-0883) restarted and went red at thread-launch C2-09a :489 on a legacy history path. The third edit 5f2ef92e5 (mos-1420, Dewey dewey-0885) reads the native session path. 219 now rebases onto 80793d824, because 294 overlaps its webui files. Correction: the first merged-tree check counted on the 292 drift and found an unrelated transient, and my P19 placeholder sed also rewrote the script's own guard line; the guard refused to post, and I restored the line before posting.
Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault run minus a separate baseline run, and a loaded whole-suite run went red. Mos approved direct timing from host /proc/uptime stamps with ambient load only (mos-1409). After: rebased onto 80793d824 as ffa93b054 (range-diff 3/3 =, manifest 277ace0b unchanged). Gate 07:26Z-07:50Z: case1 whole 3x 53/0/0, cli whole 917/0/0 (59 files, load 1.54-7.17), P10 inner 2.27 s, outer 4.01 s, M1-M6 killed. M4's planned guard return then exact outer-reason failure counts as its kill (sage-3271, mos-1426). Filbert APPROVE c31084 rev 3485, N1 M1 margin 0.02 s. 299 LANDED ffb2fedfb 08:03Z, done rev 3487, #1806 closed c31087; queue commit 8a35ffadb (revs 3474-3487). Row 303: Duckworth APPROVE c31079 rev 3480 with C1, the whole-file gate. Rows 219 (bfa421b6c) and 295 r2 (b16cef1d0) rebases: Dewey static clean. Corrections: Launchpad and Gander have no token, and the CLI refuses another seat's token file, so I posted both requests as sage (c31082, c31083). My sage-3265 wording led Duckworth to poll queue show into Gander's slot (07:20Z-07:26Z); I ruled it ambient load (sage-3274).
2026-10-23T10:48:40Z | Sage, rows 302, 295 and 219 landings, the Docker pool diagnostic, rows for Vikunja 590 and 591 | Before: refactor 1ac655005. 302 (#1809, Gizmoduck, Dewey) LANDED 3ed9fd754 08:49Z: 7 commits onto 1ac655005, range-diff all =, tree = gated merge 783602e06; queue commit 07802dc57 (revs 3488-3494). Row 219's gate went 11/3 in test-release.sh: release activate's health check failed because Docker could not create sage-diag1437_default, since all predefined address pools were used up. Under mos-1437 I reproduced it on clean 07802dc57 (11/3, the same stderr). I then removed 18 unused gate _default networks, and the rerun went 14/0 with the run-task marker ok. Mos accepted the removal and ruled that a probe that can create something waits for the go and a removal waits for the report (mos-1438). 295 (#1801, Morgana, Dewey) round 2 gate on d68b4300d (manifest e94c9aeb) was green. It LANDED 2efd127ce 10:10Z: 9 commits onto 07802dc57, HEAD~1 tree = gated a86592ffc. Done rev 3502, #1801 c31105, P19 414 c911. 219 (#1720, Rocko, Dewey): the restart on the post-295 merge 054da642f (manifest 4a2271dd, my mos-1439 call) passed node 2523/0/2 and shells 449/0, with 24 mutants killed. It LANDED 5fdfd7275 10:40Z (8 row commits plus packet-only evidence, tree e86a84a85 = gated). Done rev 3509, #1720 c31110, P19 416 c913. Queue commit 90633c864 (revs 3495-3510). Brief docs/plans/2026-10-23_gate-env-rows.md names rows for 590 (claude-session slug) and 591 (compose network cleanup), and DEFERRED.md gets 219 N1 (U6). Row 272 (Huey) conflicts with 5fdfd7275 in code, so it moves back to in-progress for a rebase. Its new fallback is bounded to a session.list access-refused on an omitted-session route (sage-3357). Corrections: my network create probe ran before Mos's go, and the removal ran before the report. My first slot-order line put the diag start at 09:44Z; it was about 09:41Z. My 219 push piped its output through grep, but the after-fetch confirmed origin. Queue commit 90633c864 also carries Huey's rev 3510 (272 back to in-progress), and its message names only 3495-3509.
2026-10-23T11:03:46Z | Gyro Vikunja571 mos-1444 PRD d8 follow-on | Branch gyro/editor-571-d8 from origin/refactor c16e943b9. Both designs now keep library.convert outside ACTIONS/ROUTED and broker processing, retain REQ-WAIT-1/hard-wait test unchanged, constrain unmarked story eligibility to mapped book/playground scope, and add whole-host pre-import restore recovery from remote import manifests plus the Gitea archive commit. Lookups refuse until one current version is proven; VM1012 restore needs Jason present under Q185. W7 no longer owns hard-waits.test.mjs. Canonical queue next gyro returned nothing; direct Mos assignment governs. Static diff, exact replacement and append-only checks pass. No code, tests, build, browser, private-file or live action. Fenton re-check requested after authorized branch push.
2026-10-22 | Huey | Row 272 / 264e (#1777) preparation on huey/row272, base 2d0d407ba. Before: bus session metadata and launcher isolation exist, CLI/Console expose only the saved thread default. Draft: session CLI, Console chooser with isolated drafts/cursors/receipts, metadata counts and safe-clear operation receipts against Gizmoduck's 0260 contracts; narrow deferred B6 reply fence. Tests authored, not run: Gander row 253 holds Sage's slot; no Node, claim, HTTPS git, push or live changes. This entry records preparation after the first draft, not a green acceptance result.
2026-10-22 | Huey | Row 272 draft, 264d additive contract gizmoduck-0272 (78e14352f). Added confirmed cancellation through the human channel, cancelled receipt retirement distinction, named refusals and routed-message cancellation output. Before: cancelled was not recognized as terminal; after: draft CLI stops polling and Console shows the before/after retirement outcome. Acceptance remains unverified: no Node/tests/claim/commit/push under Sage's gate. No runtime/lifecycle authority expanded.
2026-10-23 | Huey | Row 272 / 264e sage-3141 local rebase. Before: uncommitted draft on 2d0d407ba. After: same draft on new huey/row272-b23783f39 at b23783f39, with 270/271 landed (7328da8ca) and schema 3n. Product changes auto-merged; both append-only logs retain the full new base and original Huey additions. Checked landed session.list and clear/cancel/operation envelopes against the wired draft: no incompatible contract change. Historical unavailable-dependency cases stay as stand-ins; actual base supplies counters and lifecycle state. No tests/Node/claim/commit/push while Gander holds row 574 run 2. Existing-test fixture/assertion delta is named in the packet for Mos's ruling via Sage before execution.
2026-10-23 | Huey | Row 272 sage-3145 preservation correction. refs/stash is shared, so it was unsafe as sole draft storage. Pinned exact stash commit 07dc9d2f7944b4781b5e80dfa18425f4749f7a45 to local huey/row272-stash-keep and left the entry untouched. Sage explicitly directed committing the rebased draft before testing to preserve it: an untested local checkpoint, no acceptance claim. No Node/test/queue/network/live action. Separate claims and test CLEARs remain pending; old-test delta is with Mos via sage-3144.
2026-10-23 | Huey | Row 272 early static review follow-up, scrooge-272-early-read-huey-1. Before: a bad-session 404 erased sibling draft/recovery caches, malformed clear/cancel write receipts omitted the audit line, and another session's op key produced an ambiguous error. Draft after: purge only on named thread-level refusals, write one redacted malformed-receipt audit line, and identify key reuse without a write. New browser/HTTP/CLI tests authored, unrun under test hold; approved old-test hunks unchanged. Published 850b22680/round-1 manifest unchanged; follow-up is uncommitted, no acceptance claim.
2026-10-23 | Huey | Row 272 round 2 on row 219 landing 5fdfd7275, after 295. Replayed round 1 and B1/N1/N2; seven conflicts resolved retaining both log histories, both test sets, 219 instance chooser and 295 owner-settings-only transport. Static comparison confirms old tests differ only by mos-1388 approved hunks. Sage3357/Mos1442 authorize bounded granted-user legacy fallback: only session.list access-refused and omitted-session route; no new session grants or owner routing. RED first under3358 on new file only: 1 expected legacy assertion failure, 3 guards pass, 3.504s, TMPDIR mktemp/no underscore, no compose network; slot returned as huey0035. Then implemented fallback and added timeout/bus/malformed guards plus isolated-copy mutant runner, all unrun afterward. Candidate commit/push precedes Sage's full gate by3354; no green claim.
2026-10-23 | Huey | Row 272 round 3 per mos-1455 / sage-3435. Gate run 1 on b44332a93 RED: bus 891/0, business 231/0, CLI 940/1, plugins 110/1, webui 372/20; no mutants. Fixed only new CLI fixture role authority and three null-old render guards. Catching browser tests unchanged. manifest.r3 freezes the same 17 paths. Authorized pre-gate follow-up commit/push; no local tests, Node or queue writes; separate compactions dependency and Sage gate remain pending.
2026-10-23 | Sage (1ef1e4f8) | Row 272 landing (#1777, 264e) | Gate r4 on bea0c8e1c over 1e629c892: bus 891/0, cli 941/0, webui 392/0, business 231/0; plugins 110/1, the single red being row 65 harness.test.mjs:192 naming compactions.mjs, which row 307 owned and fixed. M1-M6 and F1-F4 killed, restore clean. Landed d33989230, queue commit 8dc132bec.
2026-10-23 | Sage (1ef1e4f8) | Row 307 landing (#1814, Vikunja 598) | Six first-parent commits replayed onto 8dc132bec with matching patch-ids, 16 paths equal to 6fdfe3790, manifest digest 17c9f8d9 rebuilt from git. Gate green 14:37:21Z: bus 891/0, cli 941/0, plugins 119/0, webui 392/0, business 231/0, every other package 0 failed 0 skipped, every scripts/test-*.sh passed (skips: test-queue canonical root, test-task live provider x2). M1-M4 killed, restore clean. Landed d243f6b25, DEFERRED 307 N1 0621c439e, queue commit fe4ee8c7b. Correction: my first note said Vikunja 598 done when I had only commented (951); Mos closed it (952).
2026-10-23T17:29:46Z | Gyro Vikunja571 sage-3542 Connections amendment | Branch gyro/editor-connections-design cut exactly from landed PRD1.17 30e6bb680; read REQ-LIB-5 in that PRD. Replaced stale J-b waits in parent and W4. Proposed W8 owns current authorized Markdown/DOCX links/backlinks list, labels, versioned refresh, leakage refusals and fixture tests. Build order under mos-1470/comment961 is W7 closed routing candidate, W8 landing, then W7 human first-real-import opening. No REQ-LIB-8 condition or acceptance change. Q234/mos-1363/S0 M1-M14 and post-10-27/MVP priority fixed. Static diff, twelve brief sections and append-only checks pass. No repo Node, tests, build, browser, queue mutation or live action. Fenton reviews after authorized docs branch push; Sage lands on approval.
2026-10-23T17:32:37Z | Gyro sage-3544 fenton-0016 N1/N2 delta | Before: W7 Gate/final real-conversion list omit the W8 build-order cross-reference and W8 human-view filtering cites agent settings. After: both locations explicitly put W8 landing before human opening as mos-1470 build order, not acceptance; comment/tracked-deletion links are not current story links and never appear in the human Connections view regardless of agent settings. New commit atop approved 5af060fb5, no amend. Static diff and append-only checks pass. Docs only, no repo Node/tests/live action. Fenton checks only this delta.
## 2026-10-23 — Row 288 C2-02 build starts (Bubba, #1793)
Before: agents can read the library; supporting-path and push-credential dependencies are separate unlanded heads. After intended: per-seat supporting writes/deletes with exact notices, human revert and one commit per Console save. Work is on bubba/row288, chained on original 283/285/286 commits; 284 tool base still pending. Plan and acceptance evidence: agents/bubba/work/queue-288/BUILD.md. Untested; no Node/test/build/gate run before Sage CLEAR. Landing waits for every dependency's own gate.
+13
View File
@@ -27,6 +27,11 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
| Gizmoduck | Federation design (#414) and builder of FED R1 | T3 session (Claude, Opus 5.5, thread 9312a896) | none, T3 only |
| Webby | Reviewer: Case 2 designs, rows 192 and 194 | T3 session (Claude, Opus 5.5, thread 03086006), started by Mos (mos-0981) | none, T3 only |
| Duckworth | Reviewer: federation designs, row 193 | T3 session (Claude, Opus 5.5, thread ffceafec), started by Mos (mos-0977) | none, T3 only |
| Della | Builder, email plan (Vikunja 460) and row 224 (C2-05b) | T3 session (Codex, gpt-6.1-sol, thread 66735a7f), started by Mos | none, T3 only |
| Scrooge | Builder of row 252 and reviewer of row 272 | T3 session (Claude, Opus 5.5, thread 629b6cd3), started by Mos | none, T3 only |
| Huey | Builder, row 272 (T1 sessions) | T3 session (Codex, gpt-6.1-sol, thread cabd45ca), started by Mos | none, T3 only |
| Louie | Builder and research, editor evaluation and row 174 | T3 session (Codex, gpt-6-luna, thread bfd13d49), started by Mos | none, T3 only |
| Ludwig | Builder and reviewer, post-v1 email PRD and row 258 (E01) | T3 session (Claude, Opus 5.5, thread 8615b2a6), started by Mos | none, T3 only |
| Sage | Project lead: coordination, review, integration; earlier DYOR strategy records retained | T3 session (Claude Code); Pi launcher `zai/glm-5.3:high` retained | `agents/sage/launch.sh` |
Each script supports `--check` and `--fresh`. Normal launches resume the agent's
@@ -42,6 +47,14 @@ its launch configuration remains fixed, including on resume. Use Filbert's
wrapper to select that configuration; a direct shared-helper invocation uses
its own supplied flags or the system defaults.
Any run that includes the webui suite, whether a seat's slot run or a landing
gate, sets `MOSAIC_REQUIRE_EDITOR_BUILD=1`, so a missing editor toolchain fails
the run instead of skipping the rebuild test. A worktree that links the
canonical `node_modules` also runs `npm ci --ignore-scripts --no-audit --no-fund`
in its own `packages/webui` first. The root install carries esbuild 0.28.1
through pi-coding-agent, and the rebuild needs the 0.28.2 that
`packages/webui/package-lock.json` pins (Vikunja 597, mos-1453).
All agents follow repository governance and current user direction. Team
leadership does not add deployment or push authority. Coordinate overlapping
work with Sage and preserve other sessions' changes.
+8
View File
@@ -0,0 +1,8 @@
# Beakley
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 217 (C2-06, handed over from Launchpad).
No launcher in this directory. The seat runs as a T3 thread (thread 88b97f65) and works in its own worktree. It has no
credentials of its own; it pushes only its own `beakley/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
+8
View File
@@ -0,0 +1,8 @@
# Boyd
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 298 (the chief-of-staff grant scope, handed over from Megavolt).
No launcher in this directory. The seat runs as a T3 thread (thread 48a3ae38) and works in its own worktree. It has no
credentials of its own; it pushes only its own `boyd/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
+8
View File
@@ -0,0 +1,8 @@
# Bubba
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 288 (agent write notices and library revert, handed over from Honker).
No launcher in this directory. The seat runs as a T3 thread (thread 3d6672b5) and works in its own worktree. It has no
credentials of its own; it pushes only its own `bubba/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
+327
View File
@@ -0,0 +1,327 @@
# Row 288 (#1793): C2-02 agent writes, notices, revert and Console saves
Owner: Bubba, Codex gpt-6.1-sol, thread 3d6672b5-5544-4b95-9e09-e79574f1aa14.
Reviewer: Webby. Gate and landing: Sage. Append-only build record.
## 2026-10-23 start and implementation plan
Base 45dca69f6 on bubba/row288. Direct mandate mos-1499 (Q245), confirmed sage-3613.
The canonical `scripts/mosaic queue next bubba` returned `nothing`; Sage owns the owner transition.
The initial worktree invocation refused linked-worktree use; retried in canonical checkout.
Spec: docs/plans/2026-10-20_case2-rows.md, C2-02; agents/gosalyn/work/case2/DESIGN.md ruling after r11.
Handoffs: Honker queue-283 BUILD.md Q4 (missing/non-100644 delete refuses); Morgana
queue-225 BUILD.md A2 at 66f7becf2 (strict revert arguments/result), confirmed by Fethry fethry-0005.
`project` is the business.projects key, never a chat UUID.
Dependencies merged with their commits intact, no amend or force-push:
- 283 origin/honker/row283-build 88ec5e8e4 (new E4 head to follow).
- 285 origin/scrooge/row285-r2 53db6b7ca.
- 286 origin/ludwig/row286-r2 573fa0be0.
- 284: no named remote branch yet. Tool changes come last.
All merge commits are marked Untested. Nothing has run under Node. Sage's latest instruction
sage-3613 reserves **all Node and test runs** for CLEAR; this supersedes the initial targeted-test
permission. Build untested now as instructed, then run RED/GREEN checks/mutants on CLEAR.
Architecture: one host-only plumbing writer under the existing library lock; broker authorizes and
selects credentials, then inserts the fixed notice directly. Human save/revert use editor +
library-human independently of seat lifecycle. No worktree, deferred push, role expansion or UI.
### Tasks and acceptance
- [ ] Plumbing: new library/src/write.mjs, mirror state publication and package exports; new
library/tests/write.test.mjs. One-parent write/delete, byte-exact content, seat identity/trailer,
stale-base, nonregular/missing deletes, retry once on an unrelated remote change, push failure
without local ref movement, human editor and revert reachability/single-path checks.
- [ ] Broker + runtime: library.doc.write/delete, library.file.save, library.revert; strict keys,
creative + library.write + live unheld project grant; direct exact notice; human-only save/revert.
New bus/tests/library-write.test.mjs covers authorization, unchanged refs, A2 contract and socket.
- [ ] Transport: only human library.file.save joins the existing large-save exception. New tests
prove >64 KiB save accepted, agent writes retain 64 KiB bound, malformed/oversize saves refuse.
- [ ] Tools after 284: library_doc_write/delete in harness tools; exact eight/ten-name snapshots
in a new harness test; cli dispatch wiring.
- [ ] Verify on CLEAR: new tests, library/bus/harness suites and all scripts/test-*.sh on Sage's
assigned slot. Mutants remove supporting guard and force push; both must fail. Fixture-token
search over captured output. No gate, landing or queue move claimed before Sage's acceptance.
Pre-flight interfaces: broker consumes library writer {project,path,commit}; revert adds reverted;
writer consumes C2-03b push {pushed} or {raced,tip} and supportingPath(mapping,universe,path,tree).
State publication must add optional lastPush while preserving old state readability. Refusals must
restore fetched remote-tracking refs as well as preserve served. Runtime must bind credential file
references itself; no file, identity, message or credential is supplied by the agent.
Necessary file-scope additions: packages/bus/src/runtime.mjs (actual adapter injection),
packages/library/src/index.mjs (exports), and CLI's existing console adapter if saves require it.
Reported to Sage in bubba-0002. Existing tests will not be edited without Mos's OK via Sage.
Review focus: remote tip races, nonregular entries and folded paths, unsafe human credential before
git, revert after revoked/fired writer, and large-request exception never extending agent writes.
### Existing-test changes
None authored by Bubba yet. Dependency test changes remain their owners' original commits.
## 2026-10-23 first static build (not a gate result)
283 E4 merged intact at 76d651dba. Honker next reports e67c27b32 (test environment only), to merge
before push. Della reports row 284 has not been built/pushed; row224's 23816cf28 is not a 284 head.
No speculative 284 pin. Typed tools and served-blob lookup integration remain pending 284.
Implemented (unrun): bare plumbing writer, one-parent commit and fixed trailer, exact-content blobs,
base comparison, supporting path guard and delete regular-blob check, one remote-race retry,
ref restoration on refusal, immediate served publication/lastPush; human save and reachable one-path
business-trailer revert; broker grants/identity/notice path; runtime adapter wiring; large-save socket
exception. New tests: library/tests/write.test.mjs and bus/tests/library-write.test.mjs. No old test edits.
### Approved scope additions, sage-3628
Every hunk outside the brief's owned list:
- `packages/bus/src/runtime.mjs`: `humanWriteOptions` checks editor before choosing the human slot;
three library adapter entries bind dataRoot, command and the host-only seat store reference. These
are needed to make actual bus calls reach the new writer without accepting agent-supplied files.
- `packages/library/src/index.mjs`: export writeLibraryDoc/saveLibraryFile/revertLibraryCommit so
runtime's existing library module adapter can call them.
- `packages/library/src/sync.mjs`: shared write guard with opaque token compared by identity;
push's race fetch recognizes only that owner and does not clear its guard; ordinary sync and a
forged token still refuse library-sync-busy. Narrow publishLibraryWrite validates ids, reads the
locked state, rebuilds the index via readLibrary, moves served/tracking and writes lastPush. The
raw writeState remains private and is not exported through index.mjs.
Ludwig ludwig-0064 reviewed the intended interface and requested the opaque token (implemented),
narrow publication (implemented), whole library + bus library-credential checks and a token mutant
(planned). His 573fa0be0 remains current and unrun. 288's additions are separate commits, not folded
into 286. Tracking restores only if the whole write refuses, not between the first race and retry.
Known tradeoff, accepted by Sage sage-3628 for MVP: the brief holds the flock through network Git;
readers can hit the existing 5-second library-sync-busy while it runs. Structural supportingPath
preflight is pure and precedes Git; served/fetched listTree checks repeat it with the real tree before
any remote write. A tree-dependent check necessarily reads the tree first (Honker honker-0184 item2).
### Mutants for CLEAR
- Remove the `pathOf` calls for agent operations: the M12 broker/bare-ref tests must fail on a
story/unsupported path while checking remote, tracking and served refs.
- Prefix the push refspec in syncCommand with `+`: write.test moved-target race must fail, because
its command seam preserves production syncCommand's refspec (only remote URL/protocol are fixture).
- Replace `writers.get(mirror) === writeToken` with `true`: write.test forged-token case must fail.
No Node, test, build or mutant has run. `git diff --check` is clean. Counts are not claimed.
### Console dispatch integration
The existing cli delegates its transport wiring to console-bus.mjs, so the save dispatch needs that
file too: one null `librarySave` option and one `--send` assignment to library.file.save. cli.mjs
threads that option into startServer; row C2-11b owns the actual route/page that consumes it. New
webui/tests/library-write-options.test.mjs covers --send and the user transport boundary. 225 owns
libraryRevert dispatch; 288 adds only librarySave to avoid overlapping that ownership. Additional
scope reported to Sage and Fethry. No changes to an existing test.
## 2026-10-23 second static pass and dependency review
First push: c867cada9 on origin/bubba/row288, reported bubba-0005 to Sage/Webby/Mos and
bubba-0006 to Fethry/Ludwig. No PR created and no review/gate requested as complete.
Ludwig ludwig-0065 checked the exact sync diff against 286 without running anything. Token identity,
ordinary-sync exclusion, 286 R1 preservation, guard key and end-of-write tracking restoration fit.
F1: state publication could fail after served moved, leaving old state/new served. Root cause is
publication order without rollback, compared against sync's existing rollback. Added an unrun
filesystem rename-failure regression, then changed the narrow helper: on failure it validates the
state under the same lock, CAS-restoring served if state has not published the new commit. If rename
succeeded but directory fsync failed and the state validates against new served, it keeps that pair.
All failure cases answer outcome-unknown; tracking remains the commit already pushed remotely.
Verification is pending CLEAR, so this is a proposed fix, not a tested resolution.
Added the actual broker-to-bare tests for each M12 path refusal (both agent verbs), unchanged remote
and all mirror refs, exact write/delete notices, and human revert after fixture grant removal/hold.
The bare fixture moved to new library/tests/write-fixture.mjs so broker tests reuse real plumbing.
Added the raw socket proof-nested-save-name case to pin the parsed-verb large-request check.
Missing editor/slot tests now make the mirror metadata invalid first: reaching Git would change the
refusal, so the tests check the pre-Git gate rather than merely count remote commands.
Additional required doc hunk: packages/library/README.md appends the four verb contracts,
lastPush shape, exact notice, lock tradeoff and publication failure behavior. No existing test from
the dependency/base has changed. All new/modified tests in this pass were authored by this row.
`git diff --check` remains clean. No Node/tests/build/mutants run; 284 and CLEAR remain pending.
## 2026-10-23 early review follow-up (Webby E1-E6; Ludwig F2)
Sage sage-3652 approves console-bus.mjs wiring (null option + --send dispatch), confirms editor.user
as the sole human login source and reserves CLEAR for T3/T4 after T2. Ordinary sync()'s separate
post-rename fsync gap stays out of 288 per that ruling; no change to sync()'s existing rollback.
Honker 283 head 46bad7ea3 merged intact: mapping editor.user is optional, and if present uses the
existing plain Gitea-user pattern. The omission at mapping level is valid; human writes refuse it.
- Ludwig F2 / Webby E6: confirmed the previous runtime used the fetch bot's mapping.user wrongly.
Runtime now passes mapping.editor.user to humanCredential, never mapping.user. Missing user gives
library-human-missing before Git. New real-runtime cases pin both save and revert with a separate
fetch-bot login, positive pushes under her-gitea-login, and no command for an absent editor login.
The broker's credential.agent is s.role, not args: added an exact assertion and strict-key refusal
cases for caller-supplied agent and credential.
- E1: replaced the business-id RegExp interpolation with a literal startsWith(business.id + '/')
check and ID_PATTERN on only the instance suffix. New acme.co vs acme-co trailer test.
- E2: new broker→bare case holds the seat in the fetch command. beforePush must refuse
library-write-no-grant with all local refs and the remote unchanged and no notice. New mutant:
remove `options.beforePush?.();` in write.mjs; the E2 test must fail.
- E3: new broker cases for expired rotate_by, fire.stopped without fire.done, missing role.publish
project, grant/identity Gitea-user mismatch, and missing identity name/email, for both agent verbs.
- E4: the exceptional publication failure currently returns outcome-unknown before the broker has
a successful result to put in a notice, so a remote agent commit can exist without a Console
notice. There is no durable notice outbox/retry in this row. The trailer commit remains human
revertible by id after sync; this is an explicit known failure-mode gap for the manifest, not
a claim that every failed publication emits a notice. A new real-push test breaks state decoding
after push admission, pins outcome-unknown, old served and no notice. It also found a local error
propagation gap: errors before publishLibraryWrite's internal try now normalize to outcome-unknown
in the writer once push has succeeded, so the caller is never told an ordinary refused-write code.
- E5: 284 still has no build. Tools/8-or-10 snapshot and served-blob integration remain pending.
librarySave is intentionally only an adapter option until C2-11b supplies its route/page.
All tests above are newly authored by row288 and remain unrun; no changes to baseline/dependency
tests. RED/GREEN and mutants await CLEAR. Proposed fixes are not claimed verified. The new host-only
library write module imports the existing pure ID_PATTERN to match the broker's accepted instance ids.
## 2026-10-23 mos-1520 ruling: post-push notice is required
Correction to the E4 interpretation above: Mos rejected the known-gap treatment. The requirement
stays: a pushed agent commit has the exact notice even if local publication then fails. This is not
an outbox or retry. The writer now calls the broker's trusted synchronous onPushed callback as soon
as push returns pushed:true, before publication. The callback records the exact library-write notice
and action.allowed in the normal Console-store transaction. Publication failure still answers
outcome-unknown. The notice's Revert can refuse library-revert-not-agent until sync makes that commit
reachable from served; README now says that explicitly and removes the rejected gap description.
The row's own real-push corruption test now pins the exact notice body with the pushed id, old
served and outcome-unknown; after restoring readable state it pins Revert's refusal before sync and
success after sync. The successful-write adapter fixture calls onPushed too, matching runtime's
writer contract. No existing baseline/dependency test changed (mos-1520 explicitly authorizes this
row's test correction without mos-1272).
Residual Console-store failure: if notice insertion/routing/its event transaction fails, the broker
calls a separate host log sink with {event:library-notice-store-failed,code:outcome-unknown,business,
instance,op,commit}. It logs no args, path content, credential, raw SQL or exception output. Runtime's
new library.log adapter writes this metadata to stderr. The caller gets outcome-unknown even if
recording action.refused in that same broken store fails. A new real-push test uses temporary SQLite
abort triggers on notice insertion and refusal evidence, checks remote advanced/served unchanged,
no message, the exact external log and outcome-unknown. It does not claim a notice can be stored in
a failed Console store. If the host log sink itself also fails, no durable log can be promised; the
caller still receives outcome-unknown. The runtime log hunk is the required residual-failure wiring.
Mutant added: move `options.onPushed?.({ project: context.project, path: change.path, commit });`
from before `await publishLibraryWrite(...)` to immediately after that await inside the same try.
Successful operations keep their notice, but the outcome-unknown publication path drops it. The
C2-02 E4 real-push corruption test must fail on its exact notice assertion. Run on CLEAR and restore.
No Node/tests/build/mutants run. Static git diff --check clean. Della's first284 head0ed4fbab9 is
explicitly incomplete and not a finished dependency; waiting for her complete-head pin before tools.
Webby webby-0253 (static, cef503cb9) closes E4 per mos-1520; no tests ran. N1: a failed notice insert throws from onPushed before publication, so served stays at the prior commit until sync; the store-failure test pins this safe direction. Only E5 (284 tools/snapshot integration) remains open in this early review.
## 2026-10-23 complete284 merge and final tool integration (unrun)
Della della-0037 supplied complete untested head47db04eba85ac3ceb7e81385603f97bb6f8c1a38,
replacing the incomplete head. Merged intact as c0ca3894c. Its22423816cf28 and2164f3fbc25a
lineage is retained. Three conflicts were in overlapping production wiring only:
- broker.mjs: keep Della's LIBRARY_LOOKUPS object and #requestLibraryLookup byte-for-byte, and
keep288 writer/notice methods; LIBRARY_VERBS contains both sets and request dispatch calls lookup.
- runtime.mjs: keep lookup adapter plus288's metadata log and write adapters.
- library/index.mjs: keep index/query exports,286's push/humanCredential exports and288's writer exports.
No dependency test body was changed in the merge. These are overlapping consumer integrations,
not rewrites of dependency commits. Every prerequisite still needs its own gate (mos-1351).
Added library_doc_write/library_doc_delete after the five lookups, only for a creative definition
with a within-role library.write grant and library enabled. Closed schemas carry exactly
{universe,path,base,content} and {universe,path,base}; base is string-or-null with 40-lowercase-hex
pattern. No project, business, author, credential, commit or message. tools.action is library.write.
New harness/tests/library-write-tools.test.mjs pins per-role eight/ten names, both schema invariants,
no repo/commit/business/accept, exact required/verb/action, nullable base, and missing library/grant
or noncreative exclusion. This satisfies sage-3678 and mos-1523's replacement coverage condition.
Publication's narrow helper now calls indexFor for the new served commit instead of only readLibrary,
so the284 lookup index is rebuilt before publication. New broker integration uses library.get's
served blob as write/delete base, independently checks served:path, reads the new text/blob immediately,
preserves default-canon null, then sees null after delete. No change to284's query behavior.
### Authorized baseline-test migration, own commit (mos-1523 via sage-3677)
File: packages/harness/tests/library-tools.test.mjs. Numstat: +4/-4, exactly.
The noncreative LOOKUPS assertion at :33 is unchanged. Every existing lookup schema assertion stays.
The four old/new lines are:
```diff
- ? ['web_search', 'web_fetch'] : []), ...LOOKUPS];
+ ? ['web_search', 'web_fetch'] : []), ...LOOKUPS, 'library_doc_write', 'library_doc_delete'];
-test('M3: per-role snapshots contain six names, or eight with the C2-08 web grant', t => {
+test('M3: per-role snapshots contain eight names, or ten with the C2-08 web grant', t => {
- for (const [i, tool] of tools.filter(t => t.name.startsWith('library_')).entries()) {
+ for (const [i, tool] of tools.filter(t => LOOKUPS.includes(t.name)).entries()) {
- assert.deepEqual(tools.map(t => t.name), ['send_message', ...LOOKUPS]);
+ assert.deepEqual(tools.map(t => t.name), ['send_message', ...LOOKUPS, 'library_doc_write', 'library_doc_delete']);
```
Additional schema mutant (mos-1523): add `commit: str("caller commit")` to writeBase in tools.mjs,
thereby adding it to either/both write schemas. The new-file schema test must fail both its exact
property set and forbidden-property assertions. Run on CLEAR and restore. No further assertion
change in library-tools.test.mjs is authorized or made.
All code/tests/mutants remain unrun. No Node, build or syntax check before Sage's named CLEAR.
Static diff --check is clean. Scope additions stay listed above, now including indexFor import and
call in the previously approved narrow publication helper. E5 implementation is present; review and
acceptance are pending evidence, not claimed complete.
## 2026-10-23 testing and landing boundary (sage-3682)
Latest complete dependency2845d587a552 (test/docs only over47db04eba) merged intact as f8f79ea2f.
Own tool implementation cd2f50dae and separate mos-1523 migration29455c0c5 are pushed; Webby reads
E5 and Dewey checks the migration. Static manifest-r1 covered21 own paths at f8f79ea2f.
Sage reserves first focused runs/RED-GREEN/mutants for T7, provisionally about22:25Z, unless he
explicitly frees an earlier lane. No Node before his CLEAR. This is a schedule, not CLEAR. Full
library/bus/harness and shell gates still need their own authorized slot; no gate result is claimed.
Before the gate, incorporate landed refactor after283/224/284 land while preserving every
prerequisite. Under the standing add-commits/no-force-push rule, update this published branch by merge
rather than rewriting its pushed history. 288 lands last in the chain; no landing is authorized here.
## 2026-10-23 E5 static closure and N2 wording
Dewey dewey-0948: the mos-1523 migration at29455c0c5 is static CLEAN, exactly+4/-4, with the
noncreative assertion unchanged and new-file schema coverage meeting every condition. Webby
webby-0263: E5 closed on29455c0c5/cd2f50dae (f8f79ea2f;98ca25ab1 only BUILD.md), nothing blocking.
Both ran nothing; Webby did not read the scratch manifest, so no manifest verification is implied.
Webby N2: delete inherited the write base description "or null to create". Delete now overrides only
that description: "The existing file's blob from library_get; a missing file cannot be deleted".
The reviewed nullable-base schema/required keys/verbs remain unchanged. The row's new schema test
pins create wording only on write and existing-file wording without create on delete. No further
assertion change in the baseline library-tools.test.mjs. No Node/tests run; still await Sage CLEAR.
## 2026-10-23 dependency283 follow-up (honker-0195)
Merged origin/honker/row283-build b2a1f4092 intact, fast-forward lineage from46bad7ea3. It changes
supporting.test's helper and Honker's BUILD receipts only; no production source changed. Honker
reports library98/98, business231/231 and14/14 mutants killed on that dependency head. Those are
his dependency receipts, not verification of288's combined branch. No288 Node/tests run; Sage's
CLEAR is still required. The dependency's own gate/landing remains Sage's responsibility.
## 2026-10-23 new candidate after283 landing (sage-3727, honker-0200)
Created bubba/row288-r2 from the preserved published bubba/row2887c53adf10, then rebased with
--rebase-merges --no-update-refs onto93694c5185d0a7f989d2fe78fb620e44599b744e. This supersedes
the earlier merge-only refresh plan for this candidate: Sage explicitly requested a fresh branch,
not a force-push. Both local and remote bubba/row288 remain7c53adf10, unchanged.
Git skipped12 duplicate283 commits already replayed upstream. Range-diff of
45dca69f6..bubba/row288 against93694c518..bubba/row288-r2 (before this receipt) has35 equal
commits,12 dropped,0 modified,0 added. Full output: scratch/bubba-row288/rebase-r2-range-diff.txt
under /mnt/storage. The other prerequisite commits retain their patches; row216/224 identities are
unchanged where Git could retain them. This is not new verification of any prerequisite.
Replayed merges had the same launcher-footer and broker/runtime/index conflicts as before. Reused
the exact previously reviewed combined file bytes, keeping both launcher test bodies and all lookup
and writer paths. Static comparison before this receipt: all21 own manifest paths byte-identical
to7c53adf10; all16 other changed paths exactly match93694c518 upstream. These include newly landed
226 and coordination/docs changes already on that base, not new288 edits. No baseline assertion or
production change was introduced by conflict resolution.
Sage's base93694c518 is an ancestor of the candidate; old283 b2a1f4092 is not. Static diff --check
clean. No Node, test, syntax, build, mutant or gate run. Named CLEAR/T7 still pending;288 lands last.
+8
View File
@@ -0,0 +1,8 @@
# Della
Builder seat, Codex gpt-6.1-sol, started by Mos for the email plan (Vikunja 460). Owns row 224 (C2-05b) under Q220.
No launcher in this directory. The seat runs as a T3 thread (thread 66735a7f) and works in its own worktree. It has no
credentials of its own; it pushes only its own `della/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Registered by Sage on 2026-10-22 so the queue can name the seat as
row owner under Q220 (mos-1301).
+170
View File
@@ -0,0 +1,170 @@
# Row 224 — C2-05b broker lookups and typed tools (#1725)
Owner: Della; reviewer: Dewey. Assignment sage-2795; scope extension sage-2804.
Base: `4f3fbc25a` (Researcher C2-05a). Branch: `della/row224`.
Brief: C2-05b in `docs/plans/2026-10-20_case2-rows.md` at `aca16404c`
and `51cc0445c`; design section 3, 5.3 M3 and 7.2; C2-05a BUILD rulings.
## Execution boundary and progress
Code/docs and local Git only while row 253 holds the gate. No Node (including
syntax checks), HTTPS Git, queue claim, test or push until Sage's CLEAR.
Tests are written first; their RED/GREEN runs are deferred, not claimed.
Queue next is deferred too: its executable uses Node. Sage assigned this row
explicitly. After 253 and 216 land, rebase under a new branch name.
- Pre-flight: query.mjs consumes `{repo,commit,mapping}`; broker cannot import
Git/filesystem library code because it is in the sandbox import closure.
Runtime injects the host lookup adapter; existing human file/status/sync stay.
- Pre-flight: resolved has no project list and a library is never a checkout.
Bundle needs an explicit library flag from launcher business configuration.
- M3 reading: exact creative base is five lookups plus send_message; retain
explicitly granted broker web_search/web_fetch for research/companion under
landed C2-08 and sage-2804. No generic spec, task, evidence or decision tools. Residual (mos-1308): those
reads lack broker action checks; their typed-tool absence is the only fence
for creative roles. M1/M2 deny bash; broker-side refusal is outside this row.
Mos confirmed this interpretation in mos-1308; design wording fix belongs to Gosalyn. Other definitions retain their current tools.
- Multi-library businesses use an optional project ID; omit it only with one
configured library. Universe is required for all five tools, including get.
- Missing library uses the brief's `library-not-configured`; argument errors
reuse `invalid-request` and C2-05a query codes. No new action or authority.
## Manifest (draft code, unrun)
- bus/src/broker.mjs: five lookup verbs, strict args, holder/business rechecks.
- bus/src/runtime.mjs: host lookup adapter, stale-before-read sync, served pin.
- harness/src/tools.mjs: five schemas; creative exact-set filtering.
- harness/src/bundle.mjs: library flag forwarded to typedTools.
- cli/src/launcher.mjs: one flag derived from trusted business projects.
- bus/tests/library-lookups.test.mjs: real query/broker/socket/runtime cases.
- harness/tests/library-tools.test.mjs: schemas, role snapshots, bundle parity.
- cli/tests/thread-launch.test.mjs: flag reaches a real launched bundle with no library checkout.
- agents/della/work/queue-224/BUILD.md: manifest, rulings and deferred verification ledger.
## Verification ledger
No test or syntax-check run yet. Acceptance requires full bus/cli/harness suites
and every scripts/test-*.sh on Sage's gate; focused files and affected CLI/
seat import-closure tests precede that only in an authorized CLEAR.
## Coordination
Huey272 reports no runtime/bundle/launcher overlap. Rocko219 changes launcher
THREAD_DEFINITIONS checks only, separate from our buildBundle flag argument.
The launcher argument is required: bundle cannot derive library presence from
checkout IDs (a library has no agent clone). No role, authority or config schema
changes; library project entries are already validated and protected by M9.
## Mount and pin impact (inspection only)
No new imports in broker.mjs or tools.mjs; no RUNNER_FILES or sandbox mount
addition. runtime.mjs is host-only and already imports library index.mjs.
Bundle forwards only a boolean, never a root/mapping/slot/credential. The
existing import-closure tests are bus/tests/project-runtime.test.mjs and
seat/tests/sandbox.test.mjs. Grep found no literal sha256 pin for runtime.mjs
or bundle.mjs; bundle tests derive current source digests through SOURCES.
Library sources remain in the existing whole-directory release copy.
## Runs to schedule only after CLEAR
Set TMPDIR=/mnt/storage/scratch/tmp. Focused new bus/harness files first,
then existing library.test.mjs, library-sync.test.mjs, project-runtime.test.mjs,
harness creative/tools/spec-tools/bundle files and affected CLI launch tests.
Follow with the required whole bus/cli/harness suites and Sage's full shell gate.
No timing claim yet: use the repository/Sage per-file limits and record every
command, outcome, elapsed time and timeout before acceptance. No focused skip
or pattern run substitutes for whole-file gate evidence.
## Read-through findings and residuals
- Library get requires universe although the older design table omitted it;
section 3 isolation rule and C2-05a's accepted API are binding.
- Query type enum matches mapping.TYPES (five values); manuscript is a distinct
lookup and not a find type. Unknown host-path/commit/business/accept fields
are refused; explicit project is restricted to this business's library list.
- Async reads recheck the live holder and current project revision. A stale
read attempts sync, then takes the new served snapshot; a returned fetch
failure/divergence keeps serving the old pin. Busy sync uses the current pin.
Unreadable state, no served commit or unsafe configuration refuse. Query
and sync never hold a SQLite transaction over await.
- Processing halt refuses new agent lookup work and rechecks before returning
async results. Existing human library file/status/sync behavior is unchanged.
- Timer sync and reads still use C2-03a's 60-second contract. No timer tuning or
resource-limit schema is introduced by this row. Q222 creative reply settings
are in C2-06; no reply guard or writing capability is implemented here.
- The generic tool socket's existing 60-second client timeout can precede the
server's 120-second Git timeout. This row does not change the generic timeout;
a timed-out lookup is read-only and retryable, never a write/outcome claim.
- All tests are authored but unrun. No RED, GREEN, syntax, runtime performance,
sandbox, broker/harness suite, shell gate, claim or push evidence exists yet.
## Candidate review checklist (read-through only)
- [x] Five verbs mapped to real C2-05a methods, all require universe.
- [x] Default sole library or explicit owned project; no caller repo/commit.
- [x] Human-only file/status/sync gates kept; lookup holder rechecked.
- [x] Async socket dispatch includes lookup verbs through LIBRARY_VERBS.
- [x] Stale/unknown fetch syncs first; results use current immutable served pin.
- [x] Six/eight creative names per mos-1308; other roles retain their tools.
- [x] Launcher -> bundle -> typedTools passes presence without a library clone.
- [x] No imports added to sandbox source closure; no mount change.
- [ ] Execute new tests, RED characterization, full files/suites after CLEAR.
- [ ] Rebase after rows 253/216 land, under a new branch name.
- [ ] Claim/push only in Sage-authorized gap; Dewey review and Sage gate.
Candidate preservation commits are explicitly unrun under sage-2795; they are
not completion, green-suite or landing claims. No dependencies installed.
Correction: the first committed-range diff --check found a blank line at EOF
in the new library-tools test, missed while it was untracked. Removed in a
follow-up local commit; the complete range must be checked again. No tests ran.
## Shared-log receipts for Sage (sage-2815)
The shared logs are unchanged from the base in this row. Sage appends these
receipts in the landing sweep; they are preserved here, not in the manifest.
For `BUILD-LOG.md`:
```text
2026-10-22T16:36:30Z | Della row224 C2-05b #1725 preparation | Before: C2-05a queries exist at 4f3fbc25a but no broker lookup verbs or typed tools. Scope: broker/tools plus Sage-authorized runtime/bundle integration, tests and docs. Existing human-only library reads retained; five agent queries require a live holder and trusted configured source. Code/docs only under row253 gate; no Node/test/push/claim. Runtime ownership coordinated with Rocko219/Huey272.
```
For `docs/SESSIONS.md`:
```text
2026-10-22T16:36:30Z | Della (sage-2795 / sage-2804, Q220) | Row 224 C2-05b #1725, della/row224 base 4f3fbc25a | Read both pinned briefs and C2-05a source; preparing broker/typed tools and authorized host runtime/bundle seams. No queue command, Node, HTTPS Git, claim, test or push during Gander gate. Dewey reviewer; Rocko/Huey runtime coordination sent. Tests and integration verification deferred to CLEAR.
```
Local-only draft pins 19c0a7adf/7cf39c390 are superseded by the
log-free candidate. Saved as local branch della/row224-draft-with-logs before
rebuilding the unpublished candidate commit; no remote history changed.
## Dewey dewey-0852 and Sage sage-2819 — focused corrections
B1: inline freshness uses max(lastFetch, lastError.at), not lastFetch alone.
An error within 60 seconds counts as a recent attempt, so later lookups serve
the old served pin without another fetch. Keep inline sync: the brief requires
it and businesses without timed libraries have no sync timer. The extended
failed-fetch test makes reads at +1, +59,999 and +60,000 ms (one attempt total),
then +60,001 ms (second attempt). All reads must return the old commit.
Required mutant M-B1: remove lastError.at from recentAttempt. The repeated
reads must fail the attempted-count assertion. This mutant has NOT been run.
Residual (Sage will defer): the first inline fetch in each outage window can
still exceed the tool timeout (60 seconds, or the runner broker client’s
30 seconds). Bounding that fetch is out of scope for 224. Subsequent reads
are paced by persisted lastError.at. No claim of first-read outage latency.
N1: whole bus, cli and harness suites are required, plus the shell gate.
If the new thread-launch integration test skips due to sandboxProblem(),
name the skip; a skip is not launcher integration proof.
N2: cli/tests/thread-launch.test.mjs now depends on library/tests/helpers.mjs
mappingEntry() and library/tests/fixtures/mapping.json. Changes to that fixture
must rerun the CLI launch file.
N3: harness/tests/library-tools.test.mjs compares the type enum to mapping.mjs
TYPES, so adding a mapped type cannot silently leave the typed tool behind.
These edits are local code/docs only. No Node, RED/GREEN run, mutant, claim,
HTTPS Git or push. New local pin goes to Dewey/Mos/Sage for focused review.
+196
View File
@@ -0,0 +1,196 @@
# Row284 C2-05c — supporting scope and served-tree blob (#1789)
Owner Della; reviewer Dewey; gate Sage. Build start sage-3658/3660 (10-23).
Branch della/row284. Original prerequisite base Honker28346bad7ea3;
merge fc05bcbc3 carries22423816cf28 and2164f3fbc25a intact. Only conflict was
CLI thread-launch's added footer tests: retained both full test bodies and
assertions. No prerequisite code copied. Every prerequisite needs its own
review/mutants/landing under mos1351; rebase after283 lands.
## Execution status
UNTESTED. No Node, node--check, test or mutant run before Sage CLEAR. First
own commit is the authorized old-test migration; the implementation follows.
Sage authorizes an immediate untested push. No queue write or shared-log edit.
224 stays first;284 is critical to Bubba288. T4 for224 is not a CLEAR yet.
## Binding readings
Brief C2-05c at94c33a43c:1737-1789, Q226, sage2918 and mos1330 item3.
get defaults canon; explicit playground/supporting opt-in; mismatch null.
Sort full outgoing/incoming lists supporting, playground, canon before paging,
keep cross-scope links and universe filter. Manuscript stays canon/playground.
No all/array scope, write tool, guard, mapping or creative-name-count change.
## mos-1330 old-test migration (separate commit)
All paths below are packages/library/tests/query.test.mjs. Baseline file at
fc05bcbc3 matches216's file. Source assertions retained; negative calls receive
explicit scope too, so universe isolation cannot pass on default-scope denial.
Call/assertion 348 -> 348:
```js
// OLD
const rin = await get(s, { universe: 'idea', path: 'Drafts/New World/Characters/Rin.md' });
// NEW
const rin = await get(s, { universe: 'idea', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' });
```
Call/assertion 350 -> 350:
```js
// OLD
assert.equal(await get(s, { universe: 'harbor', path: 'Drafts/New World/Characters/Rin.md' }), null);
// NEW
assert.equal(await get(s, { universe: 'harbor', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' }), null);
```
Call/assertion 366 -> 366:
```js
// OLD
assert.equal(await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path }), null);
// NEW
assert.equal(await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path, scope: 'playground' }), null);
```
Call/assertion 367 -> 367:
```js
// OLD
const own = await get(src(ref), { universe: 'harbor', path });
// NEW
const own = await get(src(ref), { universe: 'harbor', path, scope: 'playground' });
```
Assertion 349 retained, OLD == NEW:
```js
assert.equal(rin.scope, 'playground');
```
Assertion 368 retained, OLD == NEW:
```js
assert.equal(own.scope, 'playground');
```
The null assertions at350 and366 are the complete OLD/NEW expressions above:
expectation remains null. Added one separate no-scope playground-get -> null
behavior test. It is unrun and expected to expose old get behavior until the
new scope implementation lands. This migration is authorized by mos1330 under
mos1272; no check removed or weakened. Dewey checks this list in manifest review.
## 283 surface and blob choice
Inspected actual46bad7ea3: optional editor.user is present. reader's parsed
file records and index entries still discard tree oid. In owned query.mjs use
the existing bounded listTree(repo, servedCommit) to obtain exact path oid,
not HEAD and not a user-supplied revision expression. No reader/index edit.
Tradeoff: one additional bounded tree listing per get; latency unmeasured.
Recheck after rebasing to283's actual landing.
## Intended owned paths
query.mjs; broker.mjs lookup branch; tools.mjs lookup schemas/descriptions;
library query.test.mjs/query-fixture.mjs; bus library-lookups.test.mjs;
harness library-tools.test.mjs; this BUILD.md. Other files are prerequisites.
## Gate
Dewey manifest approval, whole library/bus/harness suites and every
scripts/test-*.sh on Sage gate. Before review grep all package tests for enum
pins and canon/playground pairs. Keep shared logs out; send receipts to Sage.
No test or mutant evidence claimed. Manifest/digest follows complete code.
## Implementation head (UNTESTED, sage-3658)
- query scope accepts supporting for find/search/get. get defaults canon,
mismatch null. Manuscript passes a separate two-value scope allowlist.
- get returns blob from listTree(source.repo,index.commit) after scope/universe
filtering. The same index.commit supplies the text; source HEAD is irrelevant.
- Full outgoing and incoming link lists are stably sorted by supporting,
playground,canon before combining/paging. All visible cross-scope links stay;
universe/hybrid visibility still applies. Scope metadata isn't added to links.
- Broker only adds optional scope to library.get's strict fields. No new verbs.
- Typed scope expands find/search/get only; manuscript retains its own two-value
enum. Names/M3 counts remain the row224 six/eight, no write tools added.
- New tests: book/universe/draft supporting find/search/get; defaults/mismatch;
absent supporting prefix; source HEAD advances but each scope's blob equals
served:path; 300 outgoing plus300 incoming links ordered across bounded pages,
retaining cross-scope links and dropping other-universe links; hybrid source
stays canon-only; no all/array scope; manuscript rejects supporting; broker
scope/blob forwarding; typed enums and unchanged names. All unrun.
## Additional old assertion pin (brief Gate)
harness/tests/library-tools.test.mjs:49 at23816cf28 (line49, unchanged in this head):
OLD find scope enum ['canon','playground']; NEW ['canon','playground','supporting'].
This is the exact scope-enum pin the brief directs updating. Existing six/eight
name snapshots and TYPES equality assertion are unchanged. The new manuscript
pin intentionally remains ['canon','playground']. No other two-scope test pin
was found by rg over packages/*/tests at this build's prerequisite merge.
## Proposed mutation evidence (reviewer confirms final set)
All unrun. Require assertion failures with whole-file runs and pristine restore:
M1 remove supporting from query SCOPES -> supporting lookup cases.
M2 drop get's requested-scope comparison -> default/mismatch null cases.
M3 use source HEAD for blob lookup -> served-tree/advanced-HEAD equality case.
M4 remove link rank sort -> multi-page outgoing/incoming expected order.
M5 restrict links to requested scope -> cross-scope link counts/order case.
M6 drop sees() from visible/link filtering -> second-universe/hybrid cases.
M7 allow supporting in manuscript validator -> manuscript refusal case.
M8 omit get scope from broker fields -> real broker supporting get case.
M9 widen typed manuscript enum -> typed-scope separation case.
## Inspection and pending gate
No production imports change in broker/tools, so no new sandbox closure file
or mount. query.mjs is host library code and adds existing reader.listTree;
no new package/export/copy-path file. rg found the one old find scope pin plus
the intentional new manuscript two-scope pin. Whole library/bus/harness and
shell gates remain required. CLI integration/merge context is prerequisite224/
219, not this row's source delta; any changed gate expectation goes to Sage.
Node checks, tests, mutants and elapsed-time evidence remain absent pending
CLEAR. The first test-migration commit and this implementation are distinct.
Sage sweep receipt: Della row284 builds on28346bad7ea3 plus intact224 merge;
mos1330 old-test migration separate, supporting scope/blob implementation and
fixtures authored, completely UNTESTED. No shared-log/queue write or live action.
## Current assertion anchors after implementation imports
The separate migration commit0ed4fbab9 retains its complete OLD/NEW text
above. Adding only the fs/path imports in the implementation shifts anchors:
query.test.mjs:130: `const blendCh2 = await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path: 'Book One/Chapters/Chapter 02.md' });`
query.test.mjs:350: `const rin = await get(s, { universe: 'idea', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' });`
query.test.mjs:351: `assert.equal(rin.scope, 'playground');`
query.test.mjs:352: `assert.equal(await get(s, { universe: 'harbor', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' }), null);`
query.test.mjs:368: `assert.equal(await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path, scope: 'playground' }), null);`
query.test.mjs:369: `const own = await get(src(ref), { universe: 'harbor', path, scope: 'playground' });`
query.test.mjs:370: `assert.equal(own.scope, 'playground');`
No result/null assertion changed; scope arguments carry the explicit opt-in.
## Dewey dewey-0941 static review — round2
Static scope fc05bcbc3..47db04eba CLEAN; not a final verdict or test evidence.
M1-M9 accepted as the final set. Optional M10 not added to this gate.
N2 taken: keep the existing canon-path supporting-scope mismatch assertion,
and ADD get(Book One/Support/Notes/Helper.md,scope:supporting) -> null under
the default mapping with no supporting prefix. The supporting-positive test
already returns this same path under withSupporting, so the added probe
pins the missing-prefix case directly. No assertion removed or weakened;
mos1330's separate migration commit and its retained assertions unchanged.
N1 proposed DEFERRED/follow-up for Sage: retain each tree oid in reader/index
entries so get can return blob without cat-file/whole ls-tree -r -l per page.
Current get's result is pinned correctly, but per-page cost grows with the
library. No performance measurement yet. This optimization needs reader/index
ownership and separate acceptance/performance evidence; do not silently expand
284's manifest or change its served-snapshot contract for it.
No Node, tests, mutants or syntax checks run in this round; hold still applies.
+8
View File
@@ -0,0 +1,8 @@
# Fethry
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 225 (C2-10b, handed over from Morgana).
No launcher in this directory. The seat runs as a T3 thread (thread 14f80e3c) and works in its own worktree. It has no
credentials of its own; it pushes only its own `fethry/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
@@ -0,0 +1,7 @@
{
"base": "01983cd3f",
"test": "packages/cli/tests/stack-release.test.mjs",
"base_is_byte_exact_prefix": true,
"old_assertions_changed": 0,
"old_console_refusal_text_unchanged": true
}
@@ -0,0 +1,48 @@
# Target config validation for every key implementation plan
Goal: #1797, mos-1353, brief f0e3dcdf4:docs/plans/2026-10-22_config-checks-any-key-brief.md. Base01983cd3f, branchgander/config-checks-any-key. Reviewer Duckworth. Queue number pending Sage; packet moves to queue-N when assigned. No Node, test or push before Sage gap/CLEAR. Rows270/271/203 land first; rebase under a new branch name before the gate.
Architecture: config_checks classifies missing/parseable/unreadable config independently of console presence. With a loader, every existing parseable config goes through the target's own validate. Missing config passes. Malformed JSON refuses. With no loader, allow only the oldest four top-level keys: configVersion, environment, dataRoot, execution. Evidence: c3d29e796:scripts/mosaic-config.mjs:114, the first loader commit, rejectUnknownKeys; row200 fixture's LOADER_OLD has the same list. A missing loader plus a later key refuses and names that key, without inventing validation support. The legacy public refusal code config-console-key and existing console-specific message stay byte-identical; new non-console refusals name the loader's first diagnostic and preserve restore-config-backup instruction. The target loader's resolved stdout is discarded. Its first stderr diagnostic is reported and may echo an invalid value.
Files: scripts/stack-release.sh config_checks/comment only; packages/cli/tests/stack-release.test.mjs appended regression coverage only; docs/TOOLS.md broadens the current console-only release-check description. Existing row200 tests and assertions stay unchanged. No fixture-module edit, flags, pointers, migration, schema, data, live unit or host change.
Tests to write before implementation, unrun until CLEAR:
- A target loader rejecting a non-console key refuses activate and rollback before any unit stop/start, pointer/unit/store mutation; one refused log entry; --plan preserves state and lists refusal. Reuse refused() and planOnly(). Use execution, already known to the current loader, to prove console-free dispatch before row270 integration; add compactionReadBytes/compactionStreams cases on final post270 base if needed.
- An accepting target loader is actually called with validate and the selected MOSAIC_CONFIG, succeeds activate/rollback without console, and resolved stdout is not leaked.
- Missing-loader target permits oldest-key config but rejects a later key with backup instruction. A stand-in rejecting the existing execution key exercises the generic target-validation branch; missing-loader later-key test may use console until final post270 base supplies non-console new keys.
- Missing file passes the isolated real config_checks function; malformed JSON refuses there, since load_config otherwise exits before this function. Tests extract only that function and provide copies/path/check_refuse stand-ins, never live state.
- Existing console-key row200 tests and malformed-load test run whole unchanged.
Mutants, each unique target, syntax preflight and byte-identical restore:
M1 restores the console-only skip, killed by the named non-console activate/rollback refusal test.
M2 ignores failed target validation, killed by the same refusal assertions.
M3 allows a missing-loader target with a later key, killed by the explicit missing-loader regression.
M4 treats malformed JSON as absence, killed by isolated parse guard test.
M5 skips the loader for an oldest-key config, killed by accepting-loader invocation witness.
Gate after static review and required rebases: intended RED/GREEN; touched stack-release.test.mjs whole, CLI suite whole and every scripts/test-*.sh whole; M1-M5 with fresh passing controls, no timeout counted as kill, restored bytes/status before next. TMPDIR mktemp-d /mnt/storage/scratch/tmp/gXXX; pipefail/tee and busy check before each start. Node timeout-k10 uses at least3x measured duration plus margin; stack-release prior full receipt around584s implies at least1871s, to be recorded from actual receipts before CLEAR. Shell3x table retained with receipts. No test ran and no behavior passes claimed.
Review focus: parser races after load_config; target stderr naming only the refused key, stdout never persisted; current versus target loader selection; unknown key with no console; missing loader compatibility set from immutable history; upcoming row270 loader-list fixture drift. Final integration audits every test dependency and hash pin against the named base. Session registration remains Sage's.
Implementation ready, unrun. Four appended tests cover full activate/rollback refusal and plan; actual accepting validate witness/configpath and stdout nonleak; oldest-only missing loader with both compaction keys; missing/invalid JSON isolated realfunction. Stand-in loaders are ESM, matching .mjs. Every old test byte is preserved as a prefix. Existing console refusal text including missing-loader diagnostic is unchanged. Missing-loader compatibility is top-level key compatibility only; current load_config still validates the whole config before normal release operations. Three-path manifest, no fixture-module changes. Static target uniqueness M1-M5 passes. No Node parse, Bash execution or behavior run. Exact full compaction activate/rollback cases must be added against post270 loader, before final gate.
Execution state:
- [x] Read brief, config_checks and row200 assertions.
- [x] Pin oldest key set from immutable loader history.
- [x] Write regression tests, implement bounded scope, prepare unique mutants.
- [ ] Duckworth static review.
- [ ] Move packet to queue-N on assignment, rebase after270/271/203 and add full compaction-key cases.
- [ ] On CLEAR run intendedRED/GREEN, touched whole file, CLI whole, M1-M5, ten shells; commit/push records only when authorized.
## Duckworth0229 static approval, gate conditions
Source0585b3213, packet e7371025f, manifestd6226d28 approved by static read, note a5a717391:agents/duckworth/work/row1797/ROUND1-0585b3213.md. No runtime proof yet.
C1: after270/271/203 land, rebase under a new branch name and open a new static round. Add exact compactionReadBytes and compactionStreams full activate/rollback refusals. The existing LOADER_OLD uses the current loader plus concat after270, so it is not a pre270 witness; the new test requires its own pre270 stand-in. Reviewer checks the integrated manifest/test before CLEAR. N3 joins that delta: compactionStreams fixture value1024, matching the real type, instead of ['messages'].
C2: every existing B/C1/C release now validates its copied real loader, including external/upgrade suites. Any old CLI red is a change-related suspect and goes to Mos/Sage under mos-1272; no automatic placement as an unrelated fixture.
N1 console plus another refused key retains the old console text by design. N2 missing-loader wording is retained pending final integration review. N4 corrected above: only resolved stdout is suppressed; first stderr may echo a value, so no blanket secrecy claim. No runtime/Node/queue/push authorization is inferred from static approval.
@@ -0,0 +1,3 @@
eea06caad07c746d194f793603052b210e437b7184009f8b1617c88db49a5517 docs/TOOLS.md
a35c49c5703e02f35073450e4a98e1b90cffcda581490f97a70cbdc6d2afee59 packages/cli/tests/stack-release.test.mjs
f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150 scripts/stack-release.sh
@@ -0,0 +1,9 @@
{
"reviewer": "duckworth",
"verdict": "static approve, conditions C1-C2",
"message": "duckworth-0229",
"source": "0585b3213",
"manifest": "d6226d28",
"note": "a5a717391:agents/duckworth/work/row1797/ROUND1-0585b3213.md",
"next": "wait for row number and post270/271/203 base, new integration static round before gate"
}
@@ -0,0 +1,5 @@
{
"docs/TOOLS.md": "eea06caad07c746d194f793603052b210e437b7184009f8b1617c88db49a5517",
"packages/cli/tests/stack-release.test.mjs": "a35c49c5703e02f35073450e4a98e1b90cffcda581490f97a70cbdc6d2afee59",
"scripts/stack-release.sh": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150"
}
@@ -0,0 +1,14 @@
{
"issue": 1797,
"row": "pending Sage assignment",
"branch": "gander/config-checks-any-key",
"base": "01983cd3f",
"source": "0585b321309e4d8a199afb1a1024cf6091f56770",
"manifest": "d6226d281c778677a45e90993bbd9c985de89ba0f870d4a3ca12b6923a497e4a",
"runtime": "NOT RUN; no Node, tests or push authorized",
"after": [
270,
271,
203
]
}
@@ -0,0 +1,32 @@
[
{
"id": "M1",
"occurrences": 1,
"before": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150",
"after": "804a86442527f2728f633f485eaeca02552df66a34350eac5101127046415188"
},
{
"id": "M2",
"occurrences": 1,
"before": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150",
"after": "1c2acb6397e29ab898d8a89bc5bc691a294be749ca7d7f7897c83ec1b3e9f9ef"
},
{
"id": "M3",
"occurrences": 1,
"before": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150",
"after": "954c9a9735904e1c566d844001b2e85c13cd16eb16e7b4b91b752974bb6dc754"
},
{
"id": "M4",
"occurrences": 1,
"before": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150",
"after": "953d91bf28073b11e4bc5a6da516a0db1b8dfff892083cd3e6fcebe9c47b6bbf"
},
{
"id": "M5",
"occurrences": 1,
"before": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150",
"after": "310c32b16f0fc554d5a7ffeeb0742ea7dd9fc84e1ddafabb25d7ca3f449865ef"
}
]
@@ -0,0 +1,37 @@
[
{
"id": "M1",
"file": "scripts/stack-release.sh",
"old": " [ \"$key\" != 1 ] || return 0\n if [ \"$key\" != 0 ]; then",
"new": " [ \"$key\" != 1 ] || return 0\n [ \"$extra\" = '\"console\"' ] || return 0\n if [ \"$key\" != 0 ]; then",
"killer": "config-any-key: a non-console target refusal"
},
{
"id": "M2",
"file": "scripts/stack-release.sh",
"old": "elif out=\"$(MOSAIC_CONFIG=\"$cfg\" node \"$loader\" validate 2>&1 >/dev/null)\"; then return 0",
"new": "elif out=\"$(MOSAIC_CONFIG=\"$cfg\" node \"$loader\" validate 2>&1 >/dev/null)\" || true; then return 0",
"killer": "config-any-key: a non-console target refusal"
},
{
"id": "M3",
"file": "scripts/stack-release.sh",
"old": " [ -n \"$extra\" ] || return 0",
"new": " return 0 # drop missing-loader later-key refusal",
"killer": "config-any-key: missing-loader copies"
},
{
"id": "M4",
"file": "scripts/stack-release.sh",
"old": " if [ \"$key\" != 0 ]; then\n check_refuse config-console-key",
"new": " if [ \"$key\" = 99 ]; then\n check_refuse config-console-key",
"killer": "config-any-key: missing config passes but malformed JSON refuses"
},
{
"id": "M5",
"file": "scripts/stack-release.sh",
"old": " if [ ! -f \"$loader\" ]; then",
"new": " [ -n \"$extra\" ] || return 0\n if [ ! -f \"$loader\" ]; then",
"killer": "config-any-key: accepting loaders run validate"
}
]
@@ -0,0 +1,711 @@
{
"base": "c3d88840c",
"stage": "pre-implementation; not the passing final static check",
"occurrences": [
{
"file": "scripts/case2-unit.sh",
"line": 25,
"text": "# optional --tester-port N (1024..65535, never --port). Only factory takes"
},
{
"file": "scripts/case2-unit.sh",
"line": 163,
"text": " --tester-port) [ \"$PROFILE\" = factory ] || die \"the library profile refuses --tester-port: the tester listener takes writes from loopback with no sign-in\" ;;"
},
{
"file": "scripts/case2-unit.sh",
"line": 193,
"text": " --tester-port) tester_port=\"$value\""
},
{
"file": "scripts/case2-unit.sh",
"line": 194,
"text": " [[ \"$value\" =~ ^[1-9][0-9]{3,4}$ ]] && [ \"$value\" -ge 1024 ] && [ \"$value\" -le 65535 ] || die \"--tester-port must be 1024..65535: $value\" ;;"
},
{
"file": "scripts/case2-unit.sh",
"line": 204,
"text": " [ -z \"$tester_port\" ] || [ \"$tester_port\" -ne \"$port\" ] || die \"--tester-port must differ from --port\""
},
{
"file": "scripts/instance.sh",
"line": 114,
"text": "# this instance's, whose ExecStart takes PORT as --port or --tester-port."
},
{
"file": "scripts/instance.sh",
"line": 132,
"text": " --tester-port) [[ \"$tok\" =~ ^[0-9]{1,5}$ ]] && ports+=(\"$((10#$tok))\") ;;"
},
{
"file": "scripts/case1-accept.sh",
"line": 65,
"text": " case \"$a\" in --port) PORT=\"$v\"; i=$((i + 1)) ;; --tester-port) TESTER_PORT=\"$v\"; i=$((i + 1)) ;; esac"
},
{
"file": "scripts/case1-accept.sh",
"line": 88,
"text": " refuse \"$ARGS_FILE lacks a numeric --port or --tester-port after --\""
},
{
"file": "scripts/webui-service.sh",
"line": 43,
"text": "# Console), --tester-socket (private Unix tester), or legacy --tester-port N"
},
{
"file": "scripts/webui-service.sh",
"line": 74,
"text": "# --tester-port that another installed Console unit in the unit directory"
},
{
"file": "scripts/webui-service.sh",
"line": 96,
"text": " FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH]"
},
{
"file": "scripts/webui-service.sh",
"line": 97,
"text": " --tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener."
},
{
"file": "scripts/webui-service.sh",
"line": 137,
"text": " --tester-port) [ $# -ge 2 ] || usage; TESTER_PORT=\"$2\"; TESTER_SET=1; SERVE_SET=1; shift 2 ;;"
},
{
"file": "scripts/webui-service.sh",
"line": 182,
"text": " echo \"webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT\" >&2"
},
{
"file": "scripts/webui-service.sh",
"line": 266,
"text": " [ \"$TESTER_SET\" = 0 ] || flags=\"$flags --tester-port $TESTER_PORT\""
},
{
"file": "scripts/webui-service.sh",
"line": 462,
"text": " if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' \"$target\"; then"
},
{
"file": "scripts/webui-service.sh",
"line": 467,
"text": " if grep -Eq '^ExecStart=.* --sso( --send)?( --tester-port [0-9]+)?$' \"$target\"; then"
},
{
"file": "scripts/webui-service.sh",
"line": 471,
"text": " tester_port=\"$(sed -n 's/^ExecStart=.* --tester-port \\([0-9][0-9]*\\)$/\\1/p' \"$target\")\""
},
{
"file": "scripts/stack-release.sh",
"line": 47,
"text": " stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]"
},
{
"file": "scripts/stack-release.sh",
"line": 63,
"text": "activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the"
},
{
"file": "scripts/stack-release.sh",
"line": 126,
"text": " --tester-port) [ \"$OP\" = activate ] && [ $# -ge 2 ] && [ -n \"$2\" ] || usage; TESTER_PORT=\"$2\"; shift 2 ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 797,
"text": " elif grep -q -- '--tester-port' \"$cli\"; then supported=1; fi"
},
{
"file": "scripts/stack-release.sh",
"line": 800,
"text": " --tester-port) i=$((i + 1)); inherited=\"${BASE_CONSOLE[i]:-}\" ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 814,
"text": " [ -z \"$inherited\" ] || DROP_TESTER=\"--tester-port $inherited (replaced by private Unix socket)\""
},
{
"file": "scripts/stack-release.sh",
"line": 830,
"text": " check_refuse tester-port-invalid \"--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other\""
},
{
"file": "scripts/stack-release.sh",
"line": 832,
"text": " [ \"$supported\" = 1 ] || check_refuse tester-port-target \"$TO's Console does not support --tester-port\""
},
{
"file": "scripts/stack-release.sh",
"line": 833,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\" --tester-port \"$TESTER_PORT\")"
},
{
"file": "scripts/stack-release.sh",
"line": 837,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\") DROP_TESTER=\"--tester-port $inherited\""
},
{
"file": "scripts/stack-release.sh",
"line": 878,
"text": " # --send and --tester-port follow the pointer, so a --no-send or a tester port set after that"
},
{
"file": "scripts/stack-release.sh",
"line": 885,
"text": " --tester-port) i=$((i + 1)) ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 893,
"text": " --tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port \"${P_CONSOLE[i]:-}\") ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 897,
"text": " SSO_PLAN=\"sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them\""
},
{
"file": "scripts/stack-release.sh",
"line": 903,
"text": "# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP."
},
{
"file": "scripts/stack-release.sh",
"line": 919,
"text": " case \"${kept[i]}\" in --send | --tester-port | --tester-socket) break ;; esac"
},
{
"file": "scripts/stack-release.sh",
"line": 1076,
"text": " case \"${without_proof[i]}\" in --send | --tester-port | --tester-socket) break ;; esac"
},
{
"file": "scripts/stack-release.sh",
"line": 1134,
"text": "# Row 190: the target Console's --port (7332 when absent) and --tester-port against every other"
},
{
"file": "scripts/stack-release.sh",
"line": 1142,
"text": " --tester-port) ports+=(\"${RUN_CONSOLE[i + 1]:-}\") ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 2106,
"text": " for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do [ \"${P_CONSOLE[i]}\" != --tester-port ] || reserved+=(\"${P_CONSOLE[i + 1]:-}\"); done"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 80,
"text": "const factoryFlags = () => [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"7333\"];"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 328,
"text": " [\"--tester-port\", \"7333\", /the library profile refuses --tester-port/],"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 364,
"text": " refuses(unit(h, { profile: \"factory\", flags: [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"443\"] }), /--tester-port must be 1024\\.\\.65535|--tester-port must differ from --port/);"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 365,
"text": " refuses(unit(h, { profile: \"factory\", flags: [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"1443\", \"--port\", \"1443\"] }), /given twice/);"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 378,
"text": " refuses(unit(h, { profile: \"factory\", flags: [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"443\"] }), /--tester-port must be 1024\\.\\.65535/);"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 380,
"text": " assert.match(read(UNIT), /--tester-port must differ from --port/);"
},
{
"file": "packages/cli/tests/stack-release-fixture.mjs",
"line": 306,
"text": " for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\\n// serve accepts '--tester-port'\\n// serve accepts '--proof-header' (row 178)\\n${[C1,C].includes(id)?\"// serve accepts '--sso' (row 192)\\n\":''}`:'// fixture Console before tester listener, never run\\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 681,
"text": "const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 687,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 688,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 690,
"text": " r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 692,
"text": " assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 693,
"text": " assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 694,
"text": " r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 697,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 702,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 705,
"text": " assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 708,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 709,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 714,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 718,
"text": " assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 724,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 731,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 732,
"text": " r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 736,
"text": " edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 738,
"text": " edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 740,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 741,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 746,
"text": " let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 749,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 821,
"text": " const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 827,
"text": " const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 830,
"text": " const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 839,
"text": " const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 843,
"text": " await t.test('a target Console without --tester-port',async(t)=>{"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 846,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 848,
"text": " assert.deepEqual(r.out.split('\\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 850,
"text": " await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1056,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1057,
"text": " assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1058,
"text": " assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1129,
"text": " await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1132,
"text": " let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1135,
"text": " assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1137,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1138,
"text": " assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1141,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1162,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1351,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1352,
"text": " assert.deepEqual(want.slice(-2),['--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1367,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1468,
"text": "const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1480,
"text": " let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1481,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1483,
"text": " const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1486,
"text": " `sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1563,
"text": " const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1564,
"text": " assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1584,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1594,
"text": " const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1682,
"text": " assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1811,
"text": " writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme','--tester-port',TESTER],schema:store,activation:'manual',at:'2026-10-20T00:00:00Z'})+'\\n');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2198,
"text": " ['beta\\'s tester port on alpha\\'s port',b,['--port',free,'--tester-port',a.port],1,`webui-service: refusing port-claimed: ${held}; nothing was changed\\n`],"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2202,
"text": " ['beta\\'s tester port on 7333',b,['--port',free,'--tester-port','7333'],4,'webui-service: port-reserved: 7333 is reserved for the default instance (7330 7332 7333)\\n'],"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2217,
"text": " await refused('--tester-port','--tester-port',a.port);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2280,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2290,
"text": " r=await run(e,'activate',C1,'acme','--tester-socket','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/--tester-socket/);assert.match(r.out,/drop.*--tester-port 7333/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2292,
"text": " assert.deepEqual(pointer(e).console,[...base,...pair(proof),'--tester-socket']);assert.match(consoleText(e),/--tester-socket/);assert.doesNotMatch(consoleText(e),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2319,
"text": " assert.doesNotMatch(conExec(e),/ --send(?: |$)|--tester-port|--public-host|--proof-header/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2388,
"text": " assert.equal(context.argv.includes('--tester-socket'),marked);assert.equal(context.argv.includes('--tester-port'),false);"
},
{
"file": "packages/cli/tests/case1-accept.test.mjs",
"line": 28,
"text": " \"--node\", NODE, \"--claude\", CLAUDE, \"--pm\", \"--\", \"--port\", \"732\", \"--business\", \"mosaic-stack\", \"--tester-port\", \"7333\"];"
},
{
"file": "packages/cli/tests/case1-accept.test.mjs",
"line": 477,
"text": " \"no tester port\": [(h) => put(h.args, ARGS.slice(0, -2).join(\"\\n\") + \"\\n\", 0o644), /lacks a numeric --port or --tester-port after --$/],"
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 68,
"text": " \"[Service]\\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\\n\");"
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 215,
"text": " put(join(h.root, \"etc\", \"systemd\", \"system\", \"mosaic-webui.service\"), \"[Service]\\nExecStart=/x serve --business mosaic-stack --tester-port 7333\\n\");"
},
{
"file": "packages/cli/tests/stack-release-external.test.mjs",
"line": 75,
"text": " for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){"
},
{
"file": "packages/webui/README.md",
"line": 128,
"text": "need `--port`, and 7330, 7332 or 7333 as `--port` or `--tester-port` exits 4"
},
{
"file": "packages/webui/README.md",
"line": 130,
"text": "`--tester-port` that another installed Console unit in the unit directory"
},
{
"file": "packages/webui/README.md",
"line": 144,
"text": "`serve --sso [--send] [--tester-port N]` makes the Console an OpenID Connect"
},
{
"file": "packages/webui/README.md",
"line": 176,
"text": "- With `--tester-port`, the tester listener refuses any request carrying an"
},
{
"file": "packages/webui/README.md",
"line": 1001,
"text": "There is no tester TCP listener. `--tester-port` is retired for this copy."
},
{
"file": "packages/webui/tests/tester-cli.test.mjs",
"line": 25,
"text": " for (const flags of [['--tester-port', '7332'], ['--port', '7444', '--tester-port', '7444'], ['--tester-port', '7444', '--port', '7444'], ...['1023', '65536', '1.5', '-1', 'nope'].map(p => ['--tester-port', p])]) {"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 125,
"text": " const r = await run(dir, ['render', '--tester-port', port, ...PREVIEW, '--send']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 127,
"text": " assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 129,
"text": " assert.ok(execStart(r.stdout).endsWith(` ${PREVIEW_FLAGS} --send --tester-port ${port}`));"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 136,
"text": " const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 150,
"text": " ['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 151,
"text": " ['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 152,
"text": " ['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\\n'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 153,
"text": " ['--port', '7444', '--tester-port', '7444'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 154,
"text": " ['--tester-port', '7444', '--port', '7444'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 164,
"text": " assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 172,
"text": " const six = await run(dir, ['render', '--tester-port', '007333']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 175,
"text": " assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\\n');"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 176,
"text": " const five = await run(dir, ['render', '--tester-port', '07333']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 178,
"text": " assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 186,
"text": " const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...PREVIEW, '--send'], bin);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 188,
"text": " assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 189,
"text": " assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 199,
"text": " const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 799,
"text": " const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send', '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 813,
"text": " const both=await run(dir,['render','--tester-socket','--tester-port','7333']);"
},
{
"file": "packages/webui/src/cli.mjs",
"line": 30,
"text": " if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket');"
}
]
}
+313
View File
@@ -0,0 +1,313 @@
# 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.
Sage-2463 integration onto 0fa7bf421: new gander/row253-0fa7 preserves both earlier branches. Only BUILD-LOG append conflicted; current upstream remains an exact prefix. README auto-merged with row191 processing controls. Focused row253 tests do not read the changed Console/processing code, but WebUI whole covers it on this base. Only README changed in the manifest; refreshed digest 24b6a01b2ede6f025ec898fa4ec830bb9585ce6151d6298e713e31c7430a502b. integration-0fa7/ records source-only dependency inspection, range-diff and every commit patch-id. No test/syntax/probe ran.
## Re-integration under sage-2659, no runs
New branch gander/row253-385a on385a7194c, rebased source141b79c29. Only the BUILD-LOG append conflicted; kept upstream as exact prefix. Runtime scripts, focused tests and shared fixture match8de88458d byte for byte. DEFERRED/README absorb upstream additions; manifest72a70e99ba0416ceb1f63ed737132ebbeb212332d1441c362b3dfa6c8b00209c. integration-385a/ holds the range-diff, full and no-log patch-ids, upstream manifest diff, test import/read drift and40 unique targets. No test or Node ran.
The focused tests indirectly import Store through CLI helpers, so they read/hash-check current schema.sql and migrations. 264a changes3k-to-3l ahead of this gate and needs a dependency re-check before CLEAR, even if its manifest overlap is empty. Whole stack-release T46 also reads current schema. case2 sandbox availability skip remains named under Duckworth0116 N9. The real Console files moved in278, but the focused tests write Console/config stand-ins; wholeWebUI covers the real changes.
SHELL-LIMITS.json now includes fresh278 run3 receipts. Same ceil(3*longest)+120 formula; foundation maximum39.901s gives240s. window.py uses each recorded whole-shell limit, and driver.py classifies GNUtimeout124/137 as timeout, never kill. No script/product/assertion changed. Historical row253 results carry nothing after C4.
| Shell | Longest recorded seconds | Limit seconds |
|---|---:|---:|
| scripts/test-auth.sh | 1.908123 | 126 |
| scripts/test-conductor.sh | 8.057174 | 145 |
| scripts/test-config.sh | 3.167892 | 130 |
| scripts/test-discord.sh | 8.776728 | 147 |
| scripts/test-extension-package.sh | 3.012857 | 130 |
| scripts/test-foundation.sh | 39.901000 | 240 |
| scripts/test-land-work.sh | 12.623263 | 158 |
| scripts/test-queue.sh | 23.443742 | 191 |
| scripts/test-release.sh | 12.168674 | 157 |
| scripts/test-task.sh | 36.783798 | 231 |
## Post264a integration, sage-2698, no runs
New gander/row253-264a rebases eleven commits onto84f53540d without conflicts; every pair and patch-id equals the385a branch. Rebased sourceb9d3d4c75c78aa6b06aefd734da781bef9b7f1da. Only DEFERRED absorbs upstream changes; seven-path manifest735fa2d7405d01ace8eb7aab366753fa5b1877cf8044b0151cb2113a0502bc1f. All runnable253 blobs unchanged. Store now loads schema3l and3k-3l migration from the landed base. integration-264a/ records the exact upstream drift and27 whole runnable test-file union (upstream-changed tests plus own focused files). Shared fixture is covered by importers. No carried result. Ask Sage for focused CLEAR, then publish after those whole files pass, then Duckworth rebase read, then whole gate on another CLEAR. N9 exact sandbox skip reason must be copied from the case2 log. No execution or push ran during integration.
## Corrected docs base, sage-2700/2702, no runs
New gander/row253-b789 rebases ontob78918850. Sourcec75c9bbd3339cb2343467715a14455044c2e4aea. Only append conflicts in BUILD-LOG and DEFERRED: current base remains exact byte prefix,253 entries appended. No old entry edited or removed. Manifest573c51cf72f2398532a6ea768536f04e3c1681997cb8b9a6f14a8aef884870ae. Five runnable blobs and README match preceding candidate; only DEFERRED hash changes.27-file whole union unchanged, no result carries. Hold branch local: final Gizmoduck docs tip must land first, then rebase/publish and focused CLEAR. No tests/Node/push. integration-b789/ records the resolution and hashes.
## Final base aca16404c, sage-2705, before execution
New gander/row253-aca1 rebases13 commits ontoaca16404c without conflicts; all13 final-hop pairs equal. Sourceed3bb376cdd5128dc58b3aa193282e1c2bfcf57d. Seven blobs unchanged, manifest573c51cf72f2398532a6ea768536f04e3c1681997cb8b9a6f14a8aef884870ae. Both append paths retain the full base prefix. Final upstream docs/queue changes touch no focused read.27-file union remains unchanged.
Step2 file bounds: every file listed in integration-aca1/FILE-LIMITS.json runs whole as timeout -k10 300 node --test --test-reporter=tap FILE, outer harness330s. Own case1/case2 first, then25 others. Busy check before each start, pipefail and tee, TMPDIR from mktemp-d /mnt/storage/scratch/tmp/gXXXXXX. First unexpected failure or timeout stops to Sage/Mos unplaced; no retry. Print HEAD and manifest digest first. No test has run. Expect10-15min; status at15min. Whole-suite limits are separate and will be written before whole-gate CLEAR.
Whole-shell limits remain as in the earlier table and SHELL-LIMITS.json:auth126,conductor145,config130,discord147,extension130,foundation240,land-work158,queue191,release157,task231. Each is at least3x its longest recorded successful whole run. No script changed.
## Focused27 green under sage-2709
Fresh candidate0604d91fc onaca16404c, manifest573c51cf. HEAD and digest are the first controller-log line, followed by the full900s CLI/300s bus-WebUI limit header before any test. Actual run14:30:03Z to2026-10-22T14:44:57Z, 894.0 wall seconds. All27 files whole604/0/0, timeout0, no retry or busy refusal. Commands use timeout-k10 per file, pipefail/tee and scratch gXXXXXX TMPDIR. No product/test/assertion changed. Entire seven-path manifest verifies after the run.
N9 did not skip. Verbatim case2 result: `ok 43 - inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds`. There are no skip lines in any focused file. Later wholeWebUI and shell skips are not measured by this focused scope.
Every measured file duration and its later3x basis follows. The last column is ceil(3*measured whole-file wall)+120, not a completed whole-gate result. Historical C4 gates carry nothing. The whole gate still awaits separate CLEAR and Duckworth rebase review.
# Focused27 whole-file results
| Whole file | Pass/fail/skip | Wall seconds | Command limit seconds | Next3x plus120 basis |
|---|---:|---:|---:|---:|
| packages/cli/tests/case1-accept.test.mjs | 34/0/0 | 99.281 | 900 | 418 |
| packages/cli/tests/case2-unit.test.mjs | 43/0/0 | 66.418 | 900 | 320 |
| packages/bus/tests/account-migration.test.mjs | 3/0/0 | 0.415 | 300 | 122 |
| packages/bus/tests/chief-of-staff.test.mjs | 42/0/0 | 2.168 | 300 | 127 |
| packages/bus/tests/comment-migration.test.mjs | 5/0/0 | 0.565 | 300 | 122 |
| packages/bus/tests/end-launch.test.mjs | 7/0/0 | 0.415 | 300 | 122 |
| packages/bus/tests/fire-revokes-grants.test.mjs | 22/0/0 | 3.271 | 300 | 130 |
| packages/bus/tests/gitea-grant.test.mjs | 49/0/0 | 8.634 | 300 | 146 |
| packages/bus/tests/gitea-migration.test.mjs | 5/0/0 | 0.415 | 300 | 122 |
| packages/bus/tests/grant-executor.test.mjs | 42/0/0 | 21.562 | 300 | 185 |
| packages/bus/tests/grant-migration.test.mjs | 6/0/0 | 0.465 | 300 | 122 |
| packages/bus/tests/lifecycle-revoke.test.mjs | 8/0/0 | 0.665 | 300 | 122 |
| packages/bus/tests/message-filter.test.mjs | 6/0/0 | 0.515 | 300 | 122 |
| packages/bus/tests/name-migration.test.mjs | 1/0/0 | 0.214 | 300 | 121 |
| packages/bus/tests/schema.test.mjs | 1/0/0 | 0.214 | 300 | 121 |
| packages/bus/tests/sessions.test.mjs | 7/0/0 | 0.615 | 300 | 122 |
| packages/bus/tests/spec-migration.test.mjs | 1/0/0 | 0.214 | 300 | 121 |
| packages/bus/tests/tester-writes.test.mjs | 8/0/0 | 0.565 | 300 | 122 |
| packages/bus/tests/thread-claims.test.mjs | 6/0/0 | 0.264 | 300 | 121 |
| packages/bus/tests/thread-migration.test.mjs | 3/0/0 | 0.365 | 300 | 122 |
| packages/bus/tests/threads.test.mjs | 17/0/0 | 1.216 | 300 | 124 |
| packages/bus/tests/usage-migration.test.mjs | 1/0/0 | 0.214 | 300 | 121 |
| packages/cli/tests/lifecycle.test.mjs | 24/0/0 | 13.296 | 900 | 160 |
| packages/cli/tests/stack-release.test.mjs | 218/0/0 | 583.453 | 900 | 1871 |
| packages/cli/tests/thread-cli.test.mjs | 11/0/0 | 1.876 | 900 | 126 |
| packages/cli/tests/thread-launch.test.mjs | 11/0/0 | 16.603 | 900 | 170 |
| packages/webui/tests/chat.test.mjs | 23/0/0 | 0.515 | 300 | 122 |
N9 verbatim: ok 43 - inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds
Sweep0 run-created leftovers. Initial broad sweep found nine current MCP/npm/code-mode infrastructure processes, all started14:20:57-14:21:07Z before this run14:30:03Z. UTC ps and /proc start ticks are retained; initial output saved, no signalling. Deleted cwd under run TMPDIR is included. No old-PID assumption replaces the receipt check.
Sage2711/2714 reported unrelated queue reads/write and Python reads by other seats. First and subsequent busy checks passed; no retry was needed and no effect is attributed to those processes. No live data was read by this seat. Raw command/log paths and all27 totals live in run3-focused/files/results.json and SUMMARY.json. Return slot after the record publication; no whole suite or M1-M40 batch starts here.
## Whole gate2737/2739, before execution
HEAD34401e7c9, product0604d91fc, manifest573c51cf. Outside agents/gander the record-tip diff is only BUILD-LOG.md, skipped by gate-lane-auth SKIP_COPY per Sage2739. Full lists/counts are printed in run3-gate/HEADER.json before first test. Serial CLI whole55 files,WebUI whole58 files, plus the20 bus files in the rebase union (subset coverage, not a bus-whole claim). Every Node call including RED/controls/changes is timeout-k10 with its killer/whole file measured bound, or900 for an unmeasured file, outerwait+30. WHOLE-GATE-LIMITS.json pins this. The two touched case files also run whole after intended REDs. M1-M40 then serial package/union files and10 shells; every mutation restores exact bytes/status before next. Shell table unchanged. First unexpected pre-existing red/product suspect/loosened assertion/timeout/second red stops unplaced. First-run own new-test red may be placed under2737, but no automatic loosening. No whole-gate test run yet.
## Whole gate2737 stopped unplaced atM21
Run started14:59:22Z. Two intendedREDs observed, exact restoration. Touched whole case243/0/0 in66.112s; case134/0/0 in93.721s. N9 ran and passed. M1-M20 killed;M21survived1/0 in8.581s against8.532s base.9controls passed,0timeouts. M22-M40 and all full-package/union/shell stages NOTRUN. Driver restored bytes/status; all7manifest checks pass, packages/scripts diff empty. Sweep0run-created leftovers. Stopped and returned slot as0106 before any retry/edit.
M21 drops only the peer regex end anchor. peer-malformed already writes a valid three-number prefix plus extra text, but the aggregate passingTAP does not expose the returnedreason. No loggedP10line can be quoted fromM21 orM17-base. Static trace shows onlyP10peerprobe executes PYTHON_STUB; configandearlierchecks useNode/shell, while tester_python reads metadata/ELF only. See run3-gate/STATIC-TRACE.md.
Mos1293requires a scratch-base diagnostic onSagegap, all17P10lines, then exactdesignreasonassertions. peer-malformed expectsprobe. Unexpecteddesignreason or anyproductchange goes toMosbeforeedits. No diagnostic or repair yet. Newcandidate/manifest/review and entiregatefromstart, no carry. Stop packet committed locally; helperpushwaitsforgap.
## Fixture-only M21 repair, mos1297, not run
Diagnostic(a) on0604 observed probe-inner-session for peer-malformed andpeer-error, not their designed probe/probe-inner-command. Mos confirmed instant shell output exits before session verification; product fails closed. Authorized option1 uses the same system Python exec for peer-output bytes/status; no delay and no product change. Sourcecommit362ec674c9e7aa8cef0c26f4d7448f459171dbd4 names1297 andquotesoldline100.
All17faults now assert exact reason with following space and anchoredP10prefix, from the accepted draft. Every old assertion retained. C4overflow andpeer-hang test bodies are byte-identical; overflow must remain probe-output-limit. Peer-hang looseprefixoutofscope unchanged. STATIC-PRESERVATION.json andREPAIR.diff in repair-1297/.
Seven-path manifesta2ac57342d9c59ddaebddade0fa475a9568719fed50c132bd7bb141167c7e72e; onlycase1testhashchanges. No repaired-source test/syntax/Node execution. Whole gate from start, no carry, after Duckworth review and SageCLEAR. M21survival orwrongmalformed/errorreason goes toMosbeforeanychange. Product fast-exit issue isVikunja532post10-27; SageownsDEFERREDentry.
### mos-1297 fresh gate under sage-2768, stopped
Ran on pushed e1bc766d8, manifest a2ac5734, no carry. Both intendedREDs observed; touched whole43/0/0 and34/0/0,17fresh controls passed. M1-M37 killed including M21 by named P10 assertion; M38 survived (drop setsid, signals killer). M39-M40 and full package/union/shell stages NOTRUN. Wall404.837s,0timeouts,0skips,N9passed. Exact restoration and all7manifest checks pass; sweep0run-created leftovers. Stopped unplaced to Sage/Mos/Duckworth as gander-0116 and returned slot; no repair or rerun. run4-gate/ preserves raw logs,commands,counts and timing.
### mos-1298: pin the direct query session
M38 drops setsid but a blocking signals query can still fail closed with inner-timeout; the old kill rule was wrong. The named killer stays row253 C4 signals query. Every sleep/signals/member case in both exec/hop modes now reads the first query-pids line and asserts exact equality of its numeric PID and kernel SID. M38 must fail this new assertion, never a timeout or a cleanup-only assertion. Every old line of the C4 query test is preserved byte for byte. Product scripts remain byte-identical. No test or Node preflight run; fresh whole gate from start follows new candidate review and Sage CLEAR, with nothing carried. Base assertion failure, another survivor or any mismatch goes to Mos before a further change.
@@ -0,0 +1,9 @@
{
"docs/plans/DEFERRED.md": "f8ad28aa8dc0f12b2c6dddb3b5dc805da05a97c7723e0525905456c064d0cfa3",
"packages/cli/tests/case-sudo-fixture.mjs": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
"packages/cli/tests/case1-accept.test.mjs": "27378eb1297dc6115b4b1ce849de6ffdb3adccf5249783106fa0570c9d696e8a",
"packages/cli/tests/case2-unit.test.mjs": "9ca67403d8614266061624ab5fbf4ea8e8a54cbd08f5b569c95592ab5ed40472",
"packages/webui/README.md": "ce9054060867120798c6b26737ac7d910d8cfbc7aa4ad614f4295619c7605e46",
"scripts/case1-accept.sh": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"scripts/case2-unit.sh": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
}
+147
View File
@@ -0,0 +1,147 @@
# Row253 C4 addendum: the supervisor belongs after sudo
Draft for Duckworth's static ruling. DESIGN.md remains unchanged. Authority:
duckworth-0109/0110, RULING-c4.md at 2c49dd80c, and sage-2307/2312.
No repaired product code or new test has run. Product scripts still have the
original frozen bytes; this addendum does not claim they satisfy C4.
## Finding and evidence
The current tester_capture puts GNU timeout --signal=KILL outside sudo, then
kills that timeout's process group. It works for the exec-style sudo fake.
sudo(8), **Process model**, says the pty monitor creates a new terminal session
and forks the command there. **Signal handling** (called Signals in the local
rendering) says SIGKILL cannot be caught and relayed. Therefore an outer group
kill does not establish that the query in the new session is killed.
Evidence was a static read of the installed sudo(8)/sudoers(5) documentation,
then Duckworth's independent ruling against sudo 1.9.17p2. No real sudo, host
experiment, live unit or live-port experiment ran. The shared fake below is
written but unrun. It models the session boundary, not authentication or a pty.
## Proposed trusted supervisor and runtime trade
Recommend a reviewed inline Bash supervisor, run **after the uid drop**, using
root-validated fixed system Bash, setsid, sleep and kill/builtins. These are
host tools, never binaries or scripts from the selected copy. Resolve system
symlinks to their real targets; require executable root-owned ELF files and
safe root-owned ancestors, outside homes. Do not use PATH lookup for these
supervisor tools. env -i removes BASH_ENV and other injected runtime settings.
Add setsid to the documented host dependencies. No new configuration knob.
This is a proposal under R1's root-validated system-binary option, pending
Duckworth's approval of Bash rather than the example GNU timeout. The exact
inline supervisor must be identical in both host scripts, with budgets and
command argv passed separately. No helper is imported from the copy.
Alternatives weighed:
- GNU timeout alone inside sudo fixes the session hop but kills one process
group. It misses a grandchild that uses setpgid without setsid. It needs the
same owned-session cleanup as the proposed supervisor.
- Validated --node with inline builtins can implement the same supervisor.
The installer already validates it. Acceptance deliberately reports unsafe
--node paths through F9/F10; making that binary a required trusted supervisor
would change those existing outcomes or need another pinned runtime.
- Fixed root-validated system Python can fork a session, multiplex bounded
output and scan /proc cleanly. It is already used for P10, but making it
mandatory for the config query changes missing/unsafe Python from a failed
P10 observation into a preflight refusal. Preserve that behavior here.
## R1: own a query session, not sudo's group
The supervisor starts in sudo's possibly separate session as the stack uid.
It forks the query child, then setsid occurs in that child through the fixed
system setsid binary. The supervisor itself may already be a group leader,
so it must not attempt setsid in its own process. Disable Bash job control;
the newly forked child is not a group leader, so setsid need not fork again.
The child's PID from $! is the kernel session id. No PID comes from copy output.
Run a separate five-second watchdog inside the uid boundary. On expiry, on a
supervisor catchable signal, on output refusal, and after normal query exit:
1. Scan numeric /proc/*/stat entries for the owned session id. Parse after the
final closing parenthesis, so spaces, parentheses and newlines in comm do
not shift the kernel fields. Use the kernel session field, not ancestry.
2. SIGKILL every live PID in that session, including different process groups.
Repeat until no running members remain or a fixed 250 ms cleanup budget
expires. Never select a process from a different session.
3. Wait for the direct child and the watchdog. Refuse on a cleanup limit or
read/parse error that prevents establishing cleanup. Zombies are not running;
only direct children can be reaped here. Tests pin that distinction.
A five-second deadline concerns the inner query. The root-side deadline is
independent. Query bytes are accepted only after success and completed cleanup.
The query can never authorize a root kill: its stdout supplies no PID/session.
The supervisor runs without root authority and kills only its owned session.
## R2: independent root bound and bounded output
Root waits at most seven seconds for the metadata sudo call. Keep GNU timeout
outside sudo for this separate purpose. Keep the 4096-byte cap plus one-byte
overflow detection. All partial output is discarded on any refusal.
Give outer timeout its own operator reason, distinct from inner timeout,
child failure, output limit and cleanup refusal. A child-supplied exit value
alone must not masquerade as proof the outer timer fired. Capture GNU timeout's
trusted verbose signal notice separately, with sudo/helper stderr redirected
away from that channel. The root-owned notice file is outside the user's
home. Validate the combination of notice and status; no eval or source.
Apply the same primitive to the new P10 peer probe, with inner two seconds,
outer four seconds, a 128-byte output cap and the existing one-second socket
connect timeout. Preserve its fixed system-Python safety check and failed-P10
behavior. All observed stdout still requires successful cleanup and exact
pid/uid/gid validation. Neither path sends application bytes to the socket.
## R3: narrowed claim and residual
C4 now means: killed at the inner bound, and root never waits past the outer
bound. A hostile same-uid query can kill/stop the supervisor or setsid away.
A hang in the trusted supervisor has the same outer-bound outcome. In that
case root refuses and discards stdout; the separate session may remain.
This is the existing DESIGN.md:97-99 same-uid limit, not a new closure claim.
No root-side cross-session killer is added. It would require another row.
The stalled-supervisor fake must prove outer timing, its own refusal reason
and no use of partial stdout. It then explicitly kills its recorded leftover
session as fixture cleanup, naming that step as the R3 residual. It must not
claim the product cleaned that session. Other normal-supervisor cases assert
no running members of the recorded query session remain after the call.
## R4 and F5: headers and remaining legacy calls
Both headers will name the inner supervisor, its runtime/tool validation,
five/seven-second query bounds and two/four-second peer-probe bounds.
The old F3 talk path (currently timeout 330 around AS_USER talk) has the same
sudo session-hop issue. Propose recording it as a bounded follow-up, rather
than changing it in this slice: it predates the socket metadata/probe helpers,
drives a 300-second functional conversation and simultaneous F5 scan, and has
existing stdout/stderr diagnostics. It is not part of the new five-second
config query. This is an explicit F5 scope choice for Duckworth/Sage to accept
or reject, not a claim the old talk timer kills a separate session. Likewise,
the other legacy AS_USER diagnostics are not newly supervised by this row.
## Shared fake and next evidence
packages/cli/tests/case-sudo-fixture.mjs provides the one process-model seam.
Both existing test files import it. Mode exec retains the prior fake. Mode
hop keeps a parent, forks through setsid, records kernel PID and SID, and
relays TERM/HUP/INT only. It cannot relay KILL. The installer draft case makes
the copy ignore TERM/HUP and block; it checks recorded query PIDs and the
separate fake session, then explicitly cleans fixture-owned PIDs on failure.
This is written but not run, and is expected to expose the old product gap.
Before the next whole gate, complete both-mode cases for timeout, TERM/HUP,
setpgid grandchild, overflow, and the distinct stalled-supervisor residual.
Both host-script paths use this seam. Add changes for absent inner supervisor,
absent outer bound, leader-group-only cleanup and unbounded read. The setpgid
case must kill the group-only change. Record every restore and raw log.
Duckworth reviews this addendum and fake statically first. Product code then
implements the approved mechanism. All product changes void prior results
under sage-2307/mos-1200. Only Sage's next CLEAR permits a diagnostic or gate
run. The next gate is both whole touched files, all checks, CLI/WebUI whole
and all ten shell files. Existing RED evidence carries only if applicable
product bytes remain equal. No host deployment is authorized by this packet.
+248
View File
@@ -0,0 +1,248 @@
# Row 253: private tester socket in the host scripts
Gander, 2026-10-21. F27, #1758. Base c3d88840c, after row196 landing
7247d80c7. Brief: docs/plans/2026-10-21_tester-socket-scripts.md at
0cdead66c. Reviewer Duckworth; gate owner Sage. Status: proposed, no product
code or tests changed. Claim gander-253-start is queue rev2995.
## Outcome and scope
The factory installer renders a bare --tester-socket flag instead of a TCP
port. Case1 acceptance consumes that saved form and examines the private
listener without claiming that a private pathname defeats hostile same-uid
processes. The library profile still has no tester. No .53 or .54 host may
take a copy carrying196 before253 lands, per mos-1198.
Change scripts/case2-unit.sh, scripts/case1-accept.sh and their two existing
CLI test files. Their comments/usage and fixture docs move with the code.
Do not edit the deliberate legacy paths named in the brief. No runtime bus,
Console, role, grant, control-board, dependency or systemd-template change.
No live installer or acceptance run, provider, home/config, unit or host access.
## Choices
1. Recommended: resolve the configured dataRoot as the stack user, preserve
hardening, check the final pathname's metadata and use SO_PEERCRED on a
connection to identify its listener. This supports custom roots within the
unit's existing writable home and matches196's kernel-peer approach.
2. Assume <home>/.mosaic-dev and inspect a pathname in /proc/net/unix. This
misses a custom config and is not a reliable final-path ownership check:
196 binds a temporary name and renames it. A socket's filesystem inode also
must not be confused with its kernel socket inode. Do not use this option.
3. Add a TCP relay, relax hardening or give acceptance a configurable probe
executable. All widen the surface or trust inputs and are outside this row.
There is no second bus principal verifier. The acceptance probe observes an
OS endpoint; it grants no capability and sends no HTTP or bus request.
## Installer arguments and preflight
Factory keeps required --port (1..1023) and --business; tester remains optional
for the installer. --tester-socket is a single marker, takes no value and keeps
its position in the rendered flags. The existing duplicate-flag check applies.
A following stray value is an unknown flag. --tester-port always refuses in
serve arguments, for both profiles, before unit writes or systemctl actions:
case2-unit: case2-unit.sh retires tester TCP; use --tester-socket
Library rejects --tester-socket with an explicit library-profile refusal. Its
other flags, proof-file checks and install behavior are unchanged. No legacy
conversion or fallback to TCP is introduced in this installer.
When the factory socket marker is present, a small inline metadata query runs
the selected copy's config loader under the target uid with sudo -u USER -H,
env -i, explicit HOME and the same pinned-node-first PATH used by the unit.
Root never imports user-writable copy code as root. It invokes the previously
validated --node path after dropping uid, with the copy module path as a
separate argument. This resolves the default instance just as the rendered
unit does; no MOSAIC_CONFIG or named-instance override is inherited.
The query returns only dataRoot and whether that copy's Console carries the
MOSAIC_TESTER_SOCKET_V1 marker. It prints no config values, credentials or child
diagnostics. Bound it to five seconds and 4096 output bytes; reject errors,
extra output/keys, malformed results, missing config and unsupported targets.
No root process opens a config or credential under the user's home. Existing
root metadata checks remain root metadata checks. Both script headers explain
the new, explicitly unprivileged config query.
Before any unit writes, validate the socket location:
- dataRoot is an absolute normalized plain path, below the declared home,
with no symlink in its existing path. Never expand the unit's writable scope.
- Neither home nor dataRoot is under /home, /root, /run/user, /tmp or /var/tmp.
These roots conflict with the existing ProtectHome/PrivateTmp contract.
- <dataRoot>/console/tester.sock is at most 107 ASCII bytes, matching196.
- A missing console parent is allowed; runtime creates it. An existing parent
must be a real directory, not a symlink, owned by the target uid with no
group/other permissions. A foreign owner, non-directory or unsafe mode refuses.
- Do not create/chmod the parent or inspect/delete an existing tester entry in
the installer. A running Console may own it during a re-pin. Runtime196 owns
binding and cleanup. Reinstall still does not restart running units.
The dataRoot query and all refusal families get fake-only tests. A failed
preflight leaves both units and the systemctl log unchanged.
## How the rendered unit reaches the socket
The unit still has User/Group for the stack account, ProtectSystem=strict,
ReadWritePaths=<home>, ProtectHome=yes and PrivateTmp=yes, in the same order.
Its ExecStart contains --tester-socket, with no pathname argument. The Console
loads dataRoot and derives console/tester.sock itself. Case1/2's /srv/mosaic
home is outside ProtectHome's protected hierarchies; the allowed dataRoot is
inside the existing writable home, and the socket is outside private /tmp.
No extra capability or writable directory is added. Systemd's documented
namespace rules motivate the location refusals, rather than a hardening change.
[systemd.exec](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html)
A mutable same-uid config can change between preflight and start. The unit's
writable boundary is still home; runtime's own config/socket checks still
refuse unsafe state. This row does not promise an immutable same-uid config.
The host window must verify the rendered unit actually starts under its
namespace; scratch tests inspect its directives, not a real systemd namespace.
## Acceptance arguments and endpoint observation
The saved args must contain one --tester-socket and the numeric --port.
Reject a missing/duplicate marker, a marker with an old numeric value, and
--tester-port with the same migration wording naming case1-accept.sh. Preserve
all existing pre-- argument validation and the default-instance contract.
Malformed saved inputs still refuse with exit2 before functional checks.
Acceptance resolves the actual configured tester dataRoot with the same
unprivileged, bounded query. It does not silently use the default path. An
unreadable/invalid config or a root outside the unit's allowed home refuses
before the functional talk. The old DATA_ROOT used by F11 remains unchanged;
generalizing that separate Discord check is outside this row.
P10 independently requires the Console's systemd MainPID to be positive and
stable, and its four /proc uid fields to be the stack uid. Check the private
parent's type, owner and mode, and the final entry's non-symlink socket type,
owner and exact 0600 mode. Snapshot parent/socket dev+ino and metadata before
and after the peer probe; a replacement or MainPID change fails observation.
The port floor must still be numeric and above the privileged main Console
port, retaining that existing part of the observation.
The bounded probe runs as the stack user with an empty inherited environment,
the fixed system /usr/bin/python3 resolved to a root-owned ELF, safe root-owned
ancestors, and -I -S. This is the system Python dependency already required by
196; never use PATH python, a home shim or a configurable helper. Unsafe/missing
Python is a failed P10 observation, not a fallback. Resolve/check the system
entry and target using metadata and the ELF header; do not run copy Python code
as root. Test seams emulate these root facts and the system executable.
The probe opens AF_UNIX/SOCK_STREAM, uses a one-second connect timeout, reads
SO_PEERCRED and closes. An outer three-second process-group bound applies. It
returns only pid/uid/gid, with exact structure, bounded output and numeric
validation. pid must equal MainPID and uid the stack uid; gid is validated but
not an identity grant. No application bytes, cookies, credentials or proof
envelope are sent. Linux supplies the peer credentials for the connected
stream, so the check does not depend on the temporary bind pathname.
[unix(7), SO_PEERCRED](https://man7.org/linux/man-pages/man7/unix.7.html)
This is a snapshot. Hostile same-uid processes can alter same-uid state or
interfere with the unprivileged probe. Root, ptrace and later pathname changes
are not defeated here. The probe does not prove another uid is denied; that
remains a host-window observation, not a fake test claim.
## P8 and P10 outcome lines
P8 must change with removal of TESTER_PORT. It checks the main Console port is
loopback-only and held only by MainPID, and that nothing listens on TCP7331 or
the retired TCP7333. Socket ownership belongs to P10. Its healthy line is:
P8 lan-listeners closed <port> is on 127.0.0.1 only by MainPID <pid>; no TCP listener on 7331 or 7333; tester uses <socket>
An unexpected listener, missing main listener or wrong owner produces P8
lan-listeners unnamed with the observed listener summaries, as today.
Do not reuse accepted mos-1101 for a new Unix-socket claim: that ruling named a
TCP-port condition. Proposed safe default pending any new Mos classification:
P10 tester-socket-squat unnamed boundary=observed path=<socket> parent=0700 owner=<uid> socket=0600 peer_pid=<pid> peer_uid=<uid> floor=<floor> port=<port>; hostile same-uid substitution remains
Any missing/unsafe metadata, wrong peer, unsafe interpreter, malformed/timeout
probe, floor miss or changed snapshot produces one P10 line instead:
P10 tester-socket-squat unnamed boundary=failed reason=<bounded-code> path=<socket> MainPID=<pid>; hostile same-uid substitution remains
Healthy observation does not mark the same-uid squat path closed or invent an
accepted ruling. P checks still never change exit status. Tests pin the reason
and observed/failed distinction, so validation cannot disappear behind an
always-unnamed mark. Sage/Mos may supply a new classification at design review;
it must be explicit before changing this proposed mark. No Jason question is
needed merely to retain unnamed evidence.
## Evidence and planned checks
Keep the existing scratch fixtures, fake account database/systemctl/curl/ss/
sudo/stat and fake interpreter. Add copy config/marker fixtures and probe
metadata under those same roots. No test connects to a live socket or runs the
real host scripts without their existing seams. Fixture fakes verify the
unprivileged command argv; no test claims a real uid drop or namespace.
The fake system interpreter executes the actual probe payload against a fake
Python socket API, checking AF_UNIX/SOCK_STREAM, timeout, connect, getsockopt
arguments and returned peer bytes. This catches errors in the inline probe
without opening an OS listener. The metadata query is tested with nonzero
status plus valid-looking output as well as malformed output, so removing its
status refusal cannot hide behind a later parse error.
| Checks | Required observations |
|---|---|
| T1 factory render | Bare marker exactly once, argument order retained, no TCP flag, hardening unchanged; duplicate/stray value refuses |
| T2 profile/migration | Both profiles refuse retired TCP; library refuses socket; all refuse before writes/start |
| T3 config/target | Invalid/missing/noisy/timed-out metadata and unmarked target refuse; query uses target uid and clean environment |
| T4 location | Default and custom in-home roots work; outside-home, protected/private hierarchies, symlinks and >107-byte pathname refuse |
| T5 parent | Missing parent allowed; file/symlink/foreign owner/group-other mode refuse without mutation or systemctl calls |
| T6 saved args | Required socket marker; missing/duplicate/stray value and retired TCP refuse exit2 before talk |
| T7 P8 | Main-only loopback succeeds; LAN/wrong owner/7331 or7333 listener gives unnamed |
| T8 P10 | Exact metadata, stable MainPID and matching peer produce boundary=observed; custom configured root used |
| T9 failed P10 | Parent/socket faults, wrong PID/uid, malformed/timeout probe, unsafe Python, bad floor and changed snapshot each give failed reason |
| T10 preserved behavior | Functional checks, their exit semantics, library behavior and all other P paths retained |
Planned checks to kill, one target change at a time in isolated snapshots:
| IDs | Removed requirement |
|---|---|
| M1-M3 | Installer TCP refusal, library socket refusal, duplicate socket refusal |
| M4-M5 | Config-query failure refusal, unsupported-target refusal |
| M6-M8 | Home containment, protected/private path refusal,107-byte limit |
| M9-M11 | Existing parent type/symlink, uid, mode checks |
| M12-M15 | Acceptance TCP refusal, required marker, duplicate/stray marker rejection, config-query refusal |
| M16 | P8 retired TCP7333 check |
| M17-M18 | P10 parent and final socket metadata checks |
| M19-M21 | Peer PID, peer uid, malformed/timeout probe refusal |
| M22-M23 | Safe fixed system interpreter, stable metadata/MainPID snapshot |
| M24-M25 | Target-uid invocation in installer and acceptance metadata/probe paths |
| M26 | Privileged main-port floor observation |
| M27 | Probe returns its own PID rather than the connected peer's credentials |
Before execution, give every definition one unique source target and a named
killer test. Preserve raw logs, base times, syntax preflight and byte-identical
SHA/Git restoration. A timeout counts only under Sage's explicit rules and
must kill the process group. Refusal variants in T1-T9 must not be merely
assertions of a generic nonzero exit.
## Static inventory and gate
After implementation, enumerate every --tester-port hit in scripts/packages
in BUILD.md and a machine-readable file. The only permitted hits are the brief's
deliberate legacy producers/parsers/tests and this row's explicit refusals and
negative fixtures. Inspect arguments and generated units, not just filenames.
Readmes must describe retirement rather than produce old argv. No modification
to webui-service, stack-release, instance, their tests/fixture, case1-hostcheck
or tester-cli negative calls is planned. Stop and ask Sage if one is needed.
Then request Sage's slot. Under that grant, run CLI whole, WebUI whole, every
shell test suite whole and all named checks above. Use an isolated candidate,
plain linked node_modules, a short gXXX TMPDIR under /mnt/storage/scratch/tmp,
busy &&, pipefail and tee. Single/pattern runs need the slot too, per sage-2142.
No harness/helper/log argument may match the busy detector pattern, per
mos-1205. Two failures end the window; report any stop to Sage/Mos before a
retry. Return the slot with script-summed counts/timings and a leftover sweep.
Design approval precedes code. The manifest, immutable candidate hashes,
static inventory and gate evidence go to Duckworth before Sage's landing.
Queue moves use the helper, with a coordinated queue-write gap. Sage posts the
jarvis review request after the in-review move. BUILD-LOG/session history stays
append-only; Sage continues session registration. No host deployment is part
of row253 acceptance.
+26
View File
@@ -0,0 +1,26 @@
# 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.
Superseding integration under sage-2463: gander/row253-0fa7 on 0fa7bf421. See integration-0fa7/. Only README changed among the seven landing entries, retaining row191 processing-control documentation; the refreshed manifest digest is 24b6a01b2ede6f025ec898fa4ec830bb9585ce6151d6298e713e31c7430a502b. No gate has run.
Superseding integration under sage-2659: gander/row253-385a on385a7194c, source141b79c29, digest72a70e99ba0416ceb1f63ed737132ebbeb212332d1441c362b3dfa6c8b00209c. Only README and DEFERRED hashes change. See integration-385a/ for drift, patch-ids and dependency reads. No gate run;264a schema dependency must be re-checked before CLEAR.
Post264a supersedes the385a preview: gander/row253-264a on84f53540d, rebased sourceb9d3d4c75c78aa6b06aefd734da781bef9b7f1da, manifest735fa2d7405d01ace8eb7aab366753fa5b1877cf8044b0151cb2113a0502bc1f. Only DEFERRED differs from the preview. See integration-264a/ for all11 equal pairs, patch-ids and whole-file union. Focused whole-file runs, publication, rebase review and full gate are pending Sage CLEAR.
Corrected interim integration2700/2702: gander/row253-b789 onb78918850, sourcec75c9bbd3339cb2343467715a14455044c2e4aea, digest573c51cf72f2398532a6ea768536f04e3c1681997cb8b9a6f14a8aef884870ae.27-file scope unchanged. Pending final docs tip, rebase and CLEAR; no gate run.
Final base under sage-2705: gander/row253-aca1 onaca16404c, sourceed3bb376cdd5128dc58b3aa193282e1c2bfcf57d, digest573c51cf unchanged. integration-aca1/ is the final-hop packet. No runs yet.
Focused27 under sage-2709 now passed on0604d91fc:604/0/0, no timeouts, no retries, N9 executed and passed (exact line in run3-focused/SUMMARY.json). All7 blobs unchanged;573c51cf remains the review digest. run3-focused/ contains every raw whole-file log/command, HEAD/digest/limits preamble, timings and3x basis. No full gate or mutation-batch result is claimed.
Whole gate2737 stopped atM21 survivor after20kills. No final acceptance. run3-gate/STOP-SUMMARY.json andSTATIC-TRACE.md preserve the stop. Mos1293requires diagnostic thennewcandidate/review/fullgatefromstart. Current7sourceblobs remain573c51cf; no test/product repair made.
Mos1297 supersedes573c51cf after one fixture-only repair and17exactreasonassertions: source362ec674c9e7aa8cef0c26f4d7448f459171dbd4, digesta2ac57342d9c59ddaebddade0fa475a9568719fed50c132bd7bb141167c7e72e. Onlycase1testchanges. repair-1297/ is the new review packet. Tests unrun; Duckworthreview and fullfreshgatepending.
mos-1298 candidate source 14e46006171ef700a72f971fda5cb3a0b8dcc4f2, seven-path manifest 591020a1096a1606c11f2ee34b37243acddf83d91c69922ccc5cfe22d6180d3a. Only case2-unit.test.mjs changes among manifest paths from e1bc766d8. Pending review and fresh full gate; no carry.
@@ -0,0 +1,7 @@
f8ad28aa8dc0f12b2c6dddb3b5dc805da05a97c7723e0525905456c064d0cfa3 docs/plans/DEFERRED.md
b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008 packages/cli/tests/case-sudo-fixture.mjs
27378eb1297dc6115b4b1ce849de6ffdb3adccf5249783106fa0570c9d696e8a packages/cli/tests/case1-accept.test.mjs
9ca67403d8614266061624ab5fbf4ea8e8a54cbd08f5b569c95592ab5ed40472 packages/cli/tests/case2-unit.test.mjs
ce9054060867120798c6b26737ac7d910d8cfbc7aa4ad614f4295619c7605e46 packages/webui/README.md
c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd scripts/case1-accept.sh
d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b scripts/case2-unit.sh
+37
View File
@@ -0,0 +1,37 @@
# Row253 implementation plan
Goal: migrate the two host scripts to the private tester socket without widening
their unit or root execution boundary. Spec: DESIGN.md, approved by
duckworth-0100 with C1-C5; Sage2173 approves the two design decisions.
Execution: Gander implements inline; Duckworth reviews the final manifest.
Constraints: four product/test files only; no edits to the brief's legacy paths;
no tests before Sage's slot, including patterns. Static inspection precedes the
full slot request. All tests use the existing scratch/fake seams.
- [x] Task1: write factory/acceptance socket fixture and refusal tests first.
Add config/marker and isolated probe fakes; assert target-uid argv, strict
parsing, all C1 path variants and C4 descendant cleanup. Preserve baseline
functional/P checks while changing only the intended socket outcomes.
- [x] Task2: implement the marker parsing, bounded target-uid query, strict bash
parser, component/no-follow location checks and parent preflight in installer.
Preserve hardening and update its header, including sudo dependency.
- [x] Task3: migrate saved args and P8/P10. Implement metadata snapshots and the
bounded isolated system-Python peer observation; keep P10 unnamed with explicit
observed/failed evidence and same-uid residual. Update header and remove the
obsolete accepted-mos-1101 mark. Keep F11 default DATA_ROOT behavior unchanged.
- [x] Task4: static audit every remaining retired-port occurrence; prepare unique
M1-M33 including C1 middle-symlink/sibling-prefix, C2 character, C3 parser and C4
process-group checks. Request the slot once the candidate static check passes.
- [ ] Task5: on Sage's grant, execute RED cases against isolated prior behavior,
GREEN candidate checks, CLI/WebUI whole, all shell files and the checks. Record
counts/times, clean restoration, leak sweep and exact frozen manifest. Return
the slot, then queue review only in a coordinated write gap.
Review focus: prefix collisions; intermediate symlinks; forged output lines;
nonzero query with valid-looking JSON; ignored signals/leftover descendants;
kernel versus filesystem inode; wrong Console peer or metadata replacement.
Ruling: tests are written first, but their execution waits for the user-required
slot after static preparation. No pre-slot test or passing claim; RED/GREEN
execution is an explicit part of the granted verification, not claimed in advance.
@@ -0,0 +1,768 @@
{
"command": "git grep -n -- --tester-port scripts packages",
"hits": 127,
"entries": [
{
"file": "packages/cli/tests/case1-accept.test.mjs",
"line": 596,
"text": " \"retired TCP\": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\\n')+'\\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],",
"reason": "New negative saved-argument fixture asserting the migration refusal."
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 68,
"text": " \"[Service]\\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\\n\");",
"reason": "Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged."
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 215,
"text": " put(join(h.root, \"etc\", \"systemd\", \"system\", \"mosaic-webui.service\"), \"[Service]\\nExecStart=/x serve --business mosaic-stack --tester-port 7333\\n\");",
"reason": "Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged."
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 445,
"text": " [\"--tester-port\", \"7333\", /case2-unit.sh retires tester TCP; use --tester-socket/],",
"reason": "New negative factory/library fixtures asserting the migration refusal."
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 496,
"text": " refuses(unit(h, { profile: \"factory\", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);",
"reason": "New negative factory/library fixtures asserting the migration refusal."
},
{
"file": "packages/cli/tests/stack-release-external.test.mjs",
"line": 75,
"text": " for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){",
"reason": "Legacy stack-release external refusal fixture, excluded by brief step 5."
},
{
"file": "packages/cli/tests/stack-release-fixture.mjs",
"line": 306,
"text": " for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\\n// serve accepts '--tester-port'\\n// serve accepts '--proof-header' (row 178)\\n${[C1,C].includes(id)?\"// serve accepts '--sso' (row 192)\\n\":''}`:'// fixture Console before tester listener, never run\\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){",
"reason": "Retained pre-196 copy fixture for legacy activation and rollback."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 681,
"text": "const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 687,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 688,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 690,
"text": " r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 692,
"text": " assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 693,
"text": " assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 694,
"text": " r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 697,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 702,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 705,
"text": " assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 708,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 709,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 714,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 718,
"text": " assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 724,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 731,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 732,
"text": " r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 736,
"text": " edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 738,
"text": " edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 740,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 741,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 746,
"text": " let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 749,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 821,
"text": " const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 827,
"text": " const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 830,
"text": " const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 839,
"text": " const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 843,
"text": " await t.test('a target Console without --tester-port',async(t)=>{",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 846,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 848,
"text": " assert.deepEqual(r.out.split('\\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 850,
"text": " await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1056,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1057,
"text": " assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1058,
"text": " assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1129,
"text": " await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1132,
"text": " let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1135,
"text": " assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1137,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1138,
"text": " assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1141,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1162,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1351,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1352,
"text": " assert.deepEqual(want.slice(-2),['--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1367,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1468,
"text": "const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1480,
"text": " let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1481,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1483,
"text": " const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1486,
"text": " `sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1563,
"text": " const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1564,
"text": " assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1584,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1594,
"text": " const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1682,
"text": " assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1811,
"text": " writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme','--tester-port',TESTER],schema:store,activation:'manual',at:'2026-10-20T00:00:00Z'})+'\\n');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2198,
"text": " ['beta\\'s tester port on alpha\\'s port',b,['--port',free,'--tester-port',a.port],1,`webui-service: refusing port-claimed: ${held}; nothing was changed\\n`],",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2202,
"text": " ['beta\\'s tester port on 7333',b,['--port',free,'--tester-port','7333'],4,'webui-service: port-reserved: 7333 is reserved for the default instance (7330 7332 7333)\\n'],",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2217,
"text": " await refused('--tester-port','--tester-port',a.port);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2280,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2290,
"text": " r=await run(e,'activate',C1,'acme','--tester-socket','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/--tester-socket/);assert.match(r.out,/drop.*--tester-port 7333/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2292,
"text": " assert.deepEqual(pointer(e).console,[...base,...pair(proof),'--tester-socket']);assert.match(consoleText(e),/--tester-socket/);assert.doesNotMatch(consoleText(e),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2319,
"text": " assert.doesNotMatch(conExec(e),/ --send(?: |$)|--tester-port|--public-host|--proof-header/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2388,
"text": " assert.equal(context.argv.includes('--tester-socket'),marked);assert.equal(context.argv.includes('--tester-port'),false);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/webui/README.md",
"line": 1001,
"text": "There is no tester TCP listener. `--tester-port` is retired for this copy.",
"reason": "Current retirement notice explicitly states no tester TCP listener."
},
{
"file": "packages/webui/src/cli.mjs",
"line": 30,
"text": " if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket');",
"reason": "Current CLI explicit retirement refusal, not an activation path."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 125,
"text": " const r = await run(dir, ['render', '--tester-port', port, ...PREVIEW, '--send']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 127,
"text": " assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 129,
"text": " assert.ok(execStart(r.stdout).endsWith(` ${PREVIEW_FLAGS} --send --tester-port ${port}`));",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 136,
"text": " const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 150,
"text": " ['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 151,
"text": " ['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 152,
"text": " ['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\\n'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 153,
"text": " ['--port', '7444', '--tester-port', '7444'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 154,
"text": " ['--tester-port', '7444', '--port', '7444'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 164,
"text": " assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 172,
"text": " const six = await run(dir, ['render', '--tester-port', '007333']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 175,
"text": " assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\\n');",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 176,
"text": " const five = await run(dir, ['render', '--tester-port', '07333']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 178,
"text": " assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 186,
"text": " const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...PREVIEW, '--send'], bin);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 188,
"text": " assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 189,
"text": " assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 199,
"text": " const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 799,
"text": " const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send', '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 813,
"text": " const both=await run(dir,['render','--tester-socket','--tester-port','7333']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/tester-cli.test.mjs",
"line": 25,
"text": " for (const flags of [['--tester-port', '7332'], ['--port', '7444', '--tester-port', '7444'], ['--tester-port', '7444', '--port', '7444'], ...['1023', '65536', '1.5', '-1', 'nope'].map(p => ['--tester-port', p])]) {",
"reason": "Explicit current-CLI negative calls; brief step 5."
},
{
"file": "scripts/case1-accept.sh",
"line": 71,
"text": " --tester-port) refuse \"case1-accept.sh retires tester TCP; use --tester-socket\" ;;",
"reason": "New explicit migration refusal for saved old arguments."
},
{
"file": "scripts/case2-unit.sh",
"line": 215,
"text": " --tester-port) die \"case2-unit.sh retires tester TCP; use --tester-socket\" ;;",
"reason": "New explicit migration refusal before rendering units."
},
{
"file": "scripts/instance.sh",
"line": 114,
"text": "# this instance's, whose ExecStart takes PORT as --port or --tester-port.",
"reason": "Legacy installed-unit port inventory/parser retained by brief step 5."
},
{
"file": "scripts/instance.sh",
"line": 132,
"text": " --tester-port) [[ \"$tok\" =~ ^[0-9]{1,5}$ ]] && ports+=(\"$((10#$tok))\") ;;",
"reason": "Legacy installed-unit port inventory/parser retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 47,
"text": " stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 63,
"text": "activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 126,
"text": " --tester-port) [ \"$OP\" = activate ] && [ $# -ge 2 ] && [ -n \"$2\" ] || usage; TESTER_PORT=\"$2\"; shift 2 ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 797,
"text": " elif grep -q -- '--tester-port' \"$cli\"; then supported=1; fi",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 800,
"text": " --tester-port) i=$((i + 1)); inherited=\"${BASE_CONSOLE[i]:-}\" ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 814,
"text": " [ -z \"$inherited\" ] || DROP_TESTER=\"--tester-port $inherited (replaced by private Unix socket)\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 830,
"text": " check_refuse tester-port-invalid \"--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 832,
"text": " [ \"$supported\" = 1 ] || check_refuse tester-port-target \"$TO's Console does not support --tester-port\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 833,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\" --tester-port \"$TESTER_PORT\")",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 837,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\") DROP_TESTER=\"--tester-port $inherited\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 878,
"text": " # --send and --tester-port follow the pointer, so a --no-send or a tester port set after that",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 885,
"text": " --tester-port) i=$((i + 1)) ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 893,
"text": " --tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port \"${P_CONSOLE[i]:-}\") ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 897,
"text": " SSO_PLAN=\"sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 903,
"text": "# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP.",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 919,
"text": " case \"${kept[i]}\" in --send | --tester-port | --tester-socket) break ;; esac",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 1076,
"text": " case \"${without_proof[i]}\" in --send | --tester-port | --tester-socket) break ;; esac",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 1134,
"text": "# Row 190: the target Console's --port (7332 when absent) and --tester-port against every other",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 1142,
"text": " --tester-port) ports+=(\"${RUN_CONSOLE[i + 1]:-}\") ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 2106,
"text": " for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do [ \"${P_CONSOLE[i]}\" != --tester-port ] || reserved+=(\"${P_CONSOLE[i + 1]:-}\"); done",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 43,
"text": "# Console), --tester-socket (private Unix tester), or legacy --tester-port N",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 74,
"text": "# --tester-port that another installed Console unit in the unit directory",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 96,
"text": " FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH]",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 97,
"text": " --tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener.",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 137,
"text": " --tester-port) [ $# -ge 2 ] || usage; TESTER_PORT=\"$2\"; TESTER_SET=1; SERVE_SET=1; shift 2 ;;",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 182,
"text": " echo \"webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT\" >&2",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 266,
"text": " [ \"$TESTER_SET\" = 0 ] || flags=\"$flags --tester-port $TESTER_PORT\"",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 462,
"text": " if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' \"$target\"; then",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 467,
"text": " if grep -Eq '^ExecStart=.* --sso( --send)?( --tester-port [0-9]+)?$' \"$target\"; then",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 471,
"text": " tester_port=\"$(sed -n 's/^ExecStart=.* --tester-port \\([0-9][0-9]*\\)$/\\1/p' \"$target\")\"",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
}
]
}
+131
View File
@@ -0,0 +1,131 @@
# Row253 retired-port audit
Current static source audit after the two approved fixture fixes. Each remaining hit and its reason follows.
- `packages/cli/tests/case1-accept.test.mjs:596`: New negative saved-argument fixture asserting the migration refusal. Text: `"retired TCP": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\n')+'\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],`
- `packages/cli/tests/case1-hostcheck.test.mjs:68`: Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged. Text: `"[Service]\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\n");`
- `packages/cli/tests/case1-hostcheck.test.mjs:215`: Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged. Text: `put(join(h.root, "etc", "systemd", "system", "mosaic-webui.service"), "[Service]\nExecStart=/x serve --business mosaic-stack --tester-port 7333\n");`
- `packages/cli/tests/case2-unit.test.mjs:445`: New negative factory/library fixtures asserting the migration refusal. Text: `["--tester-port", "7333", /case2-unit.sh retires tester TCP; use --tester-socket/],`
- `packages/cli/tests/case2-unit.test.mjs:496`: New negative factory/library fixtures asserting the migration refusal. Text: `refuses(unit(h, { profile: "factory", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);`
- `packages/cli/tests/stack-release-external.test.mjs:75`: Legacy stack-release external refusal fixture, excluded by brief step 5. Text: `for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){`
- `packages/cli/tests/stack-release-fixture.mjs:306`: Retained pre-196 copy fixture for legacy activation and rollback. Text: `for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\n// serve accepts '--tester-port'\n// serve accepts '--proof-header' (row 178)\n${[C1,C].includes(id)?"// serve accepts '--sso' (row 192)\n":''}`:'// fixture Console before tester listener, never run\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){`
- `packages/cli/tests/stack-release.test.mjs:681`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];`
- `packages/cli/tests/stack-release.test.mjs:687`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:688`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:690`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:692`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);`
- `packages/cli/tests/stack-release.test.mjs:693`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);`
- `packages/cli/tests/stack-release.test.mjs:694`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);`
- `packages/cli/tests/stack-release.test.mjs:697`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:702`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:705`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(r.out,/drop[^\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:708`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:709`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:714`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:718`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:724`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);`
- `packages/cli/tests/stack-release.test.mjs:731`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:732`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:736`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;`
- `packages/cli/tests/stack-release.test.mjs:738`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');`
- `packages/cli/tests/stack-release.test.mjs:740`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:741`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:746`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:749`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:821`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;`
- `packages/cli/tests/stack-release.test.mjs:827`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);`
- `packages/cli/tests/stack-release.test.mjs:830`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);`
- `packages/cli/tests/stack-release.test.mjs:839`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');`
- `packages/cli/tests/stack-release.test.mjs:843`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await t.test('a target Console without --tester-port',async(t)=>{`
- `packages/cli/tests/stack-release.test.mjs:846`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');`
- `packages/cli/tests/stack-release.test.mjs:848`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(r.out.split('\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);`
- `packages/cli/tests/stack-release.test.mjs:850`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);`
- `packages/cli/tests/stack-release.test.mjs:1056`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1057`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1058`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));`
- `packages/cli/tests/stack-release.test.mjs:1129`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{`
- `packages/cli/tests/stack-release.test.mjs:1132`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1135`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));`
- `packages/cli/tests/stack-release.test.mjs:1137`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1138`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));`
- `packages/cli/tests/stack-release.test.mjs:1141`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1162`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);`
- `packages/cli/tests/stack-release.test.mjs:1351`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);`
- `packages/cli/tests/stack-release.test.mjs:1352`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(want.slice(-2),['--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1367`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);`
- `packages/cli/tests/stack-release.test.mjs:1468`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;`
- `packages/cli/tests/stack-release.test.mjs:1480`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1481`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1483`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];`
- `packages/cli/tests/stack-release.test.mjs:1486`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: ``sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);`
- `packages/cli/tests/stack-release.test.mjs:1563`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');`
- `packages/cli/tests/stack-release.test.mjs:1564`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);`
- `packages/cli/tests/stack-release.test.mjs:1584`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);`
- `packages/cli/tests/stack-release.test.mjs:1594`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];`
- `packages/cli/tests/stack-release.test.mjs:1682`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]);`
- `packages/cli/tests/stack-release.test.mjs:1811`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme','--tester-port',TESTER],schema:store,activation:'manual',at:'2026-10-20T00:00:00Z'})+'\n');`
- `packages/cli/tests/stack-release.test.mjs:2198`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `['beta\'s tester port on alpha\'s port',b,['--port',free,'--tester-port',a.port],1,`webui-service: refusing port-claimed: ${held}; nothing was changed\n`],`
- `packages/cli/tests/stack-release.test.mjs:2202`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `['beta\'s tester port on 7333',b,['--port',free,'--tester-port','7333'],4,'webui-service: port-reserved: 7333 is reserved for the default instance (7330 7332 7333)\n'],`
- `packages/cli/tests/stack-release.test.mjs:2217`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused('--tester-port','--tester-port',a.port);`
- `packages/cli/tests/stack-release.test.mjs:2280`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:2290`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',C1,'acme','--tester-socket','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/--tester-socket/);assert.match(r.out,/drop.*--tester-port 7333/);`
- `packages/cli/tests/stack-release.test.mjs:2292`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...base,...pair(proof),'--tester-socket']);assert.match(consoleText(e),/--tester-socket/);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:2319`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.doesNotMatch(conExec(e),/ --send(?: |$)|--tester-port|--public-host|--proof-header/);`
- `packages/cli/tests/stack-release.test.mjs:2388`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.equal(context.argv.includes('--tester-socket'),marked);assert.equal(context.argv.includes('--tester-port'),false);`
- `packages/webui/README.md:1001`: Current retirement notice explicitly states no tester TCP listener. Text: `There is no tester TCP listener. `--tester-port` is retired for this copy.`
- `packages/webui/src/cli.mjs:30`: Current CLI explicit retirement refusal, not an activation path. Text: `if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket');`
- `packages/webui/tests/service.test.mjs:125`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['render', '--tester-port', port, ...PREVIEW, '--send']);`
- `packages/webui/tests/service.test.mjs:127`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));`
- `packages/webui/tests/service.test.mjs:129`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.ok(execStart(r.stdout).endsWith(` ${PREVIEW_FLAGS} --send --tester-port ${port}`));`
- `packages/webui/tests/service.test.mjs:136`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);`
- `packages/webui/tests/service.test.mjs:150`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],`
- `packages/webui/tests/service.test.mjs:151`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],`
- `packages/webui/tests/service.test.mjs:152`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\n'],`
- `packages/webui/tests/service.test.mjs:153`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--port', '7444', '--tester-port', '7444'],`
- `packages/webui/tests/service.test.mjs:154`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', '7444', '--port', '7444'],`
- `packages/webui/tests/service.test.mjs:164`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);`
- `packages/webui/tests/service.test.mjs:172`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const six = await run(dir, ['render', '--tester-port', '007333']);`
- `packages/webui/tests/service.test.mjs:175`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\n');`
- `packages/webui/tests/service.test.mjs:176`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const five = await run(dir, ['render', '--tester-port', '07333']);`
- `packages/webui/tests/service.test.mjs:178`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));`
- `packages/webui/tests/service.test.mjs:186`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...PREVIEW, '--send'], bin);`
- `packages/webui/tests/service.test.mjs:188`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);`
- `packages/webui/tests/service.test.mjs:189`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);`
- `packages/webui/tests/service.test.mjs:199`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);`
- `packages/webui/tests/service.test.mjs:799`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send', '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);`
- `packages/webui/tests/service.test.mjs:813`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const both=await run(dir,['render','--tester-socket','--tester-port','7333']);`
- `packages/webui/tests/tester-cli.test.mjs:25`: Explicit current-CLI negative calls; brief step 5. Text: `for (const flags of [['--tester-port', '7332'], ['--port', '7444', '--tester-port', '7444'], ['--tester-port', '7444', '--port', '7444'], ...['1023', '65536', '1.5', '-1', 'nope'].map(p => ['--tester-port', p])]) {`
- `scripts/case1-accept.sh:71`: New explicit migration refusal for saved old arguments. Text: `--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;`
- `scripts/case2-unit.sh:215`: New explicit migration refusal before rendering units. Text: `--tester-port) die "case2-unit.sh retires tester TCP; use --tester-socket" ;;`
- `scripts/instance.sh:114`: Legacy installed-unit port inventory/parser retained by brief step 5. Text: `# this instance's, whose ExecStart takes PORT as --port or --tester-port.`
- `scripts/instance.sh:132`: Legacy installed-unit port inventory/parser retained by brief step 5. Text: `--tester-port) [[ "$tok" =~ ^[0-9]{1,5}$ ]] && ports+=("$((10#$tok))") ;;`
- `scripts/stack-release.sh:47`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]`
- `scripts/stack-release.sh:63`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the`
- `scripts/stack-release.sh:126`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; TESTER_PORT="$2"; shift 2 ;;`
- `scripts/stack-release.sh:797`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `elif grep -q -- '--tester-port' "$cli"; then supported=1; fi`
- `scripts/stack-release.sh:800`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)); inherited="${BASE_CONSOLE[i]:-}" ;;`
- `scripts/stack-release.sh:814`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `[ -z "$inherited" ] || DROP_TESTER="--tester-port $inherited (replaced by private Unix socket)"`
- `scripts/stack-release.sh:830`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `check_refuse tester-port-invalid "--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other"`
- `scripts/stack-release.sh:832`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `[ "$supported" = 1 ] || check_refuse tester-port-target "$TO's Console does not support --tester-port"`
- `scripts/stack-release.sh:833`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `RUN_CONSOLE=("${without_tester[@]}" --tester-port "$TESTER_PORT")`
- `scripts/stack-release.sh:837`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `RUN_CONSOLE=("${without_tester[@]}") DROP_TESTER="--tester-port $inherited"`
- `scripts/stack-release.sh:878`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# --send and --tester-port follow the pointer, so a --no-send or a tester port set after that`
- `scripts/stack-release.sh:885`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)) ;;`
- `scripts/stack-release.sh:893`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port "${P_CONSOLE[i]:-}") ;;`
- `scripts/stack-release.sh:897`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `SSO_PLAN="sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them"`
- `scripts/stack-release.sh:903`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP.`
- `scripts/stack-release.sh:919`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `case "${kept[i]}" in --send | --tester-port | --tester-socket) break ;; esac`
- `scripts/stack-release.sh:1076`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `case "${without_proof[i]}" in --send | --tester-port | --tester-socket) break ;; esac`
- `scripts/stack-release.sh:1134`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# Row 190: the target Console's --port (7332 when absent) and --tester-port against every other`
- `scripts/stack-release.sh:1142`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) ports+=("${RUN_CONSOLE[i + 1]:-}") ;;`
- `scripts/stack-release.sh:2106`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do [ "${P_CONSOLE[i]}" != --tester-port ] || reserved+=("${P_CONSOLE[i + 1]:-}"); done`
- `scripts/webui-service.sh:43`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `# Console), --tester-socket (private Unix tester), or legacy --tester-port N`
- `scripts/webui-service.sh:74`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `# --tester-port that another installed Console unit in the unit directory`
- `scripts/webui-service.sh:96`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH]`
- `scripts/webui-service.sh:97`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `--tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener.`
- `scripts/webui-service.sh:137`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `--tester-port) [ $# -ge 2 ] || usage; TESTER_PORT="$2"; TESTER_SET=1; SERVE_SET=1; shift 2 ;;`
- `scripts/webui-service.sh:182`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `echo "webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT" >&2`
- `scripts/webui-service.sh:266`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `[ "$TESTER_SET" = 0 ] || flags="$flags --tester-port $TESTER_PORT"`
- `scripts/webui-service.sh:462`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' "$target"; then`
- `scripts/webui-service.sh:467`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `if grep -Eq '^ExecStart=.* --sso( --send)?( --tester-port [0-9]+)?$' "$target"; then`
- `scripts/webui-service.sh:471`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `tester_port="$(sed -n 's/^ExecStart=.* --tester-port \([0-9][0-9]*\)$/\1/p' "$target")"`
+38
View File
@@ -0,0 +1,38 @@
# Row253 granted gate
Do not launch until Sage's CLEAR names this scope. No tests have run yet.
Use this worktree, the frozen five blobs in CANDIDATE-SHAS.json and the plain
node_modules link. No canonical-checkout node or queue writes during the slot.
Set TMPDIR with `mktemp -d /mnt/storage/scratch/tmp/gXXX`, export it and use
`set -o pipefail`. Record the grant and the exact date before launch. Use logs
under this packet's `run1/`, with the outer log named `controller.log`.
Neither helper nor wrapper argv may match the busy detector's patterns.
Sequence, stopping to inspect every result:
1. `window.py --stage focused`: isolated prior installer and acceptance RED
examples, restoring bytes/status in finally, then both focused test files
whole GREEN. Expected RED is recorded separately from gate failures.
2. `driver.py`: 33 unique single-target changes, named base cases and killer
runs. Each base must pass in under 30 seconds. bash -n is the only invalid
classification. Runtime errors kill a change; unexpected timeouts stop for
Sage/Mos. Every run records time, code, raw tee log and byte/status restore.
3. `window.py --stage whole`: cli whole, webui whole and the ten shell files
whole. No omitted tests, patterns or partial scripts in this phase.
4. Sum counts and seconds from raw logs by script. Sweep process cwd/ancestry
for this worktree's node, tee, query and probe descendants. Report existing
processes separately. Return the slot immediately, then prepare review.
Every driver invocation needs `--grant <Sage-id> --out <record-directory>`.
Before each actual test, driver.run checks Sage's busy guard and launches only
on CLEAR. It invokes a bash pipefail/tee pipeline through launch.py, which
loads the structured command after the guard. No command text is embedded in
wrapper argv. All execution is in a new process group and timed out groups
are killed and waited for. Query-owned groups are independently bounded by
product code; the orphan fixture also cleans its own recorded PID.
Two unexpected failures end the window. These drivers stop at the first
unexpected base, focused or whole-phase failure for inspection, without
automatically rerunning it. Any timeout is a stop. Send a stop to Sage/Mos
and do not turn an ungranted retry into a new run.
@@ -0,0 +1,132 @@
{
"authority": [
"mos-1275",
"sage-2659"
],
"scope": "Own Gander row196 receipt set documented for row278, plus the complete row278 run3 shell phase. Row253 old C4 results do not carry. Receipts choose timeouts only.",
"consulted_records": [
"ec1e7fad8:agents/gander/work/queue-196/evidence/suite-results-run1.json",
"ec1e7fad8:agents/gander/work/queue-196/evidence/suite-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r2/mutants/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r2/run2/base-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r2/run2/killers/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r3/gate/base-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r3/gate/killers/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r3/gate/sso-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/base-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/killers/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/sso-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/whole-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r5/gate/whole-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r5/resume/summary.json",
"ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r6/base-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r6/first-attempt/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r6/killers/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r6/summary.json",
"ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"385a7194c:agents/gander/work/queue-278/run3/whole/results.json"
],
"scripts": {
"scripts/test-auth.sh": {
"recorded_duration_seconds": 1.9081228229999543,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"limit_seconds": 126,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "dd4720cb0cbecd2fc523a318f2222d21d257c8c5889996a3d69e522ee971a707",
"previous_maximum_seconds": 1.9081228229999543,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"row278_run3_seconds": 0.816
},
"scripts/test-conductor.sh": {
"recorded_duration_seconds": 8.057173820000026,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"limit_seconds": 145,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "da84150467494a4c979759f400854411bd93a7efce981139757bf526d688a23b",
"previous_maximum_seconds": 8.057173820000026,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"row278_run3_seconds": 6.078
},
"scripts/test-config.sh": {
"recorded_duration_seconds": 3.1678915339999367,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"limit_seconds": 130,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "3301c1fabf616bb961d62f6decd0a46dadb0f399b8ba7f3c2ca60023d2b24c78",
"previous_maximum_seconds": 3.1678915339999367,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"row278_run3_seconds": 1.868
},
"scripts/test-discord.sh": {
"recorded_duration_seconds": 8.776727847000002,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 147,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
"previous_maximum_seconds": 8.776727847000002,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 7.431
},
"scripts/test-extension-package.sh": {
"recorded_duration_seconds": 3.01285702399991,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 130,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "5d7b687f16fd1a9414b229501177d530ba3af4eadd3cac319b33f38fdba9bf99",
"previous_maximum_seconds": 3.01285702399991,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 1.366
},
"scripts/test-foundation.sh": {
"recorded_duration_seconds": 39.901,
"receipt": "385a7194c:agents/gander/work/queue-278/run3/whole/results.json",
"limit_seconds": 240,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"previous_maximum_seconds": 39.70872647900006,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 39.901
},
"scripts/test-land-work.sh": {
"recorded_duration_seconds": 12.623263413000037,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 158,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "c9d1d5c9b006d5c12e5f796a9828361059f8e0410846fc55c070c0148b15cef4",
"previous_maximum_seconds": 12.623263413000037,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 11.338
},
"scripts/test-queue.sh": {
"recorded_duration_seconds": 23.443742031000088,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 191,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"previous_maximum_seconds": 23.443742031000088,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 21.158
},
"scripts/test-release.sh": {
"recorded_duration_seconds": 12.168674453999964,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 157,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "be706dff832257bd4781abeabd19faa4ac16edf9a4af0c96457e33cf3bb6deb3",
"previous_maximum_seconds": 12.168674453999964,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 2.67
},
"scripts/test-task.sh": {
"recorded_duration_seconds": 36.783798266,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 231,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "10c4abb70bf9b83e617089b06a5436cc7fa6b866a176458b22128c3a15163f94",
"previous_maximum_seconds": 36.783798266,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 13.843
}
}
}
+50
View File
@@ -0,0 +1,50 @@
{
"at": "2026-10-21T21:52:50.545130+00:00",
"behavioral_tests_run": false,
"checks": [
{
"command": [
"bash",
"-n",
"scripts/case1-accept.sh",
"scripts/case2-unit.sh"
],
"code": 0,
"stdout": "",
"stderr": ""
},
{
"command": [
"node",
"--check",
"packages/cli/tests/case1-accept.test.mjs"
],
"code": 0,
"stdout": "",
"stderr": ""
},
{
"command": [
"node",
"--check",
"packages/cli/tests/case2-unit.test.mjs"
],
"code": 0,
"stdout": "",
"stderr": ""
},
{
"command": [
"git",
"diff",
"--check"
],
"code": 0,
"stdout": "",
"stderr": ""
}
],
"retired_port_hits": 127,
"unique_changes": 33,
"slot": "Requested gander-0071; queued by sage-2191; no CLEAR"
}
@@ -0,0 +1,233 @@
[
{
"id": "M1",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "c25003c6fea2c7693d6b3e38e1986b8397886a8a47b5c6d7c0ddb535e676171b"
},
{
"id": "M2",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "977ef3f63de87cd260bc4fa44c2695b56880066d722681fa5d9e0f45786474f3"
},
{
"id": "M3",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "9dca577302011b11fc0459517420a4862dedb792e11eba6e06a975b9978c2db9"
},
{
"id": "M4",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "538ec4d9aaa942052b62628040445103128b8894ba5b2dafdf5d6c9a52b3b4d7"
},
{
"id": "M5",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "6632ebedee012f299ff5b5b36eaa2fd7c0bcae371b8f7b11c1bfd28bce0dde80"
},
{
"id": "M6",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "4f9cfb1b87ab33c39fe2ed77286296ad1047581afca8138722ddad6dddc938d8"
},
{
"id": "M7",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "00fa60f1d341ee67622d1f89be9c5062ebe45af3c49cc82ae004670f653097a3"
},
{
"id": "M8",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "92d8b45baf861d0a12327897d6fe09f87db867b1d875d39e9769fa46242826ea"
},
{
"id": "M9",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "3ba9e143b76ab0c37c575f8a446acf69e611e604074296a176bef1b5b84aa6ea"
},
{
"id": "M10",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "6eacec588684a0ccc0a1cc009f5ec0f27b1f38341f1fb72fec37c710de38b2a9"
},
{
"id": "M11",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "dc4c4d3d4761cc8976c7b813bdcc9dd9c56fc35cf03aadf3a19d4cfec6a32233"
},
{
"id": "M12",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "6ec5c79f0a0d19f5d642dffc79a7e39815f907071fae14df0cfd66b64ebaf221"
},
{
"id": "M13",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "2af786adc1269fde529b68b7dc121bead9c8b82e2615b4dbb18207aab60e72c8"
},
{
"id": "M14",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "f3411f4887e009e1b8cc38664be6eb5e1a34379472311284b726466171572758"
},
{
"id": "M15",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "148f30346fcbfd473ca0b4a717846a2d9743af9a046d5cc009271392d958d812"
},
{
"id": "M16",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "ec27d778b49a8e5ff0b356e6b27973db8eb97a078a571cf9720ab43f15781b01"
},
{
"id": "M17",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "4c7cd4c62d2137626158091c478d20e8ace5617c5e96b258478317e57bc808f3"
},
{
"id": "M18",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "4372a2e392054f80d491fc04b328303339a57cd4c72030abf3ccdae1e0dfb024"
},
{
"id": "M19",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "766524ec13b960bace472671aa62cfb3ea69f6bdb31d27c2427758e8d8ea53ae"
},
{
"id": "M20",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "4e653a809eaa504024d57c0af132222a374c0c26313942f6f0a68b3e8d099632"
},
{
"id": "M21",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "1067c1400870258f663e7e6bdb911e9623a9c666765e86cc7ec0bda4131852f3"
},
{
"id": "M22",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "f19cc06bf32c491bdfbea56888353fcc985e10298d8416892993e0757c429421"
},
{
"id": "M23",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "f1c7326e60d5ed26d55f43107939d775d6c81dfc45398a36274bac380f95e9b3"
},
{
"id": "M24",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "be8c6cb451c4d30b621c9eb38cf2c63629bd8ae5b6d30d5d91d365e4911ca964"
},
{
"id": "M25",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "53ee61ac53156707aa11bbad35b41a0bf21f0093a5fb6d03aff215940ed023ad"
},
{
"id": "M26",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "fc9cbfc3eaa0df6adb84b67dc9cf4fb9f773b89c16597703dbebc03559977b5f"
},
{
"id": "M27",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "e727c63e45bb5a3a767c5e60870658d2a8d1cc970bfa3d6c2b8f439cb27fcbda"
},
{
"id": "M28",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "238b6bc3b2feb64de614aa2e9471dcc58867a3e44ebfad7130917d43101fd6bd"
},
{
"id": "M29",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "f7c5e06f46aea4d5c3fde288eebd732a3265583faef2d38c2b293a7364e299b7"
},
{
"id": "M30",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "d6ceb8529fa508eb48534000666f02fd4bf949dbeeeff03490c51c8d1e4ecf1f"
},
{
"id": "M31",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "c028760cf45fbae6b127b203f32ce208eeb77ea9dd646fee5af9e6e58661a6ec"
},
{
"id": "M32",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "f047a7024b67c1f7a7af503c3cb525b07c83a0e413b845818d3ea9cd0e6f9ea9"
},
{
"id": "M33",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "a197452abed015e81d0620730b95292a89cff84f8f52ea679111b0b71b17ae1d"
}
]
@@ -0,0 +1,142 @@
{
"authority": "sage-2737/sage-2739",
"files": {
"packages/cli/tests/access.test.mjs": 900,
"packages/cli/tests/app.test.mjs": 900,
"packages/cli/tests/boot-status.test.mjs": 900,
"packages/cli/tests/case1-accept.test.mjs": 418,
"packages/cli/tests/case1-hostcheck.test.mjs": 900,
"packages/cli/tests/case2-persona-import.test.mjs": 900,
"packages/cli/tests/case2-unit.test.mjs": 320,
"packages/cli/tests/channel-cli.test.mjs": 900,
"packages/cli/tests/channel-recovery.test.mjs": 900,
"packages/cli/tests/cli.test.mjs": 900,
"packages/cli/tests/config.test.mjs": 900,
"packages/cli/tests/credentials.test.mjs": 900,
"packages/cli/tests/follow.test.mjs": 900,
"packages/cli/tests/format.test.mjs": 900,
"packages/cli/tests/gate-lane-auth.test.mjs": 900,
"packages/cli/tests/gate-lane-fixtures.test.mjs": 900,
"packages/cli/tests/gate-lane-image.test.mjs": 900,
"packages/cli/tests/gate-lane-state.test.mjs": 900,
"packages/cli/tests/gate-lane.test.mjs": 900,
"packages/cli/tests/git-socket.test.mjs": 900,
"packages/cli/tests/grants.test.mjs": 900,
"packages/cli/tests/host.test.mjs": 900,
"packages/cli/tests/hosts.test.mjs": 900,
"packages/cli/tests/instances.test.mjs": 900,
"packages/cli/tests/launch-account.test.mjs": 900,
"packages/cli/tests/launch-limit-checks.test.mjs": 900,
"packages/cli/tests/launch-limits.test.mjs": 900,
"packages/cli/tests/launch-routes.test.mjs": 900,
"packages/cli/tests/launcher.test.mjs": 900,
"packages/cli/tests/lifecycle.test.mjs": 160,
"packages/cli/tests/limits-tokens.test.mjs": 900,
"packages/cli/tests/limits.test.mjs": 900,
"packages/cli/tests/mark-read.test.mjs": 900,
"packages/cli/tests/names.test.mjs": 900,
"packages/cli/tests/notifier.test.mjs": 900,
"packages/cli/tests/other-tree.test.mjs": 900,
"packages/cli/tests/processing-cli.test.mjs": 900,
"packages/cli/tests/project-name-repo.test.mjs": 900,
"packages/cli/tests/project.test.mjs": 900,
"packages/cli/tests/publish-socket.test.mjs": 900,
"packages/cli/tests/stack-copy.test.mjs": 900,
"packages/cli/tests/stack-instances.test.mjs": 900,
"packages/cli/tests/stack-release-external.test.mjs": 900,
"packages/cli/tests/stack-release.test.mjs": 1871,
"packages/cli/tests/stack-upgrade.test.mjs": 900,
"packages/cli/tests/tester-http.test.mjs": 900,
"packages/cli/tests/tester-reads.test.mjs": 900,
"packages/cli/tests/tester-threads.test.mjs": 900,
"packages/cli/tests/tester.test.mjs": 900,
"packages/cli/tests/thread-cli.test.mjs": 126,
"packages/cli/tests/thread-launch.test.mjs": 170,
"packages/cli/tests/trackers-boot.test.mjs": 900,
"packages/cli/tests/transport.test.mjs": 900,
"packages/cli/tests/usage.test.mjs": 900,
"packages/cli/tests/verbs.test.mjs": 900,
"packages/webui/tests/agents-browser.test.mjs": 900,
"packages/webui/tests/agents.test.mjs": 900,
"packages/webui/tests/assistant.test.mjs": 900,
"packages/webui/tests/board-held.test.mjs": 900,
"packages/webui/tests/browser-edge.test.mjs": 900,
"packages/webui/tests/browser.test.mjs": 900,
"packages/webui/tests/bus-browser.test.mjs": 900,
"packages/webui/tests/bus.test.mjs": 900,
"packages/webui/tests/channel-chat.test.mjs": 900,
"packages/webui/tests/chat-browser.test.mjs": 900,
"packages/webui/tests/chat.test.mjs": 122,
"packages/webui/tests/conversation.test.mjs": 900,
"packages/webui/tests/discord.test.mjs": 900,
"packages/webui/tests/docs-browser.test.mjs": 900,
"packages/webui/tests/editor-browser.test.mjs": 900,
"packages/webui/tests/editor-provenance.test.mjs": 900,
"packages/webui/tests/editor.test.mjs": 900,
"packages/webui/tests/history-return-flow.test.mjs": 900,
"packages/webui/tests/instances-browser.test.mjs": 900,
"packages/webui/tests/instances.test.mjs": 900,
"packages/webui/tests/library-cli.test.mjs": 900,
"packages/webui/tests/library-status.test.mjs": 900,
"packages/webui/tests/limits-browser.test.mjs": 900,
"packages/webui/tests/limits-route.test.mjs": 900,
"packages/webui/tests/markdown-preview.test.mjs": 900,
"packages/webui/tests/messages.test.mjs": 900,
"packages/webui/tests/pinned-copy.test.mjs": 900,
"packages/webui/tests/preview.test.mjs": 900,
"packages/webui/tests/processing-browser.test.mjs": 900,
"packages/webui/tests/processing-ui.test.mjs": 900,
"packages/webui/tests/processing.test.mjs": 900,
"packages/webui/tests/project-file.test.mjs": 900,
"packages/webui/tests/project-name-repo.test.mjs": 900,
"packages/webui/tests/proof-cli.test.mjs": 900,
"packages/webui/tests/proof.test.mjs": 900,
"packages/webui/tests/reads.test.mjs": 900,
"packages/webui/tests/relaunch.test.mjs": 900,
"packages/webui/tests/return-flow.test.mjs": 900,
"packages/webui/tests/routes-browser.test.mjs": 900,
"packages/webui/tests/routes.test.mjs": 900,
"packages/webui/tests/send-browser.test.mjs": 900,
"packages/webui/tests/send.test.mjs": 900,
"packages/webui/tests/serve.test.mjs": 900,
"packages/webui/tests/service.test.mjs": 900,
"packages/webui/tests/settings-form.test.mjs": 900,
"packages/webui/tests/settings-write.test.mjs": 900,
"packages/webui/tests/shell.test.mjs": 900,
"packages/webui/tests/sso-browser.test.mjs": 900,
"packages/webui/tests/sso.test.mjs": 900,
"packages/webui/tests/tester-browser.test.mjs": 900,
"packages/webui/tests/tester-cli.test.mjs": 900,
"packages/webui/tests/tester-listener.test.mjs": 900,
"packages/webui/tests/tester-socket.test.mjs": 900,
"packages/webui/tests/thread-claims-browser.test.mjs": 900,
"packages/webui/tests/thread-claims.test.mjs": 900,
"packages/webui/tests/usage-browser.test.mjs": 900,
"packages/webui/tests/usage.test.mjs": 900,
"packages/webui/tests/views.test.mjs": 900,
"packages/bus/tests/account-migration.test.mjs": 122,
"packages/bus/tests/chief-of-staff.test.mjs": 127,
"packages/bus/tests/comment-migration.test.mjs": 122,
"packages/bus/tests/end-launch.test.mjs": 122,
"packages/bus/tests/fire-revokes-grants.test.mjs": 130,
"packages/bus/tests/gitea-grant.test.mjs": 146,
"packages/bus/tests/gitea-migration.test.mjs": 122,
"packages/bus/tests/grant-executor.test.mjs": 185,
"packages/bus/tests/grant-migration.test.mjs": 122,
"packages/bus/tests/lifecycle-revoke.test.mjs": 122,
"packages/bus/tests/message-filter.test.mjs": 122,
"packages/bus/tests/name-migration.test.mjs": 121,
"packages/bus/tests/schema.test.mjs": 121,
"packages/bus/tests/sessions.test.mjs": 122,
"packages/bus/tests/spec-migration.test.mjs": 121,
"packages/bus/tests/tester-writes.test.mjs": 122,
"packages/bus/tests/thread-claims.test.mjs": 121,
"packages/bus/tests/thread-migration.test.mjs": 122,
"packages/bus/tests/threads.test.mjs": 124,
"packages/bus/tests/usage-migration.test.mjs": 121
},
"fallback_seconds": 900,
"kill_after_seconds": 10,
"outer_wait_slack_seconds": 30,
"source": "run3-focused/MEASURED-FILE-LIMITS.json"
}
@@ -0,0 +1,461 @@
# Deferred list
One line per item, written from evidence, never from inertia. Add a line
when a gap is found and not fixed in the current piece; strike it (move to
Done at the bottom with the commit) when a piece closes it. Nothing here
starts on its own; a line becomes a piece only when Jason calls it. Check it
at every gate. Started 2026-09-12 during the control board MVP.
## Open
- **Inherited Discord engine test intermittency during #1512 review.** Frozen
serialized run recorded a tool-turn engine assertion failure and then timed
out at 120s. A later full TAP run with a 15s test deadline passed 351/351.
Rocko isolated the busy/settled-event test race and missing failure cleanup;
Darkwing independently reproduced the event-split assertion. Rocko also reported
a timeout-log test race and a separate engine followUp-after-timeout defect.
No live incident established. Row21 owner orch-01 notified; no Discord source
takeover. Not assumed identical to earlier concurrent hangs.
Integration held pending owner disposition; logs and scope in
`agents/darkwing/work/relaunch-activity/r1-review.md`. (2026-09-15)
Outcome 2026-09-26: 1685deb4 fixed the busy/settled race and the engine no
longer sends a follow-up (held prompts). A re-run of the frozen candidate
against 21e3e908 passed the serial acceptance command 397/397 three times.
Two engine failures remain when tests run concurrently, and they reproduce on
clean HEAD without the #1512 files, so they are #1509's (entry below). Sage
ruled that #1512 integrates on the serial evidence; it landed in af4203ca.
Record: `agents/darkwing/work/relaunch-activity/rerun-2026-09-26.md`.
- **Registration has no "set by" field.** `mosaic seat task` records the
task and `updatedAt` but not who set it, so a human session and the
orchestrator look the same on the board. One-line record change; rides
with the orchestrator assignment piece. (2026-09-12, #1504)
- **Usage-limit notice was missed.** Jason reports Rocko exhausted usage without
Jason seeing the notice, and is now available again. Investigate where the
notice appeared and what operator notification is missing as part of Claude
console integration. User-reported, not independently reproduced; no root
cause established. Keep outside #1512's relaunch-display scope. (2026-09-15)
- **Claude Code seats register but get no board row.** Rocko has no Pi
session log, and rows come only from Pi logs. Needs a second row source
or a Claude-side log reader. (2026-09-12, darkwing's #1504 report)
2026-09-15 #1511 live-test preflight reconfirmed no Rocko row: expected
`.pi/state/rocko/sessions` discovery directory absent. Its existing repo
registration names dead PID 185602, while the verified native lock owner is
3707667. No registration repair, discovery expansion or relaunch performed;
test write refused before mutation. Evidence:
`agents/darkwing/work/task-attribution/backend-live-verification.json`.
- **Fleet seats (`~/.mosaic`) stay on the old launchers.** No registration,
no board reply, no `seat task`. Moving them is the stress test and comes
after the WebUI and fleet launch work. (Jason, 2026-09-12)
- **Comms skill has no rule for senders without a pane.** The board trailer
covers it today; if another pane-less sender appears, put the rule in
`skills/ms-communications` instead of a second trailer. (2026-09-12, #1505)
- **Board: multiple sessions per seat, stopping seats, auth beyond
loopback.** Out of scope for the MVP by the plan; listed so they are not
forgotten. (plan page, Boundaries)
- **`MOSAIC-STACK-D-001` is Answered, not Completed.** Mark `--completed` in
jarvis-brain once Jason says the board answers "who is waiting on me".
(2026-09-12)
- **Uncommitted working tree.** `AGENTS.md` autonomy section,
`agents/README.md` Sage row, Dewey's `work/wui` docs and evidence,
`docs/plans/reviews/2026-09-08..10_*`, `skills/ms-{arena,blast-radius,
board,executive-update,git-operations,technical-writing}`. Jason's edits
from before 2026-09-12; they ride along in the next commit that touches
them unless staged on purpose. (2026-09-12)
- **Fleet debt (orch-01, `~/.mosaic`):** `sweep-commit.sh` deny-list regex
skips `launch.env`; needs the fix plus a regression test. (2026-09-12)
- **Parked, not cancelled:** Dewey D05 dashboard mockup work beyond design 3,
registry increment 3 (headless identity-env leak), new seat directories.
(plan page, 2026-09-12 parking note)
- **Gitea helper response-file hardening.** Predictable `/tmp/gitea-api-response.$$`
has no explicit 0600 mode; use `mktemp` with cleanup in a separately reviewed
pass. Credential validation and later reread also have a race window. Other
non-blocking ledger review notes remain in
`reviews/2026-09-12_ledger-verdict.md`. No change beyond the reviewed GET
cleanup fix in this piece. Filbert, LEDGER-1506-R1, 2026-09-12, #1506.
- **One shared checkout for every seat and the coordinator.** Any `git add`
of a shared file (CURRENT.md, SESSIONS.md, the plan page) commits another
seat's in-progress edit under the wrong commit; it happened at f80404f5
(dewey's CURRENT.md status note went in with the piece 5 brief, harmless
but unowned). Fix candidates: per-seat worktrees, or shared files edited
only by the seat named in CURRENT.md. (2026-09-12)
- **Console mockup fields absent from the board API.** Controller, generation,
config fingerprint, turn count, goal/report and activity stream are omitted.
Workspace paths are shown as board data, not invented registered workspace
nodes. Launch, observe, stop, full transcript navigation, command palette and
other screens are absent. Existing palette/appearance switches remain on the
single page. Revisit only with a new brief. (2026-09-13 UTC, #1507)
- **Board send can turn into a Pi slash command.** `tools/tmux`
`send-message.sh` pastes onto whatever is already in Pi's composer without
clearing it. On Jason's 2026-09-26 20:10:57Z send, a leftover "/" landed in
front of the agent-send header. Pi read it as plain text that time, but the
same path could run a board reply as a slash command. Belongs to CHAT-03I/B3.
No tools/tmux change until that piece. Found by Dewey in the CHAT-02 brief
evidence. (2026-09-26, #1507)
- **Discord wedge restarts have no cumulative stop.** Since 6b R2, a wedged
pi ends the connector with exit 1, and the unit restarts it. With
turnTimeoutSeconds 600, one cycle takes at least 645 s, longer than the
600 s StartLimit window. So a pi that wedges on every prompt restarts once
per prompt and never hits the limit. It only happens when someone sends a
message, and that person gets an error reply. Sage accepted it for now. The
fix is a separate policy (a wedge count kept across restarts). (2026-09-26,
#1509, Rocko 6b R2 review)
- **Connector does not journal the source it started from.** The unit runs
from the live checkout, so an automatic restart loads whatever Discord
source is in the tree, even uncommitted source. Rocko recommends a startup
journal line with HEAD, the dirty state of `packages/discord` and a digest
of the runtime files. Until then the rule is that unreviewed Discord source
stays uncommitted in the tree only as long as a review needs it.
(2026-09-26, #1509)
- **Index-export suite runs leave Docker networks behind.** Each export
directory becomes a compose project, and its `<dir>_default` network
stays after the run. On 2026-09-26 the host ran out of address pools. The
task and release suites then failed 31 and 3 tests with "all predefined
address pools have been fully subnetted". Sage removed its own five stale
networks. The dw-* ones are still there. Fix: the export recipe, or the
suites, removes the project network on exit. (2026-09-26)
2026-09-27: it happened again on the Piece D candidate (task 31 and
release 3 failures). Sage removed 12 leftover export networks from
other seats, each checked for zero attached containers first, and both
suites then passed. Still open.
- **Ledger T3 read: snapshot isolation has no test.** `readT3` runs every
query in one read transaction, but no test proves a write landing during
the read stays invisible. Filbert's note 4 on the Gate F build (e47ec6da).
Notes 1 to 3 go to Darkwing's follow-up. (2026-09-26, #1506)
- **A T3 sender role with spaces counts as human.** The SetSpark lead's
reply to Sage (21:41:24Z) used the role "main content-engine agent". The
6a rule requires a one-word role, so the ledger counts that message as
human in Sage's row. Gate F reads only Filbert's row, so it is not
affected. Filbert found it through the live diagnostic (14 went to 15).
Sage asked the SetSpark lead to use a one-word role. The rule stays as
it is. (2026-09-26, #1506)
- **Queue tests: the cold-start failure came back (reopened).** Row 33
closed this as not reproduced in 20 idle cold runs. On 2026-10-04 Sage's
index export of the row 33 candidate ran the nine suites, then
`node --test packages/queue/tests/`, and got 147 passed, 1 failed. Sage's
recipe threw the output away, so again nobody knows which test failed.
Ten more runs all passed 148/148: three reruns in the same export, four
under parallel suite load, and three fresh exports run straight after
`test-queue.sh`. It needs a first run on a fresh tree after a full
suite pass, and even then it's rare. Darkwing's n2 on row 33 (the cold
runs were idle) was right. Next time: Sage's export recipe keeps the
node test output in a file, so the first failure has a name. Then fix it
as its own row. (2026-10-04, #1508)
- **Queue: `add --issue N` also sets closes to N.** Follow-up rows under
an umbrella issue, like rows 31 and 33 under #1508, would close the issue
when they finish. Sage narrowed both by hand (revs 36 and 43). Either
`add` takes `--closes` or the lead checks closes on every add. Not
urgent while Sage adds the rows. (2026-10-04, #1508)
- **Ledger metric call can orphan curl.** The metric call in
`packages/ledger/src/ledger.mjs` still relies on execFileSync's
timeout, which kills the helper but can leave curl running. E's queue
calls already use `timeout -s KILL`; the metric call should match.
(2026-09-27, #1508)
- **Ledger: a missing `timeout` command reads as a Gitea failure.** With
no `timeout` on PATH the queue issue calls report "credential or Gitea
request failure". Optional test and clearer message. Filbert, E r2.
(2026-09-27, #1508)
- **`review record` doesn't check which issue the verdict comment is on.**
Row 5 round 1's request (26671) and Filbert's verdict (26683) are on
#1507. Darkwing's verdict 26681 went to #1508, and `review record`
accepted it, because only `review resolve` reads the comment back
(`checkComment` in `packages/queue/src/review.mjs`). The receipt
points at a real comment on the wrong issue. Sage posted a pointer on
#1507 (comment 26685). Fix: have `record` check the comment's issue and author the way
`resolve` does, or refuse a comment id it can't verify. Found by
Filbert. (2026-10-04, #1508)
- **Host seats launch Pi with `--approve`.** A future `.pi/SYSTEM.md` or
`.pi/settings.json` in the checkout would load in every host seat. This
is latent, since neither file exists today. Fix in
`scripts/agent-host-dev.sh`: pass `--no-approve` and an explicit system
prompt. Filbert, meta-harness survey section 8.4. (2026-10-04)
- **Workers can read provider keys.** The worker container passes
`ZAI_API_KEY` and `ANTHROPIC_API_KEY` in the environment, which Pi's
`bash` inherits, and Pi's `read` can open the mounted `auth.json`.
Slice 1 role tokens stay in the broker (lead decision 47). The workers'
own exposure needs a separate piece. Filbert, survey section 8.6.
(2026-10-04)
- **CHAT-03 I1 round 2 follow-ups, for the I3 brief.** Neither reviewer
held any of these as blocking (Darkwing comment 26690, Filbert comment
26694 on #1507). Four of the five are closed by slice 1 S5 (see Done);
this one stays open.
- No test proves that the processes are listed only after the cgroup
is frozen. The code waits for `frozen 1` in `cgroup.events`, and
both Filbert and Dewey confirmed it by reading it. Mutants r2-B5b and
C4 survive. Proposed: a test-only hold after `members`, or a FUSE or
privileged fixture. Filbert F1.
(2026-10-04, #1507)
- **CHAT-03 `engine.envKeys` accepts founder credential names.**
`CREDENTIAL_NAME` in `packages/conversation/src/pi-pin.mjs` matches any
`*_API_KEY` or `*_TOKEN`, including `GITEA_TOKEN` and `GH_TOKEN`, which
the S6 runner refuses as founder credentials (REQ-CRED-2). Nothing
builds a Controller outside the tests yet. Whoever wires the entry point
narrows the pattern to provider names or checks the founder list.
Filbert N2 on #1522. (2026-10-10)
- **`test-task.sh` makes a live Pi call without the Docker guard.** The
"user recall" block near line 478 runs `run-task.sh` with no
availability check, so a run with Docker unavailable fails "user recall
run succeeds" and "recalled user name" (26 pass, 2 fail). The other
live groups skip. Rocko hit it during the S2 suites. Wrap the block in
the same guard as the other live groups, so a seat without provider
access gets clean skips. (2026-10-04, #1519)
- **`node --test <dir>/` fails on Node 24.** Node 24 reads a directory
argument as a module, so every package's test command in that form
fails there, while Node 26 accepts it. The glob form
(`node --test 'packages/x/tests/*.test.mjs'`) works on both. Package
READMEs, `docs/TOOLS.md` and Sage's suite recipe should use the glob
form wherever Node 24 is a target. Darkwing, S1 packet. (2026-10-04,
#1518)
- **`stack-upgrade.sh` doesn't check `$REPO`.** Nothing confirms the
source checkout is root-owned or that its tree is clean before the
upgrade builds from it. Filbert N2 on row 233 (29922, #1735).
(2026-10-21)
- **`stack-upgrade.sh` has no signal handling between A2 and A5.** A HUP,
INT or TERM in that stretch leaves the run failed closed, but recovery
is by hand. A trap that writes the stop point and the recovery step to
the log would make it a sheet step. Filbert N3 on row 233 (29922,
#1735). (2026-10-21)
- **`checkCreative` doesn't refuse `credentials`.** Row 213's
`checkCreative` (packages/business/src/role.mjs:143 at 2d2ad2760)
limits a creative role's tools, authority and messaging, but a role
file that gave companion or research a credential would still load.
The business file could then map a credential to that instance
(business.mjs:157). Nobody has traced whether a mapped credential
reaches the run's environment. The decision paths stay closed (M6),
but a creative role with `web` holding a live token is a wider channel
than design 4 means. The fix: refuse any `credentials` in
`checkCreative`, with a role.test.mjs case. It goes after row 213
lands, as a follow-up row or in the next row that edits role.mjs.
Webby N1 on row 213 r3 (REVIEW-r3.md, 3ab3c010b), mos-1160.
(2026-10-21)
- **The boot host check orders only one business's bus.**
`mosaic-hostcheck.service` (row 240) has
`[email protected]`. `case2-unit.sh` enables the
bus as `mosaic-bus@<business>.service`, so on a host whose business id
isn't `mosaic-stack` nothing orders the two. The bus can start before
the boot stop runs, and after a boot-time drift role sessions can use
bwrap until it does. The fix: `After=mosaic-hostcheck.service` in
`packages/cli/systemd/mosaic-bus.service.in`, which orders every
instance and does nothing where the unit isn't installed. It goes in
before the first install whose business id isn't `mosaic-stack`.
Filbert N1 on row 240 (29945, #1746). (2026-10-21)
- **The run sheet writes the apt hook before the first `--check`.**
Section 4 installs `80mosaic-hostcheck`, then runs
`hostcheck.sh --check`. If that check fails, the next package run
closes bwrap before anyone has read the failure. It fails closed, but
it surprises an installer. The fix: run `--check` first, or say what a
failure at that step means. Doc-only, for the next row that edits
`docs/guides/host-run-sheet.md`. Filbert N2 on row 240 (29945, #1746).
Filbert's N3, that a closed bwrap appends to `DRIFT` on every later
apt run and boot, is by design; ops-01's STATE read should expect the
repeats. (2026-10-21)
- **The sso-config first-step hint has no business.** When the target
copy has no `console.sso`, `stack-release.sh` (`:938`) refuses with
"activate $TO without --sso first, then ... activate $TO --sso". The
usage line is `activate ID BUSINESS` (`:47`), so pasting either command
as printed gives a usage error, not the first step. It fails safe,
since the usage error changes nothing. The fix: add `$BUSINESS`
(set at `:2138`) to both commands, and the business (`acme`)
to the three expected strings in `stack-release.test.mjs`
(`:1538`, `:1689`, `:1691`). For a later row that edits
`stack-release.sh`. Webby N1 on row 238 r1 (REVIEW-r1.md, 433a95292,
#1744). (2026-10-21)
- **Lane image prep inspects a base that BuildKit never tagged.**
(N7 of the row 197 proof.) After the Compose build,
`scripts/testing/lane-image.mjs:41` (`prepareImage`) runs
`docker image inspect node:24-bookworm-slim` to put the base digest
in the receipt. BuildKit resolves the base from the registry
without tagging it in the local store, so on a host without the tag
the inspect throws "No such image" after a good build. That
happened in Sage's proof try 1 (2026-10-21 09:03Z, run cf48efccf083).
The throw fails closed: the run's terminal record is unclean, both
lanes quarantine, and `gate-lane.sh recover` clears them. Pulling the
base first fixes it, and the receipt digest then matches what the
build used (91acba91 both ways). The fix: before the build, inspect
the base and refuse with a named reason if it's missing, or pull it.
After the build, refuse if the base digest changed. Add a test with a
fake Docker that has no base tag. Kept out of row 197 r6 by Sage
(sage-1883). Mos asked for this record in mos-1162. (2026-10-21)
- **Case 1 acceptance script: root parses with the node it's checking, and the header says less than root does.**
(Duckworth N1-N3 on the row 195 manifest, REVIEW-m1 6afcee2a5.)
N1: F9 runs `$ROOT$NODE` as root (`scripts/case1-accept.sh:298`)
before F10 checks that `--node` is the `/opt/mosaic-stack/node-<pin>`
path (:303-304). On a host installed as designed, `case2-unit.sh:128-133`
refused a non-root-owned `--node` and the args file is root's, so no
escalation path exists today. Run F9's parse through `AS_USER`, or
apply F10's path check first. N2: the header (:10) and the TOOLS.md
entry say root only reads, but root also runs `curl` (F2), `sudo -l`
(P1, P3), the jarvis probe (P9), `hostcheck.sh --check` and
`systemctl`. Doc-only. N3: F6's prefix match (:261) accepts a
`--claude` with `..` after `claude-<pin>/`. That claude runs only as
`mosaic`, and F10 uses equality. Darkwing has a follow-up on
`darkwing/row195-n1` 1e3c4767c, not yet run; it gets its own row now
that 195 has landed (b65014647), Duckworth reviews. (2026-10-21)
- **Morgana's row 213 mutation set: the B3 pattern no longer matches `bundle.mjs`.**
(Webby N3 on row 221 round 1, REVIEW-r1 a0a00630e.) Row 221's
candidate changes `packages/harness/src/bundle.mjs`, and the B3
anchor at Morgana's 213 `mutants.py:48` stops matching on that
base. This matters only if someone reruns that set. Fix the anchor
in that tooling before any rerun, and check that a stale anchor
refuses instead of skipping the mutant (sage-1941). (2026-10-21)
- **Grant executor test X5 starts a `sleep 30` that can outlive its test.**
(Webby N1 on row 248 r1, webby-0080; deferred by Duckworth in the row
249 review, REVIEW-r1 28174fe08, #1751 comment 30070.) The test at
`packages/bus/tests/grant-executor.test.mjs:989` (:934 before row
249) runs `sleep 30` on the sshd side, where an abort doesn't reach it. Fix it with a stub
that exits once its test dir is gone, as row 248 did for the hold
stubs, or with a shorter sleep. This binds the next row that touches
that test file. Until then, a lane cleanup that runs after this file
waits 30 s before it calls a lone `sleep 30` a leak. The row 197
lanes scan only at lane end, about 15 minutes after pkg-bus, so they
can't see it (mos-1169). (2026-10-21)
- **Row 66 live checks: the unslop notice on a live managed session, both harnesses.**
(mos-1186, Vikunja #454.) Row 66's gate asked for one landing session per
harness with a prompt that draws a known tell. Mos changed that gate. The
Claude Code session ran before landing on a scratch root from the
candidate tree (run r4ab24630d323, landed 2c38325ec). Pi ran launch only there (run r1dd94171bede):
this host has no credential pi reads until P19 #383 settles. Two live
checks remain. Claude Code: in the first re-pin whose tree includes row 66,
backup first (Q173). Pi: one full session with a tell after #383. Each
attaches the operator note on the RESULT, the next-turn correction and the
`unslop.jsonl` counts. (2026-10-21)
- **Console tester listener: `testerNoSignIn` is a dead parameter.**
(Duckworth N2 on the row 196 manifest, REVIEW-m1; carried as C1 in
REVIEW-m2 cdbf7b64e, #1694 comment 30156.) Row 196 landed 7247d80c7
with the parameter and its comment still at `serve.mjs:472-475` and
passed from `webui/src/cli.mjs:109` and `sso.test.mjs:392`. Nothing
in `serve.mjs` reads it after the destructure. Remove it, or wire it
with a test. This binds the next row that touches
`packages/webui/src/serve.mjs`. (2026-10-21)
- **Console library routes: the tester listener and `library.sync`.**
(Duckworth N5 on row 196 round 2, REVIEW-m2 cdbf7b64e.) At 7247d80c7
`startServer` (`serve.mjs:475`) takes none of `libraryFile`,
`libraryStatus` or `librarySync`, so the callbacks `cli.mjs` passes
do nothing yet. The row that adds the Console library routes decides
whether the tester listener gets `library.sync`. Duckworth's view is
that it shouldn't, because sync is a human write. (2026-10-21)
- **Console library routes: four library codes still map to 502.**
(Dewey N2 on the row 254 manifest, #1759 comment 30173.) Row 254
gave six library codes a STATUS entry in `packages/webui/src/bus.mjs`.
`library-mapping-invalid`, `library-remote-invalid`,
`library-state-invalid` and `library-git` still fall through
`busStatus` to 502. The row that adds the Console library routes
decides their statuses and gives its `humanCall` for `library.sync`
a timeout over 130 seconds. (2026-10-21)
- **T27 sweep test leaves two `sleep 60` children running.**
(Darkwing row 251 slot, darkwing-0113 and darkwing-0117; Dewey on
r2, comment 30168.) The sweep test spawns two detached shells,
`planted` at `host-run.test.mjs:528` and `decoy` at `:536`, each
running `sleep 60; :` with the run dir as `$0`. `sweepRunDir` kills
the planted shell by pid (`host-run.mjs:238`), and `t.after` kills
only the decoy shell. Neither kill reaches the `sleep` child, so each
one lives for up to 60 s after the test and trips any busy check that
follows. The fix is to kill the process group in both `t.after`
hooks: `process.kill(-pid, 'SIGKILL')`. Each shell is spawned
detached, so it leads its own group. `exec sleep 60` doesn't work.
The sweep matches the run dir in `/proc/<pid>/cmdline`
(`host-run.mjs:236`), and exec drops the `$0` that carries it, so the
planted process would survive and the "planted process killed" assert
would fail. This binds the next row that touches
`packages/bus/tests/host-run.test.mjs`. (2026-10-21)
- **Real-model suite cases run outside the gate.** Row 244 moved
`test-task.sh` fork recall (M11) and user recall (M16) behind
`MOSAIC_LIVE_PROVIDER=1`, and `test-release.sh` activations to the mock
adapter. Before any re-pin whose tree changes `packages/harness` or the
session fork code (`scripts/mosaic-task.mjs` session and
`sessionForkFrom` handling, the pi adapter), an operator runs
`MOSAIC_LIVE_PROVIDER=1 bash scripts/test-task.sh` with the real pi
credential and a real `release.sh activate`, and records both results on
the re-pin row. Morgana's wording (row 244 BUILD.md), Webby N2 on row 244
r1 (#1757). (2026-10-21)
## Queue
Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
## Done
- Ledger (piece 3, #1506): shipped cd0aa5fb, Gate D written 2026-09-12 (18.9 human messages per closed issue).
- Relaunched seat shows the old last message (darkwing, Step 3 log, 2026-09-12): closed by #1512 relaunch activity, af4203ca. Filbert authored it; Darkwing and Dewey reviewed. The live board restart and Gate F remain separate.
- Sage launcher hook uncommitted (2026-09-12): closed by the agents/sage launch files commit, Dewey review R1 revise, R2 approve (`agents/dewey/work/sage-launch-review-2026-09-26.md`), launcher test now covers sage.
- Ledger counts T3 agent messages as human (2026-09-26, #1506): fixed in ef0020ad (Darkwing 6a, Filbert reviewed). `messageKind` now counts a T3 header as agent and the control-board sender as board. It changes no current count, because no source the ledger reads has a T3 header yet. That waits for the T3 thread source (Gate F).
- Control board checked the bind address but not Host or Origin (2026-09-26, #1507, found by Dewey): closed by the board guard commit. `foreignRequest` refuses a non-loopback Host, a wrong port, userinfo or a path in Host, and any foreign Origin on every route. Dewey authored it, and Rocko approved it (`agents/rocko/work/board-guard-review-2026-09-26.md`, 5a12f08e). Rocko's one low finding: URL normalization accepts a root slash, empty userinfo and an empty query in Host, so the notes' wording overstates what the raw-syntax check refuses. No bypass was found.
- Discord engine: leaked fake pi and the timeout gap in `busy` (2026-09-26, #1509): fixed in 3edb15eb (Darkwing 6b R2, Rocko approved, review ed5510a0). Tests stop the engine in `finally`. A turn pi never started now stops pi with exit 1 instead of guessing, and the unit restarts it. Known limit: see the open entry on wedge restarts.
- Combined concurrent test hangs (2026-09-14, #1509): closed on evidence from 3edb15eb. At default concurrency the union passed 406/406 three times on the R2 snapshot and 408/408 on the commit's index, where clean HEAD had hung four runs out of four. The inherited-intermittency entry from 2026-09-15 stays open until a later run shows it is the same cause.
- A nested `node --test` passes even when its tests fail (2026-09-26, #1508): closed. Queue A1 (34a72af9) clears NODE_TEST_CONTEXT on its own path. N13 fixes `test-foundation.sh` and `test-discord.sh`, each with a check that fails the suite if the clearing is lost. Darkwing wrote it (n13.patch 00868b2f) and Filbert approved it (e464be6c). Sage's grep on 2026-09-27 finds no other nested `node --test`: in scripts, only the two suites, `test-queue.sh` and `queue-commit.sh`, and in JS, only the queue test that covers the fix.
- SetSpark service accepted any string as a decision approver (2026-09-26, #1509): closed. Fixed in shared-signals cc74d92 (Rocko approved R2, lead decision 24). The operations seat (T3 12fe8cda) deployed it to VM 1022 at 00:23Z on 2026-09-27. The running `service.py` hash matches cc74d92, and `/healthz` returns 200 inside the network and through the public tunnel. The read-only README survey found one row breaking the rule (DEC-009, Proposed, version 1), one open request that isn't ready (DEC-009's), and no Accepted decision with more than 16 approvers. No rows changed. DEC-009 stays as stored (lead decisions 21 and 28). The service now refuses approvals on it, so nothing can go wrong silently. Side effect: `up -d` also recreated caddy, about 20 s of edge downtime for all hosts. That is for the shared-signals deploy procedure to fix, not Mosaic.
- Queue suite failed its live checks outside the canonical checkout
(2026-09-27, #1508): closed by Piece D, f539466f. `test-queue.sh` now
skips `verify` and `render --check` with a printed reason when the
toplevel isn't the recorded `canonicalRoot`. An index export passed
27/0, and the canonical checkout 29/0.
- Queue: an owner listed as a reviewer blocked the row (2026-09-27,
#1508): closed by row 31, bb2fa970. `add`, `set reviewers` and `assign`
now refuse the owner as a reviewer with exit 2 and write nothing. Rocko
built it in the Gate G run, and Filbert approved round 1 (#1508 comment
26629, 8 of 8 mutants caught).
- Queue follow-ups from rows 12, 13 and 31 (2026-10-04, #1508): closed by
row 33, 6fb50cc0. Filbert built it, and Darkwing approved round 1
(#1508 comment 26651).
- `genesis` refuses a map row whose owner is one of its reviewers (exit
2, nothing written). The check is on the map, not in replay, so a log
from before the rule still loads.
- The `assign` refusal now reads "can't assign row N to SEAT: SEAT is
one of its reviewers".
- `queue-commit.sh` compares HEAD with H before each canary and again if
the clean run fails. A moved HEAD exits 2 with "HEAD moved since H was
recorded"; the guard message is kept for a real guard failure.
- Every time and date the validator accepts must format back to itself,
so month 13, day 32, 2026-02-30, 2027-02-29 and 24:00 are refused.
The live log at rev 45 still replays.
- The cold-start failure (2026-09-27): not reproduced in 20 cold runs.
Each ran `node --test packages/queue/tests/` in a fresh tree, 10
shared clones and 10 exports, one after another. All 20 passed
148/148 (2960 of 2960). The 300 ms deadline test is unchanged. Counts:
`agents/filbert/work/queue-33/cold-runs.txt`.
- CHAT-03 I1 round 2 follow-ups (2026-10-04, #1507): four of five closed
by slice 1 S5 (row 40, #1522; Dewey authored, Darkwing and Filbert
reviewing). The commit is named in the row's review record when it
lands.
- Engine command and `preArgs`: `engine` takes only `extraArgs`, `cwd`
and `envKeys`. The controller always launches the pinned Pi and
checks that at construction and at bind. The fake engine comes in
through a symbol key that JSON config can't carry (N24b).
- Engine environment: an explicit list (`ENGINE_ENV` in `pi-pin.mjs`)
plus `*_API_KEY` and `*_TOKEN` names from `engine.envKeys`. Nothing
else is inherited. `HOME` passes, so Pi reads `~/.pi/agent` unless
`PI_CODING_AGENT_DIR` is set (Darkwing F3).
- `escalating` is cleared when the force-stop fence throws (Darkwing F2,
races.test.mjs).
- Takeover then Enter in one chunk: input after Ctrl-T or Ctrl-O waits
for that action, in the same chunk or a later one, and also while an
earlier action is still pending, so it is judged as if typed one key
at a time (Filbert F2; Darkwing and Filbert T1 on #1522;
flows.test.mjs).
The frozen-cgroup test (Filbert F1) stays open above.
- Gate runs mount the host's pi auth file (Webby N4 on the row 197 proof
driver, 059577886): closed by row 244 (#1757), landing e69260a3c. The
suites and the lane's `cleanEnv` export a 0600 fixture `PI_AUTH_FILE`,
`guardDocker` refuses a Compose run without one, and
`packages/cli/tests/gate-lane-auth.test.mjs` proves no gate step mounts
the host default. The real-model cases this moved off the gate are open
above.
## Row253 F5: legacy Case AS_USER supervision
Tracked as row 276, Gitea #1781 (sage-2331), owner Gander, reviewer Duckworth, after row253.
Brief: docs/plans/2026-10-22_case-sudo-legacy-followup.md, “Brief: supervised legacy AS_USER calls”.
case1-accept.sh's legacy timeout330 around AS_USER talk and its AS_USER precheck, Claude version, PM launches and Node version diagnostics predate the new socket metadata/probe helpers. sudo's pty monitor can move them to a separate session; the old outer timeout does not prove their descendants are killed. The C4 repair in row253 covers only its new config query and peer probe. These legacy paths need the reviewed supervisor or a header explicitly explaining why each remains unsupervised. The follow-up must preserve the 300-second talk/reply contract, concurrent F5 scan, diagnostics and secret-output refusals, with fake-only session-hop tests and Sage's slot. No host or live unit change is authorized.
@@ -0,0 +1,10 @@
{
"agents/gander/work/queue-253/c4/case1-accept.sh.txt": {
"source": "scripts/case1-accept.sh",
"sha256": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f"
},
"agents/gander/work/queue-253/c4/case2-unit.sh.txt": {
"source": "scripts/case2-unit.sh",
"sha256": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd"
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,44 @@
# C4 repair for the short static pass
Prepared under duckworth-0111 and sage-2325/2331. No tests, syntax execution, probe, real sudo or host experiment ran on this repair. No passing claim applies to these new product bytes. DESIGN.md and the approved DESIGN-C4.md remain unchanged. The original frozen snapshots remain unchanged.
REPAIR.diff compares the repaired scripts against their original frozen source, and tests/docs against 968db0bcd. The *.repair.txt files are non-executable exact snapshots of all seven pending source/test/doc paths. REPAIR-SHAS.json pins them. The live source/test paths remain uncommitted until the gate permits the product commit.
## Conditions mapped to the diff
C1: fixed setsid without -f/-w; confirm the direct child's kernel SID equals its PID. M38 drops setsid; the named row253 C4 signals query killer must fail the exact numeric PID/SID equality assertion on the first query-pids line, in each kind and both sudo modes, without a timeout. A blocking child can still reach inner-timeout without setsid, so the refusal reason alone does not prove session creation (mos-1298).
C2: exact inner/outer/output-limit refusals in both fake modes, query 5/7 s and probe 2/4 s. Outer cases require at least the outer duration and less than outer + 2 s. Root discards the valid-looking stdout of a stalled supervisor. A trusted notice plus status 137 identifies the outer timer.
C3: read complete stat files, strip through the last closing parenthesis followed by space, skip vanished entries, refuse on other errors. The setpgid member has newline, closing parenthesis and space in comm. M36 kills only the leader group and must fail that case. The existing 250 ms cleanup budget and 50 x 10 ms post-return test polling remain.
C4: DEFERRED.md names row 276 / #1781 and the brief from sage-2331. It is an intended manifest path. UPSTREAM-PATHS.json records the requested comparison. Current upstream DEFERRED.md was preserved before our entry.
C5: the test compares both inline payloads byte for byte; M39 adds a comment to only one. Both payloads were prepared from supervisor-source.txt.
N1: the fake reads its mode with the Bash file-read builtin form. N2: fixture cleanup rechecks SID before each kill, including members discovered by an owned-session scan. N3: exec mode cannot safely scan the runner's session, so it checks exact PIDs and says so; hop mode checks all members of self-led recorded sessions. N4: the fake still makes no synthetic HUP on its own death. N5: polling remains bounded at 500 ms. N6: the packet must be published before the manifest round.
## Line references in matching repair snapshots
- scripts/case2-unit.sh:162: `stat_fields()`
- scripts/case2-unit.sh:180: `clean_session()`
- scripts/case2-unit.sh:220: `"$setsid_bin" "$@"`
- scripts/case2-unit.sh:221: `for ((attempt=`
- scripts/case2-unit.sh:240: `tester_tools()`
- scripts/case2-unit.sh:270: `tester_capture()`
- packages/cli/tests/case2-unit.test.mjs:192: `for(const kind`
- packages/cli/tests/case2-unit.test.mjs:219: `C4 stalled supervisor`
- packages/cli/tests/case2-unit.test.mjs:233: `C4 inline supervisor`
- packages/cli/tests/case-sudo-fixture.mjs:21: `case_sudo_run()`
- packages/cli/tests/case-sudo-fixture.mjs:84: `function sessionPairs`
- packages/cli/tests/case-sudo-fixture.mjs:75: `function killPairs`
- packages/cli/tests/case1-accept.test.mjs:309: `C4 acceptance query inner`
- packages/cli/tests/case1-accept.test.mjs:322: `C4 acceptance query outer`
- packages/cli/tests/case1-accept.test.mjs:345: `for(const fault`
- docs/plans/DEFERRED.md:456: `## Row253 F5`
## Proposed next gate
On Sage CLEAR after static approval: both touched files whole, M1-M39 with passing focused bases and byte-identical restoration, CLI whole, WebUI whole and the ten shell files. Previous completed results are historical only because product blobs changed. Use a gXXXXXX TMPDIR under /mnt/storage/scratch/tmp, pipefail and tee, a busy check before every launch, and log/wrapper names that do not match the busy detector. Stop and report according to the new grant.
REPAIR.diff uses zero context so blank context lines in the stored patch do not trigger whitespace checks on the record itself. The full matching snapshots provide surrounding context.
@@ -0,0 +1,30 @@
{
"scripts/case2-unit.sh": {
"frozen_before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"repair": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe"
},
"scripts/case1-accept.sh": {
"frozen_before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"repair": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7"
},
"packages/cli/tests/case-sudo-fixture.mjs": {
"frozen_before": "5c2cf5408944f8c1715355f75e2510a8deee814b90e2b44befb5a7dff411d6b7",
"repair": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008"
},
"packages/cli/tests/case2-unit.test.mjs": {
"frozen_before": "83cd18f05e60d489e4b0e581d9d977fbf86f897ea3d0bddaa13a63e86f22321f",
"repair": "72a07e9ad9d5c8bb01851eef18f792f2fe9b95d56803e313fad6aa1f1d6c3e8c"
},
"packages/cli/tests/case1-accept.test.mjs": {
"frozen_before": "3ed99803a7e3f81d8dce62e0577cd6c86b70a0339e688c0124bcb10d149e8de9",
"repair": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901"
},
"packages/webui/README.md": {
"frozen_before": "04b7c1165c4fa73137a72e8373d70fd7d96a5897b5511170e6d41908a7667e96",
"repair": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9"
},
"docs/plans/DEFERRED.md": {
"frozen_before": "cadfb704f03e1bfcd36e56ca7a1a3636955004e7d96be9b8765f35ea750da053",
"repair": "67bbf7bf57aa70dcade36ca4642f01cccb71ce01c130a88b84e1edb7ea674377"
}
}
+684
View File
@@ -0,0 +1,684 @@
--- a/scripts/case2-unit.sh
+++ b/scripts/case2-unit.sh
@@ -19 +19,4 @@
-# dependencies. It renders the default instance only.
+# dependencies. The inner query supervisor runs after the uid drop: 5 s inside,
+# 7 s on root's wait. Fixed system Bash, setsid, sleep, timeout and head are
+# resolved and root-validated. Only the config query uses this supervisor.
+# It renders the default instance only.
@@ -153,0 +157,113 @@
+# row253-supervisor-begin
+IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
+set +m
+seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
+child="" timer="" verified=0
+stat_fields() {
+ local pid="$1" raw rest
+ if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
+ [ -e "/proc/$pid/stat" ] && return 1
+ return 3
+ fi
+ [ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
+ rest="${raw##*) }"
+ [ "$rest" != "$raw" ] || return 1
+ read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
+ [[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
+}
+clock_cs() {
+ local boot spare
+ read -r boot spare < /proc/uptime || return 1
+ [[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
+ CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
+}
+clean_session() {
+ local entry pid rc live until
+ clock_cs || return 1
+ until=$((CLOCK_CS + 25))
+ while :; do
+ live=0
+ for entry in /proc/[0-9]*/stat; do
+ pid="${entry%/stat}"; pid="${pid##*/}"
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" != 3 ] || continue
+ [ "$rc" = 0 ] || return 1
+ [ "$ST_SESSION" = "$child" ] || continue
+ case "$ST_STATE" in Z|X) continue ;; esac
+ live=1
+ if ! kill -KILL "$pid" 2>/dev/null; then
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
+ fi
+ done
+ [ "$live" != 0 ] || return 0
+ clock_cs || return 1
+ [ "$CLOCK_CS" -lt "$until" ] || return 1
+ "$sleep_bin" 0.01
+ done
+}
+finish() {
+ local rc="$?"
+ trap - EXIT HUP INT TERM
+ if [ "$verified" = 1 ]; then
+ clean_session || rc=86
+ elif [ -n "$child" ]; then
+ kill -KILL "$child" 2>/dev/null || :
+ fi
+ [ -z "$child" ] || wait "$child" 2>/dev/null || :
+ if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
+ exit "$rc"
+}
+trap finish EXIT
+trap 'exit 88' HUP INT TERM
+"$sleep_bin" "$seconds" & timer=$!
+"$setsid_bin" "$@" & child=$!
+for ((attempt=0; attempt<100; attempt++)); do
+ rc=0; stat_fields "$child" || rc=$?
+ [ "$rc" = 0 ] || exit 85
+ if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.002
+done
+[ "$verified" = 1 ] || exit 85
+while kill -0 "$child" 2>/dev/null; do
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.01
+done
+kill -0 "$timer" 2>/dev/null || exit 81
+rc=0; wait "$child" || rc=$?
+[ "$rc" = 0 ] || exit 87
+exit 0
+MOSAIC_TESTER_SUPERVISOR
+# row253-supervisor-end
+
+tester_tools() {
+ local input target p st magic
+ TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
+ for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
+ target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
+ case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
+ [ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
+ p="$target"
+ while :; do
+ st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
+ [ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
+ [ "$p" != / ] || break
+ p="${p%/*}"; [ -n "$p" ] || p=/
+ done
+ magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
+ [ "$magic" = 7f454c46 ] || return 1
+ case "$input" in
+ /bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
+ /usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
+ /usr/bin/head) TS_HEAD="$target" ;;
+ esac
+ done
+}
+tester_capture_reason() {
+ case "$1" in
+ 81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
+ 84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
+ *) echo inner-command ;;
+ esac
+}
@@ -155,5 +271,11 @@
- local seconds="$1" maximum="$2" file child="" rc=0; shift 2
- file="$(mktemp)" || exit 1
- trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
- trap 'exit 1' HUP INT TERM
- timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
+ local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
+ tester_tools || exit 84
+ file="$(mktemp)" || exit 88
+ notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
+ trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
+ trap 'exit 88' HUP INT TERM
+ LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
+ set -o pipefail
+ maximum="$1"; head_bin="$2"; shift 2
+ "$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
+ ' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
@@ -163 +285,4 @@
- [ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
+ # Only GNU timeout and the trusted head can write this root-side channel.
+ if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
+ [ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
+ [ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
@@ -165,0 +291 @@
+
@@ -259 +385 @@
- query="$(tester_capture 5 4096 sudo -u "$USER_NAME" -H env -i "HOME=$HOME_DIR" "PATH=$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query failed"
+ query="$(tester_capture 5 7 4096 "$USER_NAME" "$HOME_DIR" "$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query $(tester_capture_reason "$?")"
--- a/scripts/case1-accept.sh
+++ b/scripts/case1-accept.sh
@@ -18,0 +19,4 @@
+# The config/probe supervisor is fixed root-validated system Bash plus setsid
+# and sleep, after the uid drop: query inner/outer 5/7 s, probe 2/4 s. Root
+# independently bounds its sudo wait. Legacy AS_USER talk and diagnostics below
+# retain their old behavior and are NOT covered by this supervisor (F5 follow-up).
@@ -110,0 +115,113 @@
+# row253-supervisor-begin
+IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
+set +m
+seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
+child="" timer="" verified=0
+stat_fields() {
+ local pid="$1" raw rest
+ if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
+ [ -e "/proc/$pid/stat" ] && return 1
+ return 3
+ fi
+ [ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
+ rest="${raw##*) }"
+ [ "$rest" != "$raw" ] || return 1
+ read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
+ [[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
+}
+clock_cs() {
+ local boot spare
+ read -r boot spare < /proc/uptime || return 1
+ [[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
+ CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
+}
+clean_session() {
+ local entry pid rc live until
+ clock_cs || return 1
+ until=$((CLOCK_CS + 25))
+ while :; do
+ live=0
+ for entry in /proc/[0-9]*/stat; do
+ pid="${entry%/stat}"; pid="${pid##*/}"
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" != 3 ] || continue
+ [ "$rc" = 0 ] || return 1
+ [ "$ST_SESSION" = "$child" ] || continue
+ case "$ST_STATE" in Z|X) continue ;; esac
+ live=1
+ if ! kill -KILL "$pid" 2>/dev/null; then
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
+ fi
+ done
+ [ "$live" != 0 ] || return 0
+ clock_cs || return 1
+ [ "$CLOCK_CS" -lt "$until" ] || return 1
+ "$sleep_bin" 0.01
+ done
+}
+finish() {
+ local rc="$?"
+ trap - EXIT HUP INT TERM
+ if [ "$verified" = 1 ]; then
+ clean_session || rc=86
+ elif [ -n "$child" ]; then
+ kill -KILL "$child" 2>/dev/null || :
+ fi
+ [ -z "$child" ] || wait "$child" 2>/dev/null || :
+ if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
+ exit "$rc"
+}
+trap finish EXIT
+trap 'exit 88' HUP INT TERM
+"$sleep_bin" "$seconds" & timer=$!
+"$setsid_bin" "$@" & child=$!
+for ((attempt=0; attempt<100; attempt++)); do
+ rc=0; stat_fields "$child" || rc=$?
+ [ "$rc" = 0 ] || exit 85
+ if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.002
+done
+[ "$verified" = 1 ] || exit 85
+while kill -0 "$child" 2>/dev/null; do
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.01
+done
+kill -0 "$timer" 2>/dev/null || exit 81
+rc=0; wait "$child" || rc=$?
+[ "$rc" = 0 ] || exit 87
+exit 0
+MOSAIC_TESTER_SUPERVISOR
+# row253-supervisor-end
+
+tester_tools() {
+ local input target p st magic
+ TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
+ for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
+ target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
+ case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
+ [ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
+ p="$target"
+ while :; do
+ st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
+ [ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
+ [ "$p" != / ] || break
+ p="${p%/*}"; [ -n "$p" ] || p=/
+ done
+ magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
+ [ "$magic" = 7f454c46 ] || return 1
+ case "$input" in
+ /bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
+ /usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
+ /usr/bin/head) TS_HEAD="$target" ;;
+ esac
+ done
+}
+tester_capture_reason() {
+ case "$1" in
+ 81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
+ 84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
+ *) echo inner-command ;;
+ esac
+}
@@ -112,5 +229,11 @@
- local seconds="$1" maximum="$2" file child="" rc=0; shift 2
- file="$(mktemp)" || exit 1
- trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
- trap 'exit 1' HUP INT TERM
- timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
+ local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
+ tester_tools || exit 84
+ file="$(mktemp)" || exit 88
+ notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
+ trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
+ trap 'exit 88' HUP INT TERM
+ LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
+ set -o pipefail
+ maximum="$1"; head_bin="$2"; shift 2
+ "$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
+ ' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
@@ -120 +243,4 @@
- [ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
+ # Only GNU timeout and the trusted head can write this root-side channel.
+ if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
+ [ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
+ [ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
@@ -122,0 +249 @@
+
@@ -196 +323 @@
-QUERY="$(tester_capture 5 4096 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query failed"
+QUERY="$(tester_capture 5 7 4096 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query $(tester_capture_reason "$?")"
@@ -536 +663 @@
- peer="$(tester_capture 3 128 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why=probe
+ peer="$(tester_capture 2 4 128 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why="probe-$(tester_capture_reason "$?")"
@@ -540 +667 @@
- else why=probe; fi
+ else [ -n "$why" ] || why=probe; fi
--- a/packages/cli/tests/case-sudo-fixture.mjs
+++ b/packages/cli/tests/case-sudo-fixture.mjs
@@ -3,0 +4,3 @@
+import { spawn } from 'node:child_process';
+import { readFileSync, readdirSync } from 'node:fs';
+
@@ -4,0 +8,13 @@
+case_sudo_entry() {
+ local record="$1" here sid; shift
+ here="$BASHPID"
+ read -r sid < <(/bin/ps -o sid= -p "$here")
+ printf "%s %s\n" "$here" "$sid" >> "$record/session-pids"
+ if { [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
+ { [ -f "$record/stall-probe" ] && [[ "$*" == *SO_PEERCRED* ]]; }; then
+ trap '' TERM HUP
+ [ ! -f "$record/stall-output" ] || /bin/cat "$record/stall-output"
+ exec /bin/sleep 60
+ fi
+ exec "$@"
+}
@@ -6,2 +22,3 @@
- local record="$1" child="" rc=0; shift
- if [ "$(cat "$record/session-mode" 2>/dev/null)" != hop ]; then exec "$@"; fi
+ local record="$1" child="" rc=0 mode=exec; shift
+ [ ! -f "$record/session-mode" ] || mode="$(<"$record/session-mode")"
+ if [ "$mode" != hop ]; then case_sudo_entry "$record" "$@"; return "$?"; fi
@@ -13,7 +30,2 @@
- /usr/bin/setsid /bin/bash -c '
- record="$1"; shift
- here="$BASHPID"
- read -r pid sid < <(/bin/ps -o pid=,sid= -p "$here")
- printf "%s %s\n" "$pid" "$sid" >> "$record/session-pids"
- exec "$@"
- ' -- "$record" "$@" &
+ export -f case_sudo_entry
+ /usr/bin/setsid /bin/bash -c 'case_sudo_entry "$@"' -- "$record" "$@" &
@@ -31 +43,3 @@
- const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>Number(line.trim().split(/\s+/)[1])));
+ // Exec fake entries share the test runner's session. Only self-led sessions
+ // are isolated and safe to scan here; exec coverage also checks exact PIDs.
+ const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number)).filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
@@ -41,0 +56,87 @@
+
+export function recordedPids(file) {
+ try{return readFileSync(file,'utf8').trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number));}
+ catch(error){if(error.code==='ENOENT')return [];throw error;}
+}
+
+export function recordedLivePids(file) {
+ return sessionPairs(recordedPids(file)).filter(([pid,sid])=>{
+ try{
+ const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
+ return Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X';
+ }catch(error){if(error.code==='ENOENT'||error.code==='ESRCH')return false;throw error;}
+ }).map(([pid])=>pid);
+}
+
+export function killRecordedPids(file) {
+ killPairs(recordedPids(file));
+}
+
+function killPairs(pairs) {
+ for(const [pid,sid] of sessionPairs(pairs)){
+ try{
+ const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
+ if(Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X')process.kill(pid,'SIGKILL');
+ }catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
+ }
+}
+
+function sessionPairs(pairs) {
+ // Only self-led records authorize a session scan. Exec-mode records belong
+ // to the runner's session and authorize checking their exact PIDs only.
+ const sessions=new Set(pairs.filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
+ const found=new Map(pairs.map(pair=>[pair[0],pair]));
+ if(sessions.size)for(const entry of readdirSync('/proc')){
+ if(!/^\d+$/.test(entry))continue;
+ try{
+ const raw=readFileSync(`/proc/${entry}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' '),sid=Number(fields[3]);
+ if(sessions.has(sid))found.set(Number(entry),[Number(entry),sid]);
+ }catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
+ }
+ return [...found.values()];
+}
+
+// Safety net for a removed-outer-bound change. It is fixture cleanup, never
+// product supervision. The guard reads only fixture records and checks sid
+// immediately before every kill. Normal outer-bound cases return before it.
+export function fixtureGuard(t,files,milliseconds=9500) {
+ const program=`const fs=require('node:fs');setTimeout(()=>{for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}},Number(process.argv[2]));`;
+ const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds)],{stdio:'ignore'});
+ const stop=async()=>{
+ if(child.exitCode!==null||child.signalCode!==null)return;
+ const exited=new Promise(resolve=>child.once('exit',resolve));
+ child.kill('SIGKILL');await exited;
+ };
+ t.after(stop);return stop;
+}
+
+export function cleanupFixture(t,files) {
+ let captured=[];
+ const capture=()=>{captured=files.flatMap(recordedPids);};
+ // tmp(t)'s directory-removal hook may run first. Retain PID/SID pairs in
+ // memory before assertions, then verify the kernel sid again before killing.
+ t.after(()=>killPairs([...captured,...files.flatMap(recordedPids)]));
+ return capture;
+}
+
+export async function pollFixtureGone(files) {
+ for(let n=0;n<50;n++){
+ if(files.every(file=>recordedLivePids(file).length===0))return [];
+ await new Promise(resolve=>setTimeout(resolve,10));
+ }
+ return files.flatMap(recordedLivePids);
+}
+
+export function blockingQuerySource(records,kind='signals',member,proof) {
+ const py=`import os,sys\nos.setpgid(0,0)\nwith open(sys.argv[2],"a") as f:f.write(str(os.getpid())+" "+str(os.getsid(0))+chr(10))\nos.execv(sys.argv[1],[sys.argv[1],"60"])`;
+ return `import fs from 'node:fs';import {spawn} from 'node:child_process';
+const records=${JSON.stringify(records)};
+function record(pid){const s=fs.readFileSync('/proc/'+pid+'/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);fs.appendFileSync(records,pid+' '+sid+String.fromCharCode(10));}
+record(process.pid);
+export function loadSystem(){
+${kind==='sleep'?`const c=spawn('/usr/bin/sleep',['60'],{stdio:'inherit'});record(c.pid);`:''}
+${kind==='signals'?`process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});`:''}
+${kind==='member'?`const c=spawn('/usr/bin/python3',['-I','-S','-c',${JSON.stringify(py)},${JSON.stringify(member)},records],{stdio:'inherit'});for(let n=0;n<200;n++){try{const s=fs.readFileSync('/proc/'+c.pid+'/stat','utf8');if(s.includes('('+${JSON.stringify(member?.split('/').at(-1))}+') ')){fs.writeFileSync(${JSON.stringify(proof)},s);break;}}catch{}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,5);}`:''}
+Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
+}`;
+}
--- a/packages/cli/tests/case2-unit.test.mjs
+++ b/packages/cli/tests/case2-unit.test.mjs
@@ -16 +16 @@
-import { CASE_SUDO_SESSION, liveRecordedSessions } from "./case-sudo-fixture.mjs";
+import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
@@ -96 +96 @@
- (trap '' TERM; exec sleep 60) </dev/null >/dev/null 2>&1 &
+ (trap '' TERM; here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here"); printf '%s %s\\n' "$here" "$sid" >'${root}/query-owned'; exec sleep 60) </dev/null >/dev/null 2>&1 &
@@ -189,2 +189,4 @@
-// Draft counterexample for DESIGN-C4. Written after sage-2307, not run.
-test('row253 C4 query cleanup handles both exec and sudo session-hop fakes', async t=>{
+// C4 cases run through both sudo process models. Exec shares the runner's
+// session, so its session-wide check would be vacuous; exact recorded PIDs
+// are checked in both modes. Hop additionally isolates the fake session.
+for(const kind of ['sleep','signals','member']) test(`row253 C4 ${kind} query has exact inner reason and cleans all members`,async t=>{
@@ -192 +194 @@
- const h=hub(t),pids=join(h.root,'session-pids'),queryPids=join(h.root,'query-pids'),copy=join(copyDir(h),'packages/cli/src/config.mjs');
+ const h=hub(t),sessions=join(h.root,'session-pids'),queries=join(h.root,'query-pids'),files=[sessions,queries];
@@ -194,21 +196,10 @@
- writeFileSync(pids,'');
- writeFileSync(copy,`import fs from 'node:fs';process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});fs.appendFileSync(${JSON.stringify(queryPids)},process.pid+'\\n');const s=fs.readFileSync('/proc/self/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);if(sid===process.pid)fs.appendFileSync(${JSON.stringify(pids)},process.pid+' '+sid+'\\n');export function loadSystem(){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);}`);
- t.after(()=>{
- if(!existsSync(pids))return;
- // Explicit fixture cleanup is safe only for these recorded fixture PIDs.
- for(const line of [read(pids),existsSync(queryPids)?read(queryPids):''].join('\n').trim().split('\n')){
- const pid=Number(line.trim().split(/\s+/)[0]);
- if(pid>0)try{process.kill(pid,'SIGKILL');}catch{}
- }
- });
- const r=unit(h,{profile:'factory'});refuses(r,/query/);
- for(let n=0;n<50;n++){
- if(!liveRecordedSessions(pids,read,()=>readdirSync('/proc')).length)break;
- await new Promise(resolve=>setTimeout(resolve,10));
- }
- assert.deepEqual(liveRecordedSessions(pids,read,()=>readdirSync('/proc')),[],mode);
- for(const pid of read(queryPids).trim().split('\n')){
- let state='gone';try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{}
- assert.ok(state==='gone'||state==='Z',`${mode}: query ${pid} still ${state}`);
- }
- assert.equal(existsSync(h.log),false,mode);
+ const member=join(h.root,'q ) '+String.fromCharCode(10)+'mem'),proof=join(h.root,'comm-proof');
+ if(kind==='member')symlinkSync('/usr/bin/sleep',member);
+ writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),blockingQuerySource(queries,kind,member,proof));
+ const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
+ const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
+ refuses(r,/^case2-unit: tester config query inner-timeout\n$/);
+ assert.ok(seconds<7,`${kind}/${mode}: ${seconds}s exceeds outer bound`);
+ assert.deepEqual(await pollFixtureGone(files),[],mode);
+ if(kind==='member')assert.ok(read(proof).includes('('+member.split('/').at(-1)+') '),'member comm with newline and closing parenthesis was observed');
+ assert.equal(existsSync(h.log),false,mode);await stopGuard();
@@ -215,0 +207,30 @@
+});
+
+test('row253 C4 overflow is output-limit in both sudo modes',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=hub(t),files=[join(h.root,'session-pids')];writeFileSync(join(h.root,'session-mode'),mode);const capture=cleanupFixture(t,files);
+ writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:${JSON.stringify(h.dataRoot)}});`);
+ const r=unit(h,{profile:'factory'});capture();refuses(r,/^case2-unit: tester config query output-limit\n$/);
+ assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
+ assert.deepEqual(await pollFixtureGone(files),[]);
+ }
+});
+
+test('row253 C4 stalled supervisor uses the outer reason and explicit residual cleanup',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=hub(t),file=join(h.root,'session-pids'),files=[file];writeFileSync(join(h.root,'session-mode'),mode);
+ writeFileSync(join(h.root,'stall-config'),'');writeFileSync(join(h.root,'stall-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
+ const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
+ const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
+ refuses(r,/^case2-unit: tester config query outer-timeout\n$/);
+ assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s slack includes root preflight`);
+ assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
+ if(mode==='hop')assert.ok(recordedLivePids(file).length>0,'R3 residual exists before fixture cleanup');
+ killRecordedPids(file);assert.deepEqual(await pollFixtureGone(files),[], 'explicit fixture cleanup of R3 residual');await stopGuard();
+ }
+});
+
+test('row253 C4 inline supervisor is byte-identical in both host scripts',()=>{
+ const extract=s=>s.split('# row253-supervisor-begin\n')[1]?.split('# row253-supervisor-end')[0];
+ const a=extract(read(UNIT)),b=extract(read(join(REPO,'scripts/case1-accept.sh')));
+ assert.ok(a?.length>100);assert.equal(a,b);
@@ -222,2 +243,2 @@
- t.after(()=>{if(pid)try{process.kill(pid,'SIGKILL');}catch{}});
- const r=unit(h,{profile:'factory'});
+ const capture=cleanupFixture(t,[join(h.root,'query-owned')]);
+ const r=unit(h,{profile:'factory'});capture();
@@ -235,2 +256,2 @@
-test('row253 config parser is strict and output capture is bounded',()=>{
- const source=read(UNIT), body=source.slice(source.indexOf('tester_capture() ('),source.indexOf('tester_location() {'));
+test('row253 config parser is strict',()=>{
+ const source=read(UNIT), body=source.slice(source.indexOf('tester_facts() {'),source.indexOf('tester_location() {'));
@@ -244,2 +264,0 @@
- const r=spawnSync('/bin/bash',['-c',body+'\ntester_capture 1 32 /usr/bin/head -c 100 /dev/zero']);
- assert.notEqual(r.status,0);assert.equal(r.stdout.length,0);
--- a/packages/cli/tests/case1-accept.test.mjs
+++ b/packages/cli/tests/case1-accept.test.mjs
@@ -14 +14 @@
-import { CASE_SUDO_SESSION } from "./case-sudo-fixture.mjs";
+import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
@@ -90 +90,2 @@
-case_sudo_run "$HC" "$ROOT_DIR$cmd" "\${args[@]}"`,
+[ ! -e "$ROOT_DIR$cmd" ] || cmd="$ROOT_DIR$cmd"
+case_sudo_run "$HC" "$cmd" "\${args[@]}"`,
@@ -100 +101,5 @@
-[ -f "$HC/peer-hang" ] && { echo "$BASHPID" > "$HC/peer-pid"; exec sleep 60; }
+[ -f "$HC/peer-hang" ] && {
+ here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here")
+ echo "$here" > "$HC/peer-pid"; printf '%s %s\\n' "$here" "$sid" >> "$HC/query-pids"
+ trap '' TERM HUP; exec sleep 60
+}
@@ -303,0 +309,58 @@
+test('row253 C4 acceptance query inner reason holds under both sudo models',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];
+ put(join(h.hc,'session-mode'),mode);
+ put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),blockingQuerySource(files[1]));
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
+ assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query inner-timeout\n');
+ assert.ok(seconds<7,`${mode}: ${seconds}s`);assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
+ assert.deepEqual(await pollFixtureGone(files),[]);await stop();
+ }
+});
+
+test('row253 C4 acceptance query outer reason discards stdout and names residual cleanup',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-config'),'');
+ put(join(h.hc,'stall-output'),JSON.stringify({dataRoot:HOME+'/.mosaic-dev',socket:true}));
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
+ assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query outer-timeout\n');
+ assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s preflight slack`);
+ assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
+ if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'R3 session remains before fixture cleanup');
+ killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit R3 fixture cleanup');await stop();
+ }
+});
+
+test('row253 C4 acceptance query overflow is output-limit in both models',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);const capture=cleanupFixture(t,files);
+ put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:'${HOME}/.mosaic-dev'});`);
+ const r=run(h);capture();assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query output-limit\n');
+ assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);assert.deepEqual(await pollFixtureGone(files),[]);
+ }
+});
+
+for(const fault of ['inner','outer','overflow'])test(`row253 C4 P10 ${fault} reason holds under both sudo models`,async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];put(join(h.hc,'session-mode'),mode);
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files,7000);
+ if(fault==='inner')put(join(h.hc,'peer-hang'),'');
+ if(fault==='outer'){put(join(h.hc,'stall-probe'),'');put(join(h.hc,'stall-output'),'200 997 997\n');}
+ if(fault==='overflow'){put(join(h.hc,'peer-output'),'200 997 997\n'+'X'.repeat(256));put(join(h.hc,'peer-status'),'0');}
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
+ assert.equal(r.status,0,r.out+r.err);
+ const reason=fault==='inner'?'inner-timeout':fault==='outer'?'outer-timeout':'output-limit';
+ assert.match(r.out,new RegExp('P10 tester-socket-squat unnamed boundary=failed reason=probe-'+reason+' '));
+ assert.doesNotMatch(r.out,/boundary=observed/);
+ if(fault==='inner')assert.ok(seconds<4,`${mode}: ${seconds}s includes functional checks`);
+ if(fault==='outer'){
+ assert.ok(seconds>=4&&seconds<6,`${mode}: ${seconds}s; 2s functional-check slack`);
+ if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'stalled supervisor is R3 residual');
+ for(const file of files)killRecordedPids(file);
+ }
+ assert.deepEqual(await pollFixtureGone(files),[],fault==='outer'?'explicit R3 cleanup':mode);await stop();
+ }
+});
+
--- a/packages/webui/README.md
+++ b/packages/webui/README.md
@@ -128 +128 @@
-need `--port`, and 7330, 7332 or 7333 as `--port` or `--tester-port` exits 4
+need `--port`, and 7330, 7332 or 7333 as the main or legacy tester port exits 4
@@ -130 +130 @@
-`--tester-port` that another installed Console unit in the unit directory
+legacy tester port that another installed Console unit in the unit directory
@@ -144 +144 @@
-`serve --sso [--send] [--tester-port N]` makes the Console an OpenID Connect
+`serve --sso [--send] [--tester-socket]` makes the Console an OpenID Connect
@@ -176 +176 @@
-- With `--tester-port`, the tester listener refuses any request carrying an
+- With `--tester-socket`, the tester listener refuses any request carrying an
--- a/docs/plans/DEFERRED.md
+++ b/docs/plans/DEFERRED.md
@@ -273,14 +272,0 @@
-- **Gate runs mount the host's pi auth file.** `compose.yaml:43` mounts
- `${PI_AUTH_FILE:-/home/jwoltje/.pi/agent/auth.json}` read-only into
- the agent container. No suite sets `PI_AUTH_FILE`, so the
- `test-task.sh` cases that call `scripts/run-task.sh`, and
- `test-release.sh` through `release.sh` `health_check` (`:41-54`), run
- Compose with the host file. Row 197's lane `cleanEnv`
- (`scripts/testing/gate-lane.mjs:109`) keeps the default too: it neither
- sets nor passes `PI_AUTH_FILE`. The mount is read-only, and the legacy
- gate already worked this way before row 197. `test-auth.sh` uses its
- own fixtures but calls `auth.sh`, not Compose. The fix: a fixture
- `PI_AUTH_FILE` for the Compose-reaching cases, set by the suites or by
- the lane's `cleanEnv`, and a test that no gate step mounts the host
- default. It needs its own row. Webby N4 on the row 197 proof driver
- (bba870ee9), traced by Herb (herb-0051). (2026-10-21)
@@ -362,0 +349,34 @@
+- **Console library routes: four library codes still map to 502.**
+ (Dewey N2 on the row 254 manifest, #1759 comment 30173.) Row 254
+ gave six library codes a STATUS entry in `packages/webui/src/bus.mjs`.
+ `library-mapping-invalid`, `library-remote-invalid`,
+ `library-state-invalid` and `library-git` still fall through
+ `busStatus` to 502. The row that adds the Console library routes
+ decides their statuses and gives its `humanCall` for `library.sync`
+ a timeout over 130 seconds. (2026-10-21)
+- **T27 sweep test leaves two `sleep 60` children running.**
+ (Darkwing row 251 slot, darkwing-0113 and darkwing-0117; Dewey on
+ r2, comment 30168.) The sweep test spawns two detached shells,
+ `planted` at `host-run.test.mjs:528` and `decoy` at `:536`, each
+ running `sleep 60; :` with the run dir as `$0`. `sweepRunDir` kills
+ the planted shell by pid (`host-run.mjs:238`), and `t.after` kills
+ only the decoy shell. Neither kill reaches the `sleep` child, so each
+ one lives for up to 60 s after the test and trips any busy check that
+ follows. The fix is to kill the process group in both `t.after`
+ hooks: `process.kill(-pid, 'SIGKILL')`. Each shell is spawned
+ detached, so it leads its own group. `exec sleep 60` doesn't work.
+ The sweep matches the run dir in `/proc/<pid>/cmdline`
+ (`host-run.mjs:236`), and exec drops the `$0` that carries it, so the
+ planted process would survive and the "planted process killed" assert
+ would fail. This binds the next row that touches
+ `packages/bus/tests/host-run.test.mjs`. (2026-10-21)
+- **Real-model suite cases run outside the gate.** Row 244 moved
+ `test-task.sh` fork recall (M11) and user recall (M16) behind
+ `MOSAIC_LIVE_PROVIDER=1`, and `test-release.sh` activations to the mock
+ adapter. Before any re-pin whose tree changes `packages/harness` or the
+ session fork code (`scripts/mosaic-task.mjs` session and
+ `sessionForkFrom` handling, the pi adapter), an operator runs
+ `MOSAIC_LIVE_PROVIDER=1 bash scripts/test-task.sh` with the real pi
+ credential and a real `release.sh activate`, and records both results on
+ the re-pin row. Morgana's wording (row 244 BUILD.md), Webby N2 on row 244
+ r1 (#1757). (2026-10-21)
@@ -427,0 +448,14 @@
+- Gate runs mount the host's pi auth file (Webby N4 on the row 197 proof
+ driver, 059577886): closed by row 244 (#1757), landing e69260a3c. The
+ suites and the lane's `cleanEnv` export a 0600 fixture `PI_AUTH_FILE`,
+ `guardDocker` refuses a Compose run without one, and
+ `packages/cli/tests/gate-lane-auth.test.mjs` proves no gate step mounts
+ the host default. The real-model cases this moved off the gate are open
+ above.
+
+## Row253 F5: legacy Case AS_USER supervision
+
+Tracked as row 276, Gitea #1781 (sage-2331), owner Gander, reviewer Duckworth, after row253.
+Brief: docs/plans/2026-10-22_case-sudo-legacy-followup.md, “Brief: supervised legacy AS_USER calls”.
+
+case1-accept.sh's legacy timeout330 around AS_USER talk and its AS_USER precheck, Claude version, PM launches and Node version diagnostics predate the new socket metadata/probe helpers. sudo's pty monitor can move them to a separate session; the old outer timeout does not prove their descendants are killed. The C4 repair in row253 covers only its new config query and peer probe. These legacy paths need the reviewed supervisor or a header explicitly explaining why each remains unsupervised. The follow-up must preserve the 300-second talk/reply contract, concurrent F5 scan, diagnostics and secret-output refusals, with fake-only session-hop tests and Sage's slot. No host or live unit change is authorized.
@@ -0,0 +1,275 @@
[
{
"id": "M1",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "ec09134dd5e735d5e2d3c9a92162a6d0dd9cc2e1718cf2e2efd3d508a7db3d13"
},
{
"id": "M2",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "5757898fb9003312b67c98037a524dcf06a8dc43e07572a21aae1afb0067e0ff"
},
{
"id": "M3",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "e9df1616060cb45e228e8c3a633299cbd2cc8e8d913544678eab3299bb3a8548"
},
{
"id": "M4",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "3cfe29b917726d13819fabc390845df1367423bb251d3cce2cd270164fe143d0"
},
{
"id": "M5",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "41f91ef6413543602ef46f412e8e289a6dd5ce24bd271cebba05c819242ebe56"
},
{
"id": "M6",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "19d71aed6a102e519f97151ca83d01dc9534b7c4722b1365b0fc365daac4bba9"
},
{
"id": "M7",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "7c629e0da6101109cd9ae7f8028fd68bb16a43e2b799ee0c8de87f13e1055652"
},
{
"id": "M8",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "dcc4a888a21c8a6cbfbfbe48df892c81b8fd038868e0dcd9b1acabf2dceb5313"
},
{
"id": "M9",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "2dccf96df3518c4e604e36245e62a2d446719e1ae1bd5bd47890fc692acad43c"
},
{
"id": "M10",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "ba59a37dac46254f148a0f40367186172ca9e8bbb98020a0c97d7d325c5e86e0"
},
{
"id": "M11",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "58f9e51f48d41c0887894f34e278937911c380d18a63770342a8e1afd1f5dc59"
},
{
"id": "M12",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "3a755dd95af1d11f0cc0bf1ae59a676b62ee7d7b72174ebb88388c6d48fa41ff"
},
{
"id": "M13",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "b230bc14a1197c9986ddb99689a0b0dc71453903f1a6a07328c9d10ceec6ff30"
},
{
"id": "M14",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "0179e9d29cc9461b665e807b00e683ce30f30e2903efc1f8ec4a1599876681f4"
},
{
"id": "M15",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "1d93cd35e92a079cbe31b71a69314fd98d1547e35d62c9a152a5fa4a67c873eb"
},
{
"id": "M16",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5c60833fa9e7fa40bfdd10290cc3e91c7476b5b1f0975367760bbedddbf91c19"
},
{
"id": "M17",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "845c7ccf5c178fb783cbf8c88d329780a0f9e4c63a2c262b6473f0eebd0f0c5e"
},
{
"id": "M18",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "ab32a811142fd84de78fe93ac541b6ee823d7d9422f816e6bd15035149a32ac2"
},
{
"id": "M19",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5ee29b36b15b45f031a6651e82898d6bb1a5ebeff4b47c98b5777f00ab535ed3"
},
{
"id": "M20",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "1c85dc5f63d4766226bb13b701b0358536e7a2a918b224841650acf244a0638a"
},
{
"id": "M21",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "b13942590fb6f518446302340c66465a16ba9ab9e4429f6fcc01714cbc53c63e"
},
{
"id": "M22",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5a87dc77e3bba09ecb9f6b90c653cbfa38117c13674c770c0af0d941c2c63fe1"
},
{
"id": "M23",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "2b7d4d59bfe4ec8f2c9d7c5562be1c57a4ecc6e4d0b4960c1ae44958f2ad8dd9"
},
{
"id": "M24",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "e0f388ef748d5e6a3df40ac486891c14d91fcafdb8ce03b36ef20f93aebbdfe2"
},
{
"id": "M25",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5ba47cf8b8a045967887d108d71a7456cba89e73c096cbdfbf76e010b4d3c5f2"
},
{
"id": "M26",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "cca6f1d39fde22603cbd649d6712c0e05041bd1a86eb7cd9b1c410f58c734a09"
},
{
"id": "M27",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "8b6b71c84ca96f3caf6e832988573a608fccd3c74fa7798468565d0b8b202c5a"
},
{
"id": "M28",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "25e1d996be7c62091b1a3a837fd8fe95871bc01f2a2d509eb1ae09785b51ea48"
},
{
"id": "M29",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "eb5e9d9a069ce226ab70baef11dc7897ae4e3d44ac4807cbed838ba1ded47494"
},
{
"id": "M30",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "280ba5c20d3b42423d681e69312731aa60b2396b1605e875a55d3e87d908eba1"
},
{
"id": "M31",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "2884a7d730d0357e4545cfccd446cad1fb5a98f7f1d86698f778063b0025b451"
},
{
"id": "M32",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "51b7bce4b8f88de20c68797afac71e4dd43d29119e68bcf65cb056f630a87011"
},
{
"id": "M33",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "dc86d9be155f56667ddabf5183d757b55fa339ff86af40ce039f0d63e6e228a6"
},
{
"id": "M34",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "3ef45b6044db0722ff25bd85260c818be9217cf43ea26be0e3f77872af0e70b1"
},
{
"id": "M35",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "c3de12c5a897407a2e0443536bfe5c97d4cefb757f23a5ba301b9479400c09d5"
},
{
"id": "M36",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "d5326f6da46ca4eae3b4043d2efa0a66d280627e4e652da5a62b860d6c6f2a72"
},
{
"id": "M37",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "f7d758bcd9752e6d7e6580ac935982ec9e9ff68b212c0be3d584a9b9757b9d5f"
},
{
"id": "M38",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "bf1796db389b3381dd5cec1fb9a64f493b1e36188462e531cad6fbba08775cd2"
},
{
"id": "M39",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "dbff45cd7878b4feae890a5b5d8bfdf9d95e151a9583e0d11a6802a4a7c83a45"
}
]
@@ -0,0 +1,12 @@
{
"968db0bcd..ddfb29af7": [
"docs/plans/DEFERRED.md",
"packages/cli/tests/case-sudo-fixture.mjs",
"packages/cli/tests/case1-accept.test.mjs",
"packages/cli/tests/case2-unit.test.mjs"
],
"c3d88840c..ddfb29af7": [
"docs/plans/DEFERRED.md"
],
"interpretation": "The branch-to-refactor comparison includes our unlanded test additions. From the actual branch base, only DEFERRED.md overlaps. Preserved upstream DEFERRED.md and appended the row 276 / #1781 entry. No rebase performed."
}
@@ -0,0 +1,142 @@
// Shared fake process model for the two Case host-script tests. No real sudo.
// Call case_sudo_run RECORD_DIR COMMAND... after the caller's argv checks.
// Missing session-mode preserves the original exec-style fake.
import { spawn } from 'node:child_process';
import { readFileSync, readdirSync } from 'node:fs';
export const CASE_SUDO_SESSION = String.raw`
case_sudo_entry() {
local record="$1" here sid; shift
here="$BASHPID"
read -r sid < <(/bin/ps -o sid= -p "$here")
printf "%s %s\n" "$here" "$sid" >> "$record/session-pids"
if { [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
{ [ -f "$record/stall-probe" ] && [[ "$*" == *SO_PEERCRED* ]]; }; then
trap '' TERM HUP
[ ! -f "$record/stall-output" ] || /bin/cat "$record/stall-output"
exec /bin/sleep 60
fi
exec "$@"
}
case_sudo_run() {
local record="$1" child="" rc=0 mode=exec; shift
[ ! -f "$record/session-mode" ] || mode="$(<"$record/session-mode")"
if [ "$mode" != hop ]; then case_sudo_entry "$record" "$@"; return "$?"; fi
# The fake stays as parent, just as sudo does on its pty path. KILL cannot
# be caught or forwarded. The child creates the separate terminal session.
trap '[ -z "$child" ] || kill -TERM -- "-$child" 2>/dev/null || :' TERM
trap '[ -z "$child" ] || kill -HUP -- "-$child" 2>/dev/null || :' HUP
trap '[ -z "$child" ] || kill -INT -- "-$child" 2>/dev/null || :' INT
export -f case_sudo_entry
/usr/bin/setsid /bin/bash -c 'case_sudo_entry "$@"' -- "$record" "$@" &
child=$!
# An interrupted wait returns while the child still runs; keep waiting.
while :; do
wait "$child"; rc=$?
kill -0 "$child" 2>/dev/null || break
done
return "$rc"
}
`;
export function liveRecordedSessions(recordFile, readFile, procDirs) {
// Exec fake entries share the test runner's session. Only self-led sessions
// are isolated and safe to scan here; exec coverage also checks exact PIDs.
const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number)).filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
const live=[];
for(const entry of procDirs()){
if(!/^\d+$/.test(entry))continue;
try{
const stat=readFile(`/proc/${entry}/stat`), fields=stat.slice(stat.lastIndexOf(') ')+2).split(' ');
if(sessions.has(Number(fields[3])) && fields[0]!=='Z') live.push(Number(entry));
}catch{}
}
return live;
}
export function recordedPids(file) {
try{return readFileSync(file,'utf8').trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number));}
catch(error){if(error.code==='ENOENT')return [];throw error;}
}
export function recordedLivePids(file) {
return sessionPairs(recordedPids(file)).filter(([pid,sid])=>{
try{
const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
return Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X';
}catch(error){if(error.code==='ENOENT'||error.code==='ESRCH')return false;throw error;}
}).map(([pid])=>pid);
}
export function killRecordedPids(file) {
killPairs(recordedPids(file));
}
function killPairs(pairs) {
for(const [pid,sid] of sessionPairs(pairs)){
try{
const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
if(Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X')process.kill(pid,'SIGKILL');
}catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
}
}
function sessionPairs(pairs) {
// Only self-led records authorize a session scan. Exec-mode records belong
// to the runner's session and authorize checking their exact PIDs only.
const sessions=new Set(pairs.filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
const found=new Map(pairs.map(pair=>[pair[0],pair]));
if(sessions.size)for(const entry of readdirSync('/proc')){
if(!/^\d+$/.test(entry))continue;
try{
const raw=readFileSync(`/proc/${entry}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' '),sid=Number(fields[3]);
if(sessions.has(sid))found.set(Number(entry),[Number(entry),sid]);
}catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
}
return [...found.values()];
}
// Safety net for a removed-outer-bound change. It is fixture cleanup, never
// product supervision. The guard reads only fixture records and checks sid
// immediately before every kill. Normal outer-bound cases return before it.
export function fixtureGuard(t,files,milliseconds=9500) {
const program=`const fs=require('node:fs');setTimeout(()=>{for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}},Number(process.argv[2]));`;
const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds)],{stdio:'ignore'});
const stop=async()=>{
if(child.exitCode!==null||child.signalCode!==null)return;
const exited=new Promise(resolve=>child.once('exit',resolve));
child.kill('SIGKILL');await exited;
};
t.after(stop);return stop;
}
export function cleanupFixture(t,files) {
let captured=[];
const capture=()=>{captured=files.flatMap(recordedPids);};
// tmp(t)'s directory-removal hook may run first. Retain PID/SID pairs in
// memory before assertions, then verify the kernel sid again before killing.
t.after(()=>killPairs([...captured,...files.flatMap(recordedPids)]));
return capture;
}
export async function pollFixtureGone(files) {
for(let n=0;n<50;n++){
if(files.every(file=>recordedLivePids(file).length===0))return [];
await new Promise(resolve=>setTimeout(resolve,10));
}
return files.flatMap(recordedLivePids);
}
export function blockingQuerySource(records,kind='signals',member,proof) {
const py=`import os,sys\nos.setpgid(0,0)\nwith open(sys.argv[2],"a") as f:f.write(str(os.getpid())+" "+str(os.getsid(0))+chr(10))\nos.execv(sys.argv[1],[sys.argv[1],"60"])`;
return `import fs from 'node:fs';import {spawn} from 'node:child_process';
const records=${JSON.stringify(records)};
function record(pid){const s=fs.readFileSync('/proc/'+pid+'/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);fs.appendFileSync(records,pid+' '+sid+String.fromCharCode(10));}
record(process.pid);
export function loadSystem(){
${kind==='sleep'?`const c=spawn('/usr/bin/sleep',['60'],{stdio:'inherit'});record(c.pid);`:''}
${kind==='signals'?`process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});`:''}
${kind==='member'?`const c=spawn('/usr/bin/python3',['-I','-S','-c',${JSON.stringify(py)},${JSON.stringify(member)},records],{stdio:'inherit'});for(let n=0;n<200;n++){try{const s=fs.readFileSync('/proc/'+c.pid+'/stat','utf8');if(s.includes('('+${JSON.stringify(member?.split('/').at(-1))}+') ')){fs.writeFileSync(${JSON.stringify(proof)},s);break;}}catch{}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,5);}`:''}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
}`;
}
@@ -0,0 +1,707 @@
#!/usr/bin/env bash
# Row 195 (#1693): the Case 1 acceptance on 10.1.1.53, design r7 section 5
# (agents/darkwing/work/queue-195/DESIGN.md). Deviations and decisions: agents/darkwing/work/queue-195/BUILD.md.
#
# case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N
#
# Root runs it on the Case 1 host after the install window, from a root-owned checkout. It reads the
# user, home, copy, node, claude, main port and tester socket marker from the args file, which must pass stack-upgrade.sh's
# rules and be a factory file for --business. Every command that runs the stack's code runs as
# `sudo -u USER -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin` from /. Root itself reads ss,
# /proc, the account database and file metadata, and runs these: systemctl show and is-enabled (F1,
# P11), curl on the Console's loopback healthz (F2), sudo -l (P1, P3), sudo -u jarvis bwrap --version
# and hostcheck.sh --check (P9), and node to parse managed-settings.json (F9). F9 runs node only when
# --node is the pinned path and it and every directory above it are root's with no group or other
# write. The script changes nothing on the host but the message F3 sends and its reply, and it never
# opens claude-session.env. A bounded config/target query imports the copy only as the stack
# user, with sudo and an empty environment. Root parses its two facts in bash. P10 uses the
# fixed system Python, also as the stack user, for a read-only SO_PEERCRED observation.
# The config/probe supervisor is fixed root-validated system Bash plus setsid
# and sleep, after the uid drop: query inner/outer 5/7 s, probe 2/4 s. Root
# independently bounds its sudo wait. Legacy AS_USER talk and diagnostics below
# retain their old behavior and are NOT covered by this supervisor (F5 follow-up).
#
# One line per check, `<id> <path> <mark> <evidence>`, then `count <mark> N` for each mark and a
# result line. F checks are `pass` or `FAIL`. P checks are `closed`, `accepted <ruling>` or
# `unnamed`, which means no Q182, Q183 or Mos ruling names the path; each unnamed line goes to Mos.
#
# Exit 0 when every F check passes, 1 when one fails, 2 on usage or a refusal (a bad args file, or
# --root misused). A P check never changes the exit.
#
# Test seam, for packages/cli/tests/case1-accept.test.mjs only. Root takes none:
# --root DIR prefixes the args file, /etc, /proc, the stack's home, the host check and node, and
# reads the account database from DIR/etc/passwd and DIR/etc/group instead of getent
set -uo pipefail
usage() { echo "usage: case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N" >&2; exit 2; }
refuse() { echo "case1-accept: refusing: $*" >&2; exit 2; }
BUSINESS="" ARGS_FILE="" CLAUDE_PIN="" NODE_PIN="" ROOT=""
while [ $# -gt 0 ]; do
case "$1" in
--business) [ $# -ge 2 ] && [[ "$2" =~ ^[a-z0-9][a-z0-9-]*$ ]] || usage; BUSINESS="$2"; shift 2 ;;
--args) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] || usage; ARGS_FILE="$2"; shift 2 ;;
--claude-pin) [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; CLAUDE_PIN="$2"; shift 2 ;;
--node-pin) [ $# -ge 2 ] && [[ "$2" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; NODE_PIN="$2"; shift 2 ;;
--root) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [ -d "$2" ] || usage; ROOT="${2%/}"; shift 2 ;;
*) usage ;;
esac
done
[ -n "$BUSINESS" ] && [ -n "$ARGS_FILE" ] && [ -n "$CLAUDE_PIN" ] && [ -n "$NODE_PIN" ] || usage
if [ "$(id -u)" = 0 ]; then
[ -z "$ROOT" ] || refuse "--root is a test seam, and root takes none"
PATH=/usr/sbin:/usr/bin:/sbin:/bin
else
[ -n "$ROOT" ] || refuse "run it as root"
fi
cd / || refuse "cannot cd to /"
# --- The args file, by stack-upgrade.sh's read_args rules, plus the serve ports ---
f="$ROOT$ARGS_FILE"
[ -f "$f" ] && [ ! -L "$f" ] || refuse "no regular file $ARGS_FILE"
st="$(stat -c '%u %a' -- "$f")" || refuse "cannot stat $ARGS_FILE"
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) ||
refuse "$ARGS_FILE must be $(id -un)'s with no group or other write (it is uid ${st% *}, mode ${st#* })"
mapfile -t ARGS < "$f" || refuse "cannot read $ARGS_FILE"
S_USER="" S_HOME="" A_BUSINESS="" NODE="" COPY_ID="" CLAUDE="" PROFILE="" PORT="" TESTER_SOCKET=0
copies=0 past=""
for ((i = 0; i < ${#ARGS[@]}; i++)); do
a="${ARGS[i]}" v="${ARGS[i + 1]:-}"
if [ -n "$past" ]; then
case "$a" in
--port) PORT="$v"; i=$((i + 1)) ;;
--business) [ "$v" = "$BUSINESS" ] || refuse "serve business differs"; i=$((i + 1)) ;;
--tester-socket) TESTER_SOCKET=$((TESTER_SOCKET + 1)) ;;
--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;
*) refuse "unknown serve flag $a" ;;
esac
continue
fi
case "$a" in
--) past=1 ;;
--pm) ;;
--user) S_USER="$v"; i=$((i + 1)) ;;
--home) S_HOME="$v"; i=$((i + 1)) ;;
--business) A_BUSINESS="$v"; i=$((i + 1)) ;;
--node) NODE="$v"; i=$((i + 1)) ;;
--claude) CLAUDE="$v"; i=$((i + 1)) ;;
--profile) PROFILE="$v"; i=$((i + 1)) ;;
--copy) COPY_ID="$v"; copies=$((copies + 1)); i=$((i + 1)) ;;
*) refuse "$ARGS_FILE has $a before --, which case2-unit.sh does not take" ;;
esac
done
[ "$copies" = 1 ] || refuse "$ARGS_FILE needs exactly one --copy before --, not $copies"
[[ "$S_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] && [[ "$S_HOME" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$A_BUSINESS" =~ ^[a-z0-9][a-z0-9-]*$ ]] &&
[[ "$NODE" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] && [[ "$CLAUDE" =~ ^/[A-Za-z0-9/._+@-]+$ ]] ||
refuse "$ARGS_FILE lacks a plain --user, --home, --business, --node, --claude or --copy"
[ "$PROFILE" = factory ] || refuse "$ARGS_FILE has --profile '${PROFILE}', not factory"
[ "$A_BUSINESS" = "$BUSINESS" ] || refuse "$ARGS_FILE is for business $A_BUSINESS, not $BUSINESS"
[[ "$PORT" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$PORT" -le 1023 ] || refuse "$ARGS_FILE lacks a privileged numeric --port after --"
[ "$TESTER_SOCKET" = 1 ] || refuse "$ARGS_FILE needs one bare --tester-socket after --"
COPY="$S_HOME/.local/share/mosaic-stack/copies/$COPY_ID"
DATA_ROOT="$S_HOME/.mosaic-dev"
CRED="$S_HOME/.config/mosaic-dev/secrets/claude-session.env"
BUSINESS_FILE="$S_HOME/.config/mosaic-dev/businesses/$BUSINESS.json"
BUS_UNIT="mosaic-bus@$BUSINESS.service" CON_UNIT=mosaic-webui.service
HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh
SETTINGS=/etc/claude-code/managed-settings.json
LABEL_OUTER='bwrap (enforce)' LABEL_CHILD='bwrap//&unpriv_bwrap (enforce)'
AS_USER=(sudo -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin")
WORK="$(mktemp -d)" || refuse "cannot make a temp dir"
trap 'rm -rf -- "$WORK"' EXIT
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
# row253-supervisor-begin
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
stat_fields() {
local pid="$1" raw rest
if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
[ -e "/proc/$pid/stat" ] && return 1
return 3
fi
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
[[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
}
clock_cs() {
local boot spare
read -r boot spare < /proc/uptime || return 1
[[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
}
clean_session() {
local entry pid rc live until
clock_cs || return 1
until=$((CLOCK_CS + 25))
while :; do
live=0
for entry in /proc/[0-9]*/stat; do
pid="${entry%/stat}"; pid="${pid##*/}"
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" != 3 ] || continue
[ "$rc" = 0 ] || return 1
[ "$ST_SESSION" = "$child" ] || continue
case "$ST_STATE" in Z|X) continue ;; esac
live=1
if ! kill -KILL "$pid" 2>/dev/null; then
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
fi
done
[ "$live" != 0 ] || return 0
clock_cs || return 1
[ "$CLOCK_CS" -lt "$until" ] || return 1
"$sleep_bin" 0.01
done
}
finish() {
local rc="$?"
trap - EXIT HUP INT TERM
if [ "$verified" = 1 ]; then
clean_session || rc=86
elif [ -n "$child" ]; then
kill -KILL "$child" 2>/dev/null || :
fi
[ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
"$setsid_bin" "$@" & child=$!
for ((attempt=0; attempt<100; attempt++)); do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 85
if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
tester_tools() {
local input target p st magic
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
p="$target"
while :; do
st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
[ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
[ "$p" != / ] || break
p="${p%/*}"; [ -n "$p" ] || p=/
done
magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
case "$input" in
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
/usr/bin/head) TS_HEAD="$target" ;;
esac
done
}
tester_capture_reason() {
case "$1" in
81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
*) echo inner-command ;;
esac
}
tester_capture() (
local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
tester_tools || exit 84
file="$(mktemp)" || exit 88
notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
trap 'exit 88' HUP INT TERM
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
set -o pipefail
maximum="$1"; head_bin="$2"; shift 2
"$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
# Only GNU timeout and the trusted head can write this root-side channel.
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
[ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
[ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
[ "${5:-}" != observe ] || return 0
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# --- Output ---
MARKS=(pass FAIL closed "accepted Q182+Q183" "accepted Q183" "accepted mos-1096" unnamed)
declare -A COUNT=()
F_FAILED=0
# Text the stack's user wrote, made safe to print: printable ASCII, one line, at most 200 bytes.
clean() { tr -c '[:print:]' ' ' <<< "$1" | cut -c1-200 | sed 's/ *$//'; }
line() { local id="$1" path="$2" mark="$3"; shift 3; echo "$id $path $mark $*"; COUNT[$mark]=$(( ${COUNT[$mark]:-0} + 1 )); }
fpass() { line "$1" "$2" pass "${@:3}"; }
ffail() { line "$1" "$2" FAIL "${@:3}"; F_FAILED=1; }
# --- The account database: getent on the host, the seam's files under --root ---
db() {
if [ -n "$ROOT" ]; then
if [ $# -gt 1 ]; then awk -F: -v k="$2" '$1 == k' "$ROOT/etc/$1"; else cat "$ROOT/etc/$1"; fi
else
getent "$@"
fi
}
uid_of() { db passwd "$1" | cut -d: -f3; }
gid_of() { db passwd "$1" | cut -d: -f4; }
# The group ids USER belongs to, as `id -G USER` prints them: the primary gid, then each group
# whose member list names USER.
groups_of() {
local g; g="$(gid_of "$1")"
[ -n "$g" ] || return 0
{ echo "$g"; db group | awk -F: -v u="$1" '{ n = split($4, m, ","); for (i = 1; i <= n; i++) if (m[i] == u) print $3 }'; } | awk '!seen[$0]++'
}
names_of_groups() { local g; for g in "$@"; do db group | awk -F: -v g="$g" '$3 == g { print $1 }'; done; }
MOSAIC_UID="$(uid_of "$S_USER")"
[[ "$MOSAIC_UID" =~ ^[1-9][0-9]*$ ]] || refuse "stack account has no non-root uid"
[ "$(db passwd "$S_USER" | cut -d: -f6)" = "$S_HOME" ] || refuse "saved home differs from account home"
QUERY="$(tester_capture 5 7 4096 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query $(tester_capture_reason "$?")"
tester_facts "$QUERY" || refuse "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || refuse "target does not support --tester-socket"
tester_location "$TS_DATA" "$S_HOME" "$MOSAIC_UID" "$ROOT" observe || refuse "tester path refuses $TS_REASON"
TESTER_PATH="$TS_SOCKET"
# --- systemd, /proc and listeners ---
# KEY=VALUE lines for UNIT's properties.
unit_show() { local u="$1"; shift; systemctl show "$u" "${@/#/-p}" 2>/dev/null; }
prop() { sed -n "s/^$2=//p" <<< "$1" | tail -n 1; }
# The real, effective, saved and filesystem uids of PID, space-separated.
proc_uids() { awk '$1 == "Uid:" { print $2, $3, $4, $5 }' "$ROOT/proc/$1/status" 2>/dev/null; }
proc_label() { local l; l="$(cat "$ROOT/proc/$1/attr/current" 2>/dev/null)" || return 1; printf '%s' "${l%$'\n'}"; }
# One `ADDRESS PIDS` line per TCP listener on port $1, PIDS comma-separated.
listeners() {
ss -Hltnp 2>/dev/null | awk -v p="$1" '{
n = split($4, a, ":"); if (a[n] != p) next
addr = substr($4, 1, length($4) - length(p) - 1); pids = ""; s = $0
while (match(s, /pid=[0-9]+/)) { pids = pids (pids == "" ? "" : ",") substr(s, RSTART + 4, RLENGTH - 4); s = substr(s, RSTART + RLENGTH) }
print addr, pids
}'
}
# 0 when port $1 has a listener and every one is on 127.0.0.1, held by pid $2 alone.
loopback_by() {
local addr pids p any=0
[ -n "$2" ] && [ "$2" != 0 ] || return 1
while read -r addr pids; do
[ -n "$addr" ] || continue
any=1
[ "$addr" = 127.0.0.1 ] || return 1
for p in ${pids//,/ }; do [ "$p" = "$2" ] || return 1; done
[ -n "$pids" ] || return 1
done <<< "$(listeners "$1")"
[ "$any" = 1 ]
}
listener_text() { local t; t="$(listeners "$1" | tr '\n' ';')"; echo "${t:-none}"; }
BUS_SHOW="$(unit_show "$BUS_UNIT" ActiveState User MainPID)"
CON_SHOW="$(unit_show "$CON_UNIT" ActiveState User MainPID)"
CON_PID="$(prop "$CON_SHOW" MainPID)"
# --- F checks ---
check_f1() {
local u show state user pid uids why=""
for u in "$BUS_UNIT" "$CON_UNIT"; do
if [ "$u" = "$BUS_UNIT" ]; then show="$BUS_SHOW"; else show="$CON_SHOW"; fi
state="$(prop "$show" ActiveState)" user="$(prop "$show" User)" pid="$(prop "$show" MainPID)"
uids="$(proc_uids "${pid:-0}")"
[ "$state" = active ] || { why+="$u is ${state:-unknown}; "; continue; }
[ "$user" = "$S_USER" ] || { why+="$u has User=${user:-unset}; "; continue; }
[ -n "$MOSAIC_UID" ] && [ "$uids" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] ||
why+="$u MainPID ${pid:-none} has uids '${uids:-unreadable}', not $S_USER's ${MOSAIC_UID:-unknown}; "
done
if [ -z "$why" ]; then fpass F1 units-user "$BUS_UNIT and $CON_UNIT are active as $S_USER (uid $MOSAIC_UID)"
else ffail F1 units-user "${why%; }"; fi
}
check_f2() {
local rc=0
curl -fsS --max-time 10 -o /dev/null "http://127.0.0.1:$PORT/healthz" 2>/dev/null || rc=$?
if [ "$rc" != 0 ]; then ffail F2 console-loopback "curl http://127.0.0.1:$PORT/healthz exited $rc"
elif ! loopback_by "$PORT" "$CON_PID"; then ffail F2 console-loopback "port $PORT listeners are '$(listener_text "$PORT")', not 127.0.0.1 by the Console's MainPID ${CON_PID:-none}"
else fpass F2 console-loopback "healthz answers, and $PORT is bound to 127.0.0.1 only, by MainPID $CON_PID"; fi
}
# F5's scan: an outer /usr/bin/bwrap owned by the stack's user with a claude descendant. Sets F5_OK
# when one pair has both labels exactly, F5_WHY to the last mismatch otherwise.
F5_OK="" F5_WHY="no /usr/bin/bwrap process of $S_USER with a claude descendant was seen while the reply was pending"
scan_f5() {
local pid ppid uids kids q c argv0 comm lo lc
local -A parent=() children=() bw=()
# One read of every PPid line and one of every exe link, so a scan stays short on a busy host.
while read -r pid ppid; do
parent[$pid]="$ppid" children[$ppid]+=" $pid"
done < <(grep -H '^PPid:' "$ROOT"/proc/[0-9]*/status 2>/dev/null | sed -n 's#^.*/proc/\([0-9]*\)/status:PPid:[[:space:]]*\([0-9]*\)$#\1 \2#p')
while read -r pid; do
[[ "$pid" =~ ^[0-9]+$ ]] && [ -n "${parent[$pid]:-}" ] && bw[$pid]=1
done < <(find "$ROOT/proc" -mindepth 2 -maxdepth 2 -name exe -lname /usr/bin/bwrap -printf '%h\n' 2>/dev/null | sed 's#.*/##')
for pid in "${!bw[@]}"; do
[ -z "${bw[${parent[$pid]}]:-}" ] || continue
uids="$(proc_uids "$pid")"
[ -n "$MOSAIC_UID" ] && [ "${uids%% *}" = "$MOSAIC_UID" ] || continue
# Every descendant, breadth first.
kids=() q=("$pid")
while [ ${#q[@]} -gt 0 ]; do
for c in ${children[${q[0]}]:-}; do kids+=("$c") q+=("$c"); done
q=("${q[@]:1}")
done
for c in "${kids[@]}"; do
argv0="$(tr '\0' '\n' < "$ROOT/proc/$c/cmdline" 2>/dev/null | sed -n 1p)"
comm="$(cat "$ROOT/proc/$c/comm" 2>/dev/null)"
[[ "${argv0##*/}" == claude* ]] || [[ "$comm" == claude* ]] || continue
lo="$(proc_label "$pid")" lc="$(proc_label "$c")"
if [ "$lo" = "$LABEL_OUTER" ] && [ "$lc" = "$LABEL_CHILD" ]; then
F5_OK="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc'"; return 0
fi
F5_WHY="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc', not '$LABEL_OUTER' and '$LABEL_CHILD'"
done
done
return 1
}
# F3 and F5 together: the message goes out in the background and the scan runs until it is answered.
check_f3_f5() {
local marker text rc=0 out err n tpid
marker="case1-accept-$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"
text="$marker: this is the Case 1 acceptance; reply with one short line."
timeout 330 "${AS_USER[@]}" "$COPY/scripts/mosaic" talk chief "$text" --business "$BUSINESS" --tester --wait 300 > "$WORK/talk.out" 2> "$WORK/talk.err" &
tpid=$!
while :; do
scan_f5 && break
kill -0 "$tpid" 2>/dev/null || break
sleep 0.5
done
wait "$tpid" || rc=$?
# Both the sent line and the reply are on stdout; an error is on stderr.
out="$(cat "$WORK/talk.out")" err="$(cat "$WORK/talk.err")"
n="$(grep -c . <<< "$out")"
if [ "$rc" = 0 ] && grep -q "^sent [^ ]* to chief" <<< "$out" && [ "$n" -ge 2 ]; then
fpass F3 tester-reply "chief replied to $marker"
else
ffail F3 tester-reply "talk exited $rc with $n lines on stdout; last: $(clean "$(tail -n 1 <<< "${err:-$out}")")"
fi
if [ -n "$F5_OK" ]; then F5_LINE=(fpass "$F5_OK"); else F5_LINE=(ffail "$F5_WHY"); fi
}
check_f4() {
local rc=0 out
out="$("${AS_USER[@]}" "$COPY/scripts/case2-precheck.sh" --business "$BUSINESS" --profile factory 2>&1)" || rc=$?
if [ "$rc" = 0 ]; then fpass F4 precheck "case2-precheck.sh --profile factory exits 0"
else ffail F4 precheck "case2-precheck.sh --profile factory exited $rc; last: $(clean "$(tail -n 1 <<< "$out")")"; fi
}
check_f6() {
local want="/opt/mosaic-stack/claude-$CLAUDE_PIN/" v
[[ "$CLAUDE" == "$want"* ]] || { ffail F6 claude-pin "--claude $CLAUDE is not under $want"; return; }
[[ "$CLAUDE/" != */../* ]] || { ffail F6 claude-pin "--claude $CLAUDE has a .. segment"; return; }
v="$("${AS_USER[@]}" "$CLAUDE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$CLAUDE_PIN (Claude Code)" ]; then fpass F6 claude-pin "$CLAUDE prints '$v'"
else ffail F6 claude-pin "$CLAUDE --version prints '$(clean "$v")', not '$CLAUDE_PIN (Claude Code)'"; fi
}
# The PM check confirm runs (stack-release.sh pm_external): exactly one live launches list entry for
# launch.by, for this business. The stack's user runs the whole check and prints one result line.
F7_JS='
const {readFileSync}=require("node:fs");const {spawnSync}=require("node:child_process");
const [file,cli,b]=process.argv.slice(1);
let by;
try{by=JSON.parse(readFileSync(file,"utf8")).launch?.by}catch(e){console.log(`fail cannot read ${file}: ${e.code??"not JSON"}`);process.exit(0)}
if(typeof by!=="string"||!by){console.log("fail the business file has no launch.by");process.exit(0)}
const r=spawnSync(cli,["launches","list","--json"],{encoding:"utf8"});
if(r.status!==0){console.log(`fail launches list exited ${r.status}`);process.exit(0)}
let l;try{l=JSON.parse(r.stdout)}catch{console.log("fail launches list printed no JSON");process.exit(0)}
const live=Array.isArray(l)?l.filter((x)=>x&&x.instance===by&&x.live===true):[];
console.log(live.length!==1?`fail ${live.length} live launches of ${by}`:live[0].business!==b?`fail the live launch of ${by} is for ${live[0].business}`:`ok ${by} run ${live[0].run}`);'
check_f7() {
local out
out="$("${AS_USER[@]}" "$NODE" -e "$F7_JS" -- "$BUSINESS_FILE" "$COPY/scripts/mosaic" "$BUSINESS" 2>/dev/null | sed -n 1p)"
if [[ "$out" =~ ^ok\ [a-z0-9][a-z0-9-]*\ run\ [A-Za-z0-9._-]+$ ]]; then fpass F7 pm-live "one live launch: ${out#ok }"
else ffail F7 pm-live "$(clean "${out#fail }")"; fi
}
check_f8() {
local s
s="$(stat -c '%U %a %F' -- "$ROOT$CRED" 2>/dev/null)" || s="missing"
if [ ! -L "$ROOT$CRED" ] && [ "$s" = "$S_USER 600 regular file" ]; then fpass F8 credential-file "$CRED is '$s'"
else ffail F8 credential-file "$CRED is '$s', not '$S_USER 600 regular file'"; fi
}
# Empty when $1 and every directory above it, up to /, are the running user's (root's on the host) with
# no group or other write: case2-unit.sh's root_only rule for --node, checked again before root runs it.
# Otherwise the first path that breaks the rule. A symlink breaks it, because its mode is 777.
not_root_only() {
local p="$1" st
while :; do
st="$(stat -c '%u %a' -- "$ROOT$p" 2>/dev/null)" || { echo "$p (cannot stat)"; return; }
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) || { echo "$p (uid ${st% *}, mode ${st#* })"; return; }
[ "$p" = / ] && return
p="$(dirname -- "$p")"
done
}
check_f9() {
local s v bad
s="$(stat -c '%U:%G %a %F' -- "$ROOT$SETTINGS" 2>/dev/null)" || s="missing"
[ ! -L "$ROOT$SETTINGS" ] && [ "$s" = "root:root 644 regular file" ] || { ffail F9 autoupdater-off "$SETTINGS is '$s', not 'root:root 644 regular file'"; return; }
# Root runs node here, so it runs only the pinned path, and only when nothing but root can replace it.
[ "$NODE" = "/opt/mosaic-stack/node-$NODE_PIN/bin/node" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: it is not the --node-pin path"; return; }
bad="$(not_root_only "$NODE")"
[ -z "$bad" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: $bad is not root's alone"; return; }
v="$("$ROOT$NODE" -e 'const s=JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(JSON.stringify(s?.env?.DISABLE_AUTOUPDATER??null))' -- "$ROOT$SETTINGS" 2>/dev/null)" || v="unreadable"
if [ "$v" = '"1"' ]; then fpass F9 autoupdater-off "$SETTINGS is root:root 0644 with env.DISABLE_AUTOUPDATER \"1\""
else ffail F9 autoupdater-off "$SETTINGS env.DISABLE_AUTOUPDATER is $v, not \"1\""; fi
}
check_f10() {
local want="/opt/mosaic-stack/node-$NODE_PIN/bin/node" v
[ "$NODE" = "$want" ] || { ffail F10 node-pin "--node $NODE is not $want"; return; }
v="$("${AS_USER[@]}" "$NODE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$NODE_PIN" ]; then fpass F10 node-pin "$NODE prints $v"
else ffail F10 node-pin "$NODE --version prints '$(clean "$v")', not $NODE_PIN"; fi
}
check_f11() {
local d="$ROOT$DATA_ROOT/discord" found
if [ -L "$d" ]; then ffail F11 no-discord "$DATA_ROOT/discord is a symlink"; return; fi
[ -d "$d" ] || { fpass F11 no-discord "$DATA_ROOT/discord is absent"; return; }
found="$(find "$d" -maxdepth 1 -name '*.json' -printf '%f ' 2>/dev/null)"
if [ -z "$found" ]; then fpass F11 no-discord "$DATA_ROOT/discord holds no binding"
else ffail F11 no-discord "$DATA_ROOT/discord holds bindings: ${found% }"; fi
}
# --- P checks ---
check_p1() {
local out
out="$(sudo -l -U "$S_USER" 2>&1)"
if grep -q "is not allowed to run sudo" <<< "$out"; then line P1 "$S_USER-sudo" closed "sudo -l: $(clean "$(sed -n 1p <<< "$out")")"
else line P1 "$S_USER-sudo" unnamed "sudo -l: $(clean "$(tr '\n' ' ' <<< "$out")")"; fi
}
check_p2() {
local gs keys="$ROOT$S_HOME/.ssh/authorized_keys" kstate n
gs="$(groups_of "$S_USER" | tr '\n' ' ')"; gs="${gs% }"
n="$(wc -w <<< "$gs")"
if [ ! -e "$keys" ] && [ ! -L "$keys" ]; then kstate=absent
elif [ -f "$keys" ] && [ ! -L "$keys" ] && [ ! -s "$keys" ]; then kstate=empty
else kstate=present; fi
if [ "$n" = 1 ] && [ "$kstate" != present ]; then line P2 "$S_USER-groups-keys" closed "id -G is '$gs'; authorized_keys is $kstate"
else line P2 "$S_USER-groups-keys" unnamed "id -G is '$gs'; authorized_keys is $kstate"; fi
}
check_p3() {
local n
n="$(sudo -l -U localadmin 2>&1 | grep -c NOPASSWD)"
line P3 localadmin-sudo "accepted Q183" "sudo -l -U localadmin lists $n NOPASSWD lines"
}
check_p4() { line P4 pve-root-pbs "accepted Q182+Q183" "root on pve over VM 1008, and PBS backups that hold $S_HOME with the credential; this script can't see pve"; }
check_p5() {
local gs reach
if [ -z "$(uid_of jarvis)" ]; then line P5 jarvis-reach closed "no jarvis account"; return; fi
gs="$(names_of_groups $(groups_of jarvis) | tr '\n' ' ')"
reach="$(tr ' ' '\n' <<< "$gs" | grep -xE 'sudo|docker|lxd' | tr '\n' ' ')"
if [ -z "$reach" ]; then line P5 jarvis-reach closed "jarvis is in none of sudo, docker and lxd (groups: ${gs% })"
else line P5 jarvis-reach unnamed "jarvis is in ${reach% }"; fi
}
check_p6() {
local g m members bad=0
for g in sudo docker lxd; do
members="$(db group "$g" | cut -d: -f4)"
for m in ${members//,/ }; do
[ "$g" = sudo ] && [ "$m" = localadmin ] && continue
line P6 "group-$g" unnamed "$m is a member of $g"; bad=1
done
done
[ "$bad" = 1 ] || line P6 group-members closed "sudo holds only localadmin, and docker and lxd have no members"
}
check_p7() {
local found
found="$(grep -rlw -- "$S_USER" "$ROOT/etc/sudoers.d" 2>/dev/null | sed "s#^$ROOT##" | tr '\n' ' ')"
if [ -z "$found" ]; then line P7 sudoers-d closed "no file under /etc/sudoers.d names $S_USER"
else line P7 sudoers-d unnamed "files naming $S_USER: ${found% }"; fi
}
check_p8() {
local other
other="$(listeners 7331)$(listeners 7333)"
if loopback_by "$PORT" "$CON_PID" && [ -z "$other" ]; then
line P8 lan-listeners closed "$PORT is on 127.0.0.1 only by MainPID $CON_PID; no TCP listener on 7331 or 7333; tester uses $TESTER_PATH"
else
line P8 lan-listeners unnamed "$PORT: $(listener_text "$PORT"); 7333: $(listener_text 7333); 7331: $(listener_text 7331); Console MainPID ${CON_PID:-none}"
fi
}
check_p9() {
local out n missing=() rc=0 err mg others
out="$("$ROOT$HOSTCHECK" --check 2>/dev/null)"
for n in 1 2 3 4 5 6; do grep -q "^ok $n " <<< "$out" || missing+=("hostcheck item $n has no ok line")
done
err="$(sudo -u jarvis /usr/bin/bwrap --version 2>&1 >/dev/null)" || rc=$?
[ "$rc" != 0 ] && grep -q "Permission denied" <<< "$err" || missing+=("sudo -u jarvis /usr/bin/bwrap --version exited $rc without Permission denied")
mg="$(gid_of "$S_USER")"
[ -n "$mg" ] && [ -z "$(db group "$S_USER" | cut -d: -f4)" ] || missing+=("group $S_USER lists members, or $S_USER has no group")
others="$(db passwd | awk -F: -v g="$mg" -v u="$S_USER" '$4 == g && $1 != u { print $1 }' | tr '\n' ' ')"
[ -z "$others" ] || missing+=("other accounts have $S_USER's gid $mg: ${others% }")
if [ ${#missing[@]} = 0 ]; then
line P9 bwrap-userns "accepted mos-1096" "hostcheck items 1-6 ok, jarvis is denied bwrap, and group $S_USER has no other member"
else
for n in "${missing[@]}"; do line P9 bwrap-userns unnamed "$n"; done
fi
}
# P10 is an observation of mutable same-uid state, never a closure ruling.
TESTER_PEER='import socket,struct,sys
s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
try:
s.settimeout(1)
s.connect(sys.argv[1])
p=struct.unpack("3i",s.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
print(*p)
finally:
s.close()'
tester_snapshot() {
local parent="${TESTER_PATH%/*}" p s pid
[ ! -L "$ROOT$parent" ] && [ ! -L "$ROOT$TESTER_PATH" ] || return 1
p="$(stat -c '%u %a %F %d %i' -- "$ROOT$parent" 2>/dev/null)" || return 1
s="$(stat -c '%u %a %F %d %i' -- "$ROOT$TESTER_PATH" 2>/dev/null)" || return 1
[[ "$p" =~ ^$MOSAIC_UID\ 700\ directory\ [0-9]+\ [0-9]+$ ]] || return 1
[[ "$s" =~ ^$MOSAIC_UID\ 600\ socket\ [0-9]+\ [0-9]+$ ]] || return 1
pid="$(prop "$(unit_show "$CON_UNIT" MainPID)" MainPID)"
[[ "$pid" =~ ^[1-9][0-9]*$ ]] && [ "$pid" = "$CON_PID" ] || return 1
[ "$(proc_uids "$pid")" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] || return 1
printf '%s|%s|%s' "$pid" "$p" "$s"
}
tester_python() {
local real bad magic
# Resolve the system entry, then validate the target and every ancestor.
[ -f "$ROOT/usr/bin/python3" ] && [ -x "$ROOT/usr/bin/python3" ] || return 1
real="$(realpath -e -- "$ROOT/usr/bin/python3" 2>/dev/null)" || return 1
[ -z "$ROOT" ] || real="${real#"$ROOT"}"
case "$real" in /home/*|/root/*|/run/user/*|"$S_HOME"/*) return 1 ;; esac
bad="$(not_root_only "$real")"; [ -z "$bad" ] || return 1
bad="$(not_root_only /usr/bin)"; [ -z "$bad" ] || return 1
magic="$(od -An -tx1 -N4 -- "$ROOT$real" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
printf '%s' "$real"
}
check_p10() {
local floor before after python peer pid uid gid why="" pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})$'
floor="$(cat "$ROOT/proc/sys/net/ipv4/ip_unprivileged_port_start" 2>/dev/null)"
[[ "$floor" =~ ^[0-9]{1,5}$ ]] && [ "$floor" -gt "$PORT" ] || why=floor
before="$(tester_snapshot)" || why=metadata
python="$(tester_python)" || why=interpreter
if [ -z "$why" ]; then
peer="$(tester_capture 2 4 128 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why="probe-$(tester_capture_reason "$?")"
if [[ "$peer" =~ $pattern ]]; then
pid="${BASH_REMATCH[1]}" uid="${BASH_REMATCH[2]}" gid="${BASH_REMATCH[3]}"
[ "$pid" = "$CON_PID" ] && [ "$uid" = "$MOSAIC_UID" ] && [ "$gid" -le 2147483647 ] || why=peer
else [ -n "$why" ] || why=probe; fi
after="$(tester_snapshot)" || why=metadata
[ "$before" = "$after" ] || why=changed
fi
if [ -z "$why" ]; then
line P10 tester-socket-squat unnamed "boundary=observed path=$TESTER_PATH parent=0700 owner=$MOSAIC_UID socket=0600 peer_pid=$pid peer_uid=$uid floor=$floor port=$PORT; hostile same-uid substitution remains"
else
line P10 tester-socket-squat unnamed "boundary=failed reason=$why path=$TESTER_PATH MainPID=${CON_PID:-none}; hostile same-uid substitution remains"
fi
}
check_p11() {
local en before
en="$(systemctl is-enabled mosaic-hostcheck.service 2>/dev/null)"
before="$(prop "$(unit_show mosaic-hostcheck.service Before)" Before)"
if [ "$en" = enabled ] && grep -qxF -- "$BUS_UNIT" <<< "${before// /$'\n'}"; then
line P11 hostcheck-before-bus closed "mosaic-hostcheck.service is enabled, and its Before= names $BUS_UNIT"
else
line P11 hostcheck-before-bus unnamed "mosaic-hostcheck.service is '${en:-unknown}', Before='$before'; it needs enabled and $BUS_UNIT"
fi
}
check_f1
check_f2
F5_LINE=()
check_f3_f5
check_f4
"${F5_LINE[0]}" F5 bwrap-label "${F5_LINE[@]:1}"
check_f6
check_f7
check_f8
check_f9
check_f10
check_f11
check_p1; check_p2; check_p3; check_p4; check_p5; check_p6
check_p7; check_p8; check_p9; check_p10; check_p11
for m in "${MARKS[@]}"; do echo "count $m ${COUNT[$m]:-0}"; done
if [ "$F_FAILED" = 0 ]; then echo "case1-accept: every F check passes; send the whole output to Duckworth, and each unnamed line to Mos"; exit 0; fi
echo "case1-accept: an F check failed, so the row fails"
exit 1
@@ -0,0 +1,580 @@
#!/usr/bin/env bash
# Row 195 (#1693): the Case 1 acceptance on 10.1.1.53, design r7 section 5
# (agents/darkwing/work/queue-195/DESIGN.md). Deviations and decisions: agents/darkwing/work/queue-195/BUILD.md.
#
# case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N
#
# Root runs it on the Case 1 host after the install window, from a root-owned checkout. It reads the
# user, home, copy, node, claude, main port and tester socket marker from the args file, which must pass stack-upgrade.sh's
# rules and be a factory file for --business. Every command that runs the stack's code runs as
# `sudo -u USER -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin` from /. Root itself reads ss,
# /proc, the account database and file metadata, and runs these: systemctl show and is-enabled (F1,
# P11), curl on the Console's loopback healthz (F2), sudo -l (P1, P3), sudo -u jarvis bwrap --version
# and hostcheck.sh --check (P9), and node to parse managed-settings.json (F9). F9 runs node only when
# --node is the pinned path and it and every directory above it are root's with no group or other
# write. The script changes nothing on the host but the message F3 sends and its reply, and it never
# opens claude-session.env. A bounded config/target query imports the copy only as the stack
# user, with sudo and an empty environment. Root parses its two facts in bash. P10 uses the
# fixed system Python, also as the stack user, for a read-only SO_PEERCRED observation.
#
# One line per check, `<id> <path> <mark> <evidence>`, then `count <mark> N` for each mark and a
# result line. F checks are `pass` or `FAIL`. P checks are `closed`, `accepted <ruling>` or
# `unnamed`, which means no Q182, Q183 or Mos ruling names the path; each unnamed line goes to Mos.
#
# Exit 0 when every F check passes, 1 when one fails, 2 on usage or a refusal (a bad args file, or
# --root misused). A P check never changes the exit.
#
# Test seam, for packages/cli/tests/case1-accept.test.mjs only. Root takes none:
# --root DIR prefixes the args file, /etc, /proc, the stack's home, the host check and node, and
# reads the account database from DIR/etc/passwd and DIR/etc/group instead of getent
set -uo pipefail
usage() { echo "usage: case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N" >&2; exit 2; }
refuse() { echo "case1-accept: refusing: $*" >&2; exit 2; }
BUSINESS="" ARGS_FILE="" CLAUDE_PIN="" NODE_PIN="" ROOT=""
while [ $# -gt 0 ]; do
case "$1" in
--business) [ $# -ge 2 ] && [[ "$2" =~ ^[a-z0-9][a-z0-9-]*$ ]] || usage; BUSINESS="$2"; shift 2 ;;
--args) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] || usage; ARGS_FILE="$2"; shift 2 ;;
--claude-pin) [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; CLAUDE_PIN="$2"; shift 2 ;;
--node-pin) [ $# -ge 2 ] && [[ "$2" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; NODE_PIN="$2"; shift 2 ;;
--root) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [ -d "$2" ] || usage; ROOT="${2%/}"; shift 2 ;;
*) usage ;;
esac
done
[ -n "$BUSINESS" ] && [ -n "$ARGS_FILE" ] && [ -n "$CLAUDE_PIN" ] && [ -n "$NODE_PIN" ] || usage
if [ "$(id -u)" = 0 ]; then
[ -z "$ROOT" ] || refuse "--root is a test seam, and root takes none"
PATH=/usr/sbin:/usr/bin:/sbin:/bin
else
[ -n "$ROOT" ] || refuse "run it as root"
fi
cd / || refuse "cannot cd to /"
# --- The args file, by stack-upgrade.sh's read_args rules, plus the serve ports ---
f="$ROOT$ARGS_FILE"
[ -f "$f" ] && [ ! -L "$f" ] || refuse "no regular file $ARGS_FILE"
st="$(stat -c '%u %a' -- "$f")" || refuse "cannot stat $ARGS_FILE"
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) ||
refuse "$ARGS_FILE must be $(id -un)'s with no group or other write (it is uid ${st% *}, mode ${st#* })"
mapfile -t ARGS < "$f" || refuse "cannot read $ARGS_FILE"
S_USER="" S_HOME="" A_BUSINESS="" NODE="" COPY_ID="" CLAUDE="" PROFILE="" PORT="" TESTER_SOCKET=0
copies=0 past=""
for ((i = 0; i < ${#ARGS[@]}; i++)); do
a="${ARGS[i]}" v="${ARGS[i + 1]:-}"
if [ -n "$past" ]; then
case "$a" in
--port) PORT="$v"; i=$((i + 1)) ;;
--business) [ "$v" = "$BUSINESS" ] || refuse "serve business differs"; i=$((i + 1)) ;;
--tester-socket) TESTER_SOCKET=$((TESTER_SOCKET + 1)) ;;
--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;
*) refuse "unknown serve flag $a" ;;
esac
continue
fi
case "$a" in
--) past=1 ;;
--pm) ;;
--user) S_USER="$v"; i=$((i + 1)) ;;
--home) S_HOME="$v"; i=$((i + 1)) ;;
--business) A_BUSINESS="$v"; i=$((i + 1)) ;;
--node) NODE="$v"; i=$((i + 1)) ;;
--claude) CLAUDE="$v"; i=$((i + 1)) ;;
--profile) PROFILE="$v"; i=$((i + 1)) ;;
--copy) COPY_ID="$v"; copies=$((copies + 1)); i=$((i + 1)) ;;
*) refuse "$ARGS_FILE has $a before --, which case2-unit.sh does not take" ;;
esac
done
[ "$copies" = 1 ] || refuse "$ARGS_FILE needs exactly one --copy before --, not $copies"
[[ "$S_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] && [[ "$S_HOME" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$A_BUSINESS" =~ ^[a-z0-9][a-z0-9-]*$ ]] &&
[[ "$NODE" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] && [[ "$CLAUDE" =~ ^/[A-Za-z0-9/._+@-]+$ ]] ||
refuse "$ARGS_FILE lacks a plain --user, --home, --business, --node, --claude or --copy"
[ "$PROFILE" = factory ] || refuse "$ARGS_FILE has --profile '${PROFILE}', not factory"
[ "$A_BUSINESS" = "$BUSINESS" ] || refuse "$ARGS_FILE is for business $A_BUSINESS, not $BUSINESS"
[[ "$PORT" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$PORT" -le 1023 ] || refuse "$ARGS_FILE lacks a privileged numeric --port after --"
[ "$TESTER_SOCKET" = 1 ] || refuse "$ARGS_FILE needs one bare --tester-socket after --"
COPY="$S_HOME/.local/share/mosaic-stack/copies/$COPY_ID"
DATA_ROOT="$S_HOME/.mosaic-dev"
CRED="$S_HOME/.config/mosaic-dev/secrets/claude-session.env"
BUSINESS_FILE="$S_HOME/.config/mosaic-dev/businesses/$BUSINESS.json"
BUS_UNIT="mosaic-bus@$BUSINESS.service" CON_UNIT=mosaic-webui.service
HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh
SETTINGS=/etc/claude-code/managed-settings.json
LABEL_OUTER='bwrap (enforce)' LABEL_CHILD='bwrap//&unpriv_bwrap (enforce)'
AS_USER=(sudo -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin")
WORK="$(mktemp -d)" || refuse "cannot make a temp dir"
trap 'rm -rf -- "$WORK"' EXIT
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
tester_capture() (
local seconds="$1" maximum="$2" file child="" rc=0; shift 2
file="$(mktemp)" || exit 1
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
trap 'exit 1' HUP INT TERM
timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
[ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
[ "${5:-}" != observe ] || return 0
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# --- Output ---
MARKS=(pass FAIL closed "accepted Q182+Q183" "accepted Q183" "accepted mos-1096" unnamed)
declare -A COUNT=()
F_FAILED=0
# Text the stack's user wrote, made safe to print: printable ASCII, one line, at most 200 bytes.
clean() { tr -c '[:print:]' ' ' <<< "$1" | cut -c1-200 | sed 's/ *$//'; }
line() { local id="$1" path="$2" mark="$3"; shift 3; echo "$id $path $mark $*"; COUNT[$mark]=$(( ${COUNT[$mark]:-0} + 1 )); }
fpass() { line "$1" "$2" pass "${@:3}"; }
ffail() { line "$1" "$2" FAIL "${@:3}"; F_FAILED=1; }
# --- The account database: getent on the host, the seam's files under --root ---
db() {
if [ -n "$ROOT" ]; then
if [ $# -gt 1 ]; then awk -F: -v k="$2" '$1 == k' "$ROOT/etc/$1"; else cat "$ROOT/etc/$1"; fi
else
getent "$@"
fi
}
uid_of() { db passwd "$1" | cut -d: -f3; }
gid_of() { db passwd "$1" | cut -d: -f4; }
# The group ids USER belongs to, as `id -G USER` prints them: the primary gid, then each group
# whose member list names USER.
groups_of() {
local g; g="$(gid_of "$1")"
[ -n "$g" ] || return 0
{ echo "$g"; db group | awk -F: -v u="$1" '{ n = split($4, m, ","); for (i = 1; i <= n; i++) if (m[i] == u) print $3 }'; } | awk '!seen[$0]++'
}
names_of_groups() { local g; for g in "$@"; do db group | awk -F: -v g="$g" '$3 == g { print $1 }'; done; }
MOSAIC_UID="$(uid_of "$S_USER")"
[[ "$MOSAIC_UID" =~ ^[1-9][0-9]*$ ]] || refuse "stack account has no non-root uid"
[ "$(db passwd "$S_USER" | cut -d: -f6)" = "$S_HOME" ] || refuse "saved home differs from account home"
QUERY="$(tester_capture 5 4096 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query failed"
tester_facts "$QUERY" || refuse "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || refuse "target does not support --tester-socket"
tester_location "$TS_DATA" "$S_HOME" "$MOSAIC_UID" "$ROOT" observe || refuse "tester path refuses $TS_REASON"
TESTER_PATH="$TS_SOCKET"
# --- systemd, /proc and listeners ---
# KEY=VALUE lines for UNIT's properties.
unit_show() { local u="$1"; shift; systemctl show "$u" "${@/#/-p}" 2>/dev/null; }
prop() { sed -n "s/^$2=//p" <<< "$1" | tail -n 1; }
# The real, effective, saved and filesystem uids of PID, space-separated.
proc_uids() { awk '$1 == "Uid:" { print $2, $3, $4, $5 }' "$ROOT/proc/$1/status" 2>/dev/null; }
proc_label() { local l; l="$(cat "$ROOT/proc/$1/attr/current" 2>/dev/null)" || return 1; printf '%s' "${l%$'\n'}"; }
# One `ADDRESS PIDS` line per TCP listener on port $1, PIDS comma-separated.
listeners() {
ss -Hltnp 2>/dev/null | awk -v p="$1" '{
n = split($4, a, ":"); if (a[n] != p) next
addr = substr($4, 1, length($4) - length(p) - 1); pids = ""; s = $0
while (match(s, /pid=[0-9]+/)) { pids = pids (pids == "" ? "" : ",") substr(s, RSTART + 4, RLENGTH - 4); s = substr(s, RSTART + RLENGTH) }
print addr, pids
}'
}
# 0 when port $1 has a listener and every one is on 127.0.0.1, held by pid $2 alone.
loopback_by() {
local addr pids p any=0
[ -n "$2" ] && [ "$2" != 0 ] || return 1
while read -r addr pids; do
[ -n "$addr" ] || continue
any=1
[ "$addr" = 127.0.0.1 ] || return 1
for p in ${pids//,/ }; do [ "$p" = "$2" ] || return 1; done
[ -n "$pids" ] || return 1
done <<< "$(listeners "$1")"
[ "$any" = 1 ]
}
listener_text() { local t; t="$(listeners "$1" | tr '\n' ';')"; echo "${t:-none}"; }
BUS_SHOW="$(unit_show "$BUS_UNIT" ActiveState User MainPID)"
CON_SHOW="$(unit_show "$CON_UNIT" ActiveState User MainPID)"
CON_PID="$(prop "$CON_SHOW" MainPID)"
# --- F checks ---
check_f1() {
local u show state user pid uids why=""
for u in "$BUS_UNIT" "$CON_UNIT"; do
if [ "$u" = "$BUS_UNIT" ]; then show="$BUS_SHOW"; else show="$CON_SHOW"; fi
state="$(prop "$show" ActiveState)" user="$(prop "$show" User)" pid="$(prop "$show" MainPID)"
uids="$(proc_uids "${pid:-0}")"
[ "$state" = active ] || { why+="$u is ${state:-unknown}; "; continue; }
[ "$user" = "$S_USER" ] || { why+="$u has User=${user:-unset}; "; continue; }
[ -n "$MOSAIC_UID" ] && [ "$uids" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] ||
why+="$u MainPID ${pid:-none} has uids '${uids:-unreadable}', not $S_USER's ${MOSAIC_UID:-unknown}; "
done
if [ -z "$why" ]; then fpass F1 units-user "$BUS_UNIT and $CON_UNIT are active as $S_USER (uid $MOSAIC_UID)"
else ffail F1 units-user "${why%; }"; fi
}
check_f2() {
local rc=0
curl -fsS --max-time 10 -o /dev/null "http://127.0.0.1:$PORT/healthz" 2>/dev/null || rc=$?
if [ "$rc" != 0 ]; then ffail F2 console-loopback "curl http://127.0.0.1:$PORT/healthz exited $rc"
elif ! loopback_by "$PORT" "$CON_PID"; then ffail F2 console-loopback "port $PORT listeners are '$(listener_text "$PORT")', not 127.0.0.1 by the Console's MainPID ${CON_PID:-none}"
else fpass F2 console-loopback "healthz answers, and $PORT is bound to 127.0.0.1 only, by MainPID $CON_PID"; fi
}
# F5's scan: an outer /usr/bin/bwrap owned by the stack's user with a claude descendant. Sets F5_OK
# when one pair has both labels exactly, F5_WHY to the last mismatch otherwise.
F5_OK="" F5_WHY="no /usr/bin/bwrap process of $S_USER with a claude descendant was seen while the reply was pending"
scan_f5() {
local pid ppid uids kids q c argv0 comm lo lc
local -A parent=() children=() bw=()
# One read of every PPid line and one of every exe link, so a scan stays short on a busy host.
while read -r pid ppid; do
parent[$pid]="$ppid" children[$ppid]+=" $pid"
done < <(grep -H '^PPid:' "$ROOT"/proc/[0-9]*/status 2>/dev/null | sed -n 's#^.*/proc/\([0-9]*\)/status:PPid:[[:space:]]*\([0-9]*\)$#\1 \2#p')
while read -r pid; do
[[ "$pid" =~ ^[0-9]+$ ]] && [ -n "${parent[$pid]:-}" ] && bw[$pid]=1
done < <(find "$ROOT/proc" -mindepth 2 -maxdepth 2 -name exe -lname /usr/bin/bwrap -printf '%h\n' 2>/dev/null | sed 's#.*/##')
for pid in "${!bw[@]}"; do
[ -z "${bw[${parent[$pid]}]:-}" ] || continue
uids="$(proc_uids "$pid")"
[ -n "$MOSAIC_UID" ] && [ "${uids%% *}" = "$MOSAIC_UID" ] || continue
# Every descendant, breadth first.
kids=() q=("$pid")
while [ ${#q[@]} -gt 0 ]; do
for c in ${children[${q[0]}]:-}; do kids+=("$c") q+=("$c"); done
q=("${q[@]:1}")
done
for c in "${kids[@]}"; do
argv0="$(tr '\0' '\n' < "$ROOT/proc/$c/cmdline" 2>/dev/null | sed -n 1p)"
comm="$(cat "$ROOT/proc/$c/comm" 2>/dev/null)"
[[ "${argv0##*/}" == claude* ]] || [[ "$comm" == claude* ]] || continue
lo="$(proc_label "$pid")" lc="$(proc_label "$c")"
if [ "$lo" = "$LABEL_OUTER" ] && [ "$lc" = "$LABEL_CHILD" ]; then
F5_OK="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc'"; return 0
fi
F5_WHY="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc', not '$LABEL_OUTER' and '$LABEL_CHILD'"
done
done
return 1
}
# F3 and F5 together: the message goes out in the background and the scan runs until it is answered.
check_f3_f5() {
local marker text rc=0 out err n tpid
marker="case1-accept-$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"
text="$marker: this is the Case 1 acceptance; reply with one short line."
timeout 330 "${AS_USER[@]}" "$COPY/scripts/mosaic" talk chief "$text" --business "$BUSINESS" --tester --wait 300 > "$WORK/talk.out" 2> "$WORK/talk.err" &
tpid=$!
while :; do
scan_f5 && break
kill -0 "$tpid" 2>/dev/null || break
sleep 0.5
done
wait "$tpid" || rc=$?
# Both the sent line and the reply are on stdout; an error is on stderr.
out="$(cat "$WORK/talk.out")" err="$(cat "$WORK/talk.err")"
n="$(grep -c . <<< "$out")"
if [ "$rc" = 0 ] && grep -q "^sent [^ ]* to chief" <<< "$out" && [ "$n" -ge 2 ]; then
fpass F3 tester-reply "chief replied to $marker"
else
ffail F3 tester-reply "talk exited $rc with $n lines on stdout; last: $(clean "$(tail -n 1 <<< "${err:-$out}")")"
fi
if [ -n "$F5_OK" ]; then F5_LINE=(fpass "$F5_OK"); else F5_LINE=(ffail "$F5_WHY"); fi
}
check_f4() {
local rc=0 out
out="$("${AS_USER[@]}" "$COPY/scripts/case2-precheck.sh" --business "$BUSINESS" --profile factory 2>&1)" || rc=$?
if [ "$rc" = 0 ]; then fpass F4 precheck "case2-precheck.sh --profile factory exits 0"
else ffail F4 precheck "case2-precheck.sh --profile factory exited $rc; last: $(clean "$(tail -n 1 <<< "$out")")"; fi
}
check_f6() {
local want="/opt/mosaic-stack/claude-$CLAUDE_PIN/" v
[[ "$CLAUDE" == "$want"* ]] || { ffail F6 claude-pin "--claude $CLAUDE is not under $want"; return; }
[[ "$CLAUDE/" != */../* ]] || { ffail F6 claude-pin "--claude $CLAUDE has a .. segment"; return; }
v="$("${AS_USER[@]}" "$CLAUDE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$CLAUDE_PIN (Claude Code)" ]; then fpass F6 claude-pin "$CLAUDE prints '$v'"
else ffail F6 claude-pin "$CLAUDE --version prints '$(clean "$v")', not '$CLAUDE_PIN (Claude Code)'"; fi
}
# The PM check confirm runs (stack-release.sh pm_external): exactly one live launches list entry for
# launch.by, for this business. The stack's user runs the whole check and prints one result line.
F7_JS='
const {readFileSync}=require("node:fs");const {spawnSync}=require("node:child_process");
const [file,cli,b]=process.argv.slice(1);
let by;
try{by=JSON.parse(readFileSync(file,"utf8")).launch?.by}catch(e){console.log(`fail cannot read ${file}: ${e.code??"not JSON"}`);process.exit(0)}
if(typeof by!=="string"||!by){console.log("fail the business file has no launch.by");process.exit(0)}
const r=spawnSync(cli,["launches","list","--json"],{encoding:"utf8"});
if(r.status!==0){console.log(`fail launches list exited ${r.status}`);process.exit(0)}
let l;try{l=JSON.parse(r.stdout)}catch{console.log("fail launches list printed no JSON");process.exit(0)}
const live=Array.isArray(l)?l.filter((x)=>x&&x.instance===by&&x.live===true):[];
console.log(live.length!==1?`fail ${live.length} live launches of ${by}`:live[0].business!==b?`fail the live launch of ${by} is for ${live[0].business}`:`ok ${by} run ${live[0].run}`);'
check_f7() {
local out
out="$("${AS_USER[@]}" "$NODE" -e "$F7_JS" -- "$BUSINESS_FILE" "$COPY/scripts/mosaic" "$BUSINESS" 2>/dev/null | sed -n 1p)"
if [[ "$out" =~ ^ok\ [a-z0-9][a-z0-9-]*\ run\ [A-Za-z0-9._-]+$ ]]; then fpass F7 pm-live "one live launch: ${out#ok }"
else ffail F7 pm-live "$(clean "${out#fail }")"; fi
}
check_f8() {
local s
s="$(stat -c '%U %a %F' -- "$ROOT$CRED" 2>/dev/null)" || s="missing"
if [ ! -L "$ROOT$CRED" ] && [ "$s" = "$S_USER 600 regular file" ]; then fpass F8 credential-file "$CRED is '$s'"
else ffail F8 credential-file "$CRED is '$s', not '$S_USER 600 regular file'"; fi
}
# Empty when $1 and every directory above it, up to /, are the running user's (root's on the host) with
# no group or other write: case2-unit.sh's root_only rule for --node, checked again before root runs it.
# Otherwise the first path that breaks the rule. A symlink breaks it, because its mode is 777.
not_root_only() {
local p="$1" st
while :; do
st="$(stat -c '%u %a' -- "$ROOT$p" 2>/dev/null)" || { echo "$p (cannot stat)"; return; }
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) || { echo "$p (uid ${st% *}, mode ${st#* })"; return; }
[ "$p" = / ] && return
p="$(dirname -- "$p")"
done
}
check_f9() {
local s v bad
s="$(stat -c '%U:%G %a %F' -- "$ROOT$SETTINGS" 2>/dev/null)" || s="missing"
[ ! -L "$ROOT$SETTINGS" ] && [ "$s" = "root:root 644 regular file" ] || { ffail F9 autoupdater-off "$SETTINGS is '$s', not 'root:root 644 regular file'"; return; }
# Root runs node here, so it runs only the pinned path, and only when nothing but root can replace it.
[ "$NODE" = "/opt/mosaic-stack/node-$NODE_PIN/bin/node" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: it is not the --node-pin path"; return; }
bad="$(not_root_only "$NODE")"
[ -z "$bad" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: $bad is not root's alone"; return; }
v="$("$ROOT$NODE" -e 'const s=JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(JSON.stringify(s?.env?.DISABLE_AUTOUPDATER??null))' -- "$ROOT$SETTINGS" 2>/dev/null)" || v="unreadable"
if [ "$v" = '"1"' ]; then fpass F9 autoupdater-off "$SETTINGS is root:root 0644 with env.DISABLE_AUTOUPDATER \"1\""
else ffail F9 autoupdater-off "$SETTINGS env.DISABLE_AUTOUPDATER is $v, not \"1\""; fi
}
check_f10() {
local want="/opt/mosaic-stack/node-$NODE_PIN/bin/node" v
[ "$NODE" = "$want" ] || { ffail F10 node-pin "--node $NODE is not $want"; return; }
v="$("${AS_USER[@]}" "$NODE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$NODE_PIN" ]; then fpass F10 node-pin "$NODE prints $v"
else ffail F10 node-pin "$NODE --version prints '$(clean "$v")', not $NODE_PIN"; fi
}
check_f11() {
local d="$ROOT$DATA_ROOT/discord" found
if [ -L "$d" ]; then ffail F11 no-discord "$DATA_ROOT/discord is a symlink"; return; fi
[ -d "$d" ] || { fpass F11 no-discord "$DATA_ROOT/discord is absent"; return; }
found="$(find "$d" -maxdepth 1 -name '*.json' -printf '%f ' 2>/dev/null)"
if [ -z "$found" ]; then fpass F11 no-discord "$DATA_ROOT/discord holds no binding"
else ffail F11 no-discord "$DATA_ROOT/discord holds bindings: ${found% }"; fi
}
# --- P checks ---
check_p1() {
local out
out="$(sudo -l -U "$S_USER" 2>&1)"
if grep -q "is not allowed to run sudo" <<< "$out"; then line P1 "$S_USER-sudo" closed "sudo -l: $(clean "$(sed -n 1p <<< "$out")")"
else line P1 "$S_USER-sudo" unnamed "sudo -l: $(clean "$(tr '\n' ' ' <<< "$out")")"; fi
}
check_p2() {
local gs keys="$ROOT$S_HOME/.ssh/authorized_keys" kstate n
gs="$(groups_of "$S_USER" | tr '\n' ' ')"; gs="${gs% }"
n="$(wc -w <<< "$gs")"
if [ ! -e "$keys" ] && [ ! -L "$keys" ]; then kstate=absent
elif [ -f "$keys" ] && [ ! -L "$keys" ] && [ ! -s "$keys" ]; then kstate=empty
else kstate=present; fi
if [ "$n" = 1 ] && [ "$kstate" != present ]; then line P2 "$S_USER-groups-keys" closed "id -G is '$gs'; authorized_keys is $kstate"
else line P2 "$S_USER-groups-keys" unnamed "id -G is '$gs'; authorized_keys is $kstate"; fi
}
check_p3() {
local n
n="$(sudo -l -U localadmin 2>&1 | grep -c NOPASSWD)"
line P3 localadmin-sudo "accepted Q183" "sudo -l -U localadmin lists $n NOPASSWD lines"
}
check_p4() { line P4 pve-root-pbs "accepted Q182+Q183" "root on pve over VM 1008, and PBS backups that hold $S_HOME with the credential; this script can't see pve"; }
check_p5() {
local gs reach
if [ -z "$(uid_of jarvis)" ]; then line P5 jarvis-reach closed "no jarvis account"; return; fi
gs="$(names_of_groups $(groups_of jarvis) | tr '\n' ' ')"
reach="$(tr ' ' '\n' <<< "$gs" | grep -xE 'sudo|docker|lxd' | tr '\n' ' ')"
if [ -z "$reach" ]; then line P5 jarvis-reach closed "jarvis is in none of sudo, docker and lxd (groups: ${gs% })"
else line P5 jarvis-reach unnamed "jarvis is in ${reach% }"; fi
}
check_p6() {
local g m members bad=0
for g in sudo docker lxd; do
members="$(db group "$g" | cut -d: -f4)"
for m in ${members//,/ }; do
[ "$g" = sudo ] && [ "$m" = localadmin ] && continue
line P6 "group-$g" unnamed "$m is a member of $g"; bad=1
done
done
[ "$bad" = 1 ] || line P6 group-members closed "sudo holds only localadmin, and docker and lxd have no members"
}
check_p7() {
local found
found="$(grep -rlw -- "$S_USER" "$ROOT/etc/sudoers.d" 2>/dev/null | sed "s#^$ROOT##" | tr '\n' ' ')"
if [ -z "$found" ]; then line P7 sudoers-d closed "no file under /etc/sudoers.d names $S_USER"
else line P7 sudoers-d unnamed "files naming $S_USER: ${found% }"; fi
}
check_p8() {
local other
other="$(listeners 7331)$(listeners 7333)"
if loopback_by "$PORT" "$CON_PID" && [ -z "$other" ]; then
line P8 lan-listeners closed "$PORT is on 127.0.0.1 only by MainPID $CON_PID; no TCP listener on 7331 or 7333; tester uses $TESTER_PATH"
else
line P8 lan-listeners unnamed "$PORT: $(listener_text "$PORT"); 7333: $(listener_text 7333); 7331: $(listener_text 7331); Console MainPID ${CON_PID:-none}"
fi
}
check_p9() {
local out n missing=() rc=0 err mg others
out="$("$ROOT$HOSTCHECK" --check 2>/dev/null)"
for n in 1 2 3 4 5 6; do grep -q "^ok $n " <<< "$out" || missing+=("hostcheck item $n has no ok line")
done
err="$(sudo -u jarvis /usr/bin/bwrap --version 2>&1 >/dev/null)" || rc=$?
[ "$rc" != 0 ] && grep -q "Permission denied" <<< "$err" || missing+=("sudo -u jarvis /usr/bin/bwrap --version exited $rc without Permission denied")
mg="$(gid_of "$S_USER")"
[ -n "$mg" ] && [ -z "$(db group "$S_USER" | cut -d: -f4)" ] || missing+=("group $S_USER lists members, or $S_USER has no group")
others="$(db passwd | awk -F: -v g="$mg" -v u="$S_USER" '$4 == g && $1 != u { print $1 }' | tr '\n' ' ')"
[ -z "$others" ] || missing+=("other accounts have $S_USER's gid $mg: ${others% }")
if [ ${#missing[@]} = 0 ]; then
line P9 bwrap-userns "accepted mos-1096" "hostcheck items 1-6 ok, jarvis is denied bwrap, and group $S_USER has no other member"
else
for n in "${missing[@]}"; do line P9 bwrap-userns unnamed "$n"; done
fi
}
# P10 is an observation of mutable same-uid state, never a closure ruling.
TESTER_PEER='import socket,struct,sys
s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
try:
s.settimeout(1)
s.connect(sys.argv[1])
p=struct.unpack("3i",s.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
print(*p)
finally:
s.close()'
tester_snapshot() {
local parent="${TESTER_PATH%/*}" p s pid
[ ! -L "$ROOT$parent" ] && [ ! -L "$ROOT$TESTER_PATH" ] || return 1
p="$(stat -c '%u %a %F %d %i' -- "$ROOT$parent" 2>/dev/null)" || return 1
s="$(stat -c '%u %a %F %d %i' -- "$ROOT$TESTER_PATH" 2>/dev/null)" || return 1
[[ "$p" =~ ^$MOSAIC_UID\ 700\ directory\ [0-9]+\ [0-9]+$ ]] || return 1
[[ "$s" =~ ^$MOSAIC_UID\ 600\ socket\ [0-9]+\ [0-9]+$ ]] || return 1
pid="$(prop "$(unit_show "$CON_UNIT" MainPID)" MainPID)"
[[ "$pid" =~ ^[1-9][0-9]*$ ]] && [ "$pid" = "$CON_PID" ] || return 1
[ "$(proc_uids "$pid")" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] || return 1
printf '%s|%s|%s' "$pid" "$p" "$s"
}
tester_python() {
local real bad magic
# Resolve the system entry, then validate the target and every ancestor.
[ -f "$ROOT/usr/bin/python3" ] && [ -x "$ROOT/usr/bin/python3" ] || return 1
real="$(realpath -e -- "$ROOT/usr/bin/python3" 2>/dev/null)" || return 1
[ -z "$ROOT" ] || real="${real#"$ROOT"}"
case "$real" in /home/*|/root/*|/run/user/*|"$S_HOME"/*) return 1 ;; esac
bad="$(not_root_only "$real")"; [ -z "$bad" ] || return 1
bad="$(not_root_only /usr/bin)"; [ -z "$bad" ] || return 1
magic="$(od -An -tx1 -N4 -- "$ROOT$real" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
printf '%s' "$real"
}
check_p10() {
local floor before after python peer pid uid gid why="" pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})$'
floor="$(cat "$ROOT/proc/sys/net/ipv4/ip_unprivileged_port_start" 2>/dev/null)"
[[ "$floor" =~ ^[0-9]{1,5}$ ]] && [ "$floor" -gt "$PORT" ] || why=floor
before="$(tester_snapshot)" || why=metadata
python="$(tester_python)" || why=interpreter
if [ -z "$why" ]; then
peer="$(tester_capture 3 128 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why=probe
if [[ "$peer" =~ $pattern ]]; then
pid="${BASH_REMATCH[1]}" uid="${BASH_REMATCH[2]}" gid="${BASH_REMATCH[3]}"
[ "$pid" = "$CON_PID" ] && [ "$uid" = "$MOSAIC_UID" ] && [ "$gid" -le 2147483647 ] || why=peer
else why=probe; fi
after="$(tester_snapshot)" || why=metadata
[ "$before" = "$after" ] || why=changed
fi
if [ -z "$why" ]; then
line P10 tester-socket-squat unnamed "boundary=observed path=$TESTER_PATH parent=0700 owner=$MOSAIC_UID socket=0600 peer_pid=$pid peer_uid=$uid floor=$floor port=$PORT; hostile same-uid substitution remains"
else
line P10 tester-socket-squat unnamed "boundary=failed reason=$why path=$TESTER_PATH MainPID=${CON_PID:-none}; hostile same-uid substitution remains"
fi
}
check_p11() {
local en before
en="$(systemctl is-enabled mosaic-hostcheck.service 2>/dev/null)"
before="$(prop "$(unit_show mosaic-hostcheck.service Before)" Before)"
if [ "$en" = enabled ] && grep -qxF -- "$BUS_UNIT" <<< "${before// /$'\n'}"; then
line P11 hostcheck-before-bus closed "mosaic-hostcheck.service is enabled, and its Before= names $BUS_UNIT"
else
line P11 hostcheck-before-bus unnamed "mosaic-hostcheck.service is '${en:-unknown}', Before='$before'; it needs enabled and $BUS_UNIT"
fi
}
check_f1
check_f2
F5_LINE=()
check_f3_f5
check_f4
"${F5_LINE[0]}" F5 bwrap-label "${F5_LINE[@]:1}"
check_f6
check_f7
check_f8
check_f9
check_f10
check_f11
check_p1; check_p2; check_p3; check_p4; check_p5; check_p6
check_p7; check_p8; check_p9; check_p10; check_p11
for m in "${MARKS[@]}"; do echo "count $m ${COUNT[$m]:-0}"; done
if [ "$F_FAILED" = 0 ]; then echo "case1-accept: every F check passes; send the whole output to Duckworth, and each unnamed line to Mos"; exit 0; fi
echo "case1-accept: an F check failed, so the row fails"
exit 1
@@ -0,0 +1,717 @@
// Row 195 (#1693): scripts/case1-accept.sh, root's acceptance run on the Case 1 host. Design r7 section 5
// (agents/darkwing/work/queue-195/DESIGN.md); deviations in agents/darkwing/work/queue-195/BUILD.md.
// Every case runs the script with --root on a scratch host tree: /etc, /proc, /srv/mosaic, /opt and the
// host check live under it. Fake systemctl, curl, ss, sudo and stat on PATH record their argv; the fake
// sudo runs the stack's user's commands from the tree. Nothing here reads or writes the real /etc,
// /proc or /srv, and the stand-in credential is not a credential.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { tmp } from "./helpers.mjs";
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const SCRIPT = join(REPO, "scripts", "case1-accept.sh");
const COPY = "0123456789abcdef0123456789abcdef01234567";
const NODE_PIN = "v24.1.0", CLAUDE_PIN = "2.1.0";
const NODE = `/opt/mosaic-stack/node-${NODE_PIN}/bin/node`;
const CLAUDE = `/opt/mosaic-stack/claude-${CLAUDE_PIN}/node_modules/@anthropic-ai/claude-code/bin/claude.exe`;
const HOME = "/srv/mosaic";
const CLI = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/mosaic`;
const PRECHECK = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/case2-precheck.sh`;
const CRED = `${HOME}/.config/mosaic-dev/secrets/claude-session.env`;
const CRED_VALUE = "stand-in-not-a-credential-7f3e2a";
const AS_MOSAIC = `-u mosaic -H env PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin`;
const ARGS = ["--profile", "factory", "--user", "mosaic", "--home", HOME, "--business", "mosaic-stack", "--copy", COPY,
"--node", NODE, "--claude", CLAUDE, "--pm", "--", "--port", "732", "--business", "mosaic-stack", "--tester-socket"];
const OUTER = "bwrap (enforce)", CHILD = "bwrap//&unpriv_bwrap (enforce)";
const MARKS = ["pass", "FAIL", "closed", "accepted Q182+Q183", "accepted Q183", "accepted mos-1096", "unnamed"];
const P_PASS = { P1: "closed", P2: "closed", P3: "accepted Q183", P4: "accepted Q182+Q183", P5: "closed", P6: "closed",
P7: "closed", P8: "closed", P9: "accepted mos-1096", P10: "unnamed", P11: "closed" };
const PASSWD = "root:x:0:0:root:/root:/bin/bash\nmosaic:x:997:997::/srv/mosaic:/usr/sbin/nologin\nlocaladmin:x:1000:1000::/home/localadmin:/bin/bash\njarvis:x:1001:1001::/home/jarvis:/bin/bash\n";
const GROUP = "root:x:0:\nsudo:x:27:localadmin\nmosaic:x:997:\nlxd:x:998:\ndocker:x:999:\nlocaladmin:x:1000:\njarvis:x:1001:\n";
const SS = "LISTEN 0 511 127.0.0.1:732 0.0.0.0:* users:((\"node\",pid=200,fd=21))\n"
+ "LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:((\"sshd\",pid=900,fd=3))\n";
const HOSTCHECK_OK = [1, 2, 3, 4, 5, 6, 7].map((n) => `ok ${n} item ${n} holds`).join("\n") + "\nnote bubblewrap 0.9.0 is the install pin\n";
function put(file, text, mode) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, text);
if (mode) chmodSync(file, mode);
}
const edit = (file, f) => writeFileSync(file, f(readFileSync(file, "utf8")));
const log = (name) => `printf '%s' ${JSON.stringify(name)} >> "$HC/calls.log"; printf ' %s' "$@" >> "$HC/calls.log"; echo >> "$HC/calls.log"`;
// Fakes on PATH. Each records its argv and answers from a file in the state directory.
const FAKES = {
systemctl: `case "$1" in
show) [ -f "$HC/show-$2" ] && cat "$HC/show-$2" ;;
is-enabled) cat "$HC/enabled-$2" 2>/dev/null || { echo disabled; exit 1; } ;;
esac`,
curl: `exit "$(cat "$HC/curl-rc")"`,
ss: `cat "$HC/ss"`,
od: `if [ "$1" = -An ] && [ "$2" = -tx1 ] && [ "$3" = -N4 ]; then
[ -f "$HC/elf" ] && cat "$HC/elf" || echo 7f454c46
else exec /usr/bin/od "$@"; fi`,
// The credential and the settings file answer from the state directory, as root would see them, and
// only those calls are recorded; everything else is the real stat, which reads the args file.
stat: `case "\${@: -1}" in
*claude-session.env) ${log("stat")}; cat "$HC/stat-cred" ;;
*console/tester.sock) cat "$HC/stat-socket" ;;
*console) [ "$2" = '%u %a %F %d %i' ] && cat "$HC/stat-parent" || echo "997 700" ;;
*managed-settings.json) ${log("stat")}; cat "$HC/stat-settings" ;;
*) exec /usr/bin/stat "$@" ;;
esac`,
// -l -U USER prints the state file; -u jarvis is the bwrap probe; -u mosaic -H env PATH=... CMD runs
// CMD from the tree, with the PATH entries and each absolute argument that exists there moved under it.
sudo: `${CASE_SUDO_SESSION}
case "$1" in
-l) [ -f "$HC/sudo-l-$3" ] && cat "$HC/sudo-l-$3"; exit 0 ;;
-u) ;;
*) exit 97 ;;
esac
user="$2"; shift 2
if [ "$user" = jarvis ]; then cat "$HC/jarvis-bwrap" >&2; exit "$(cat "$HC/jarvis-bwrap-rc")"; fi
[ "$user" = mosaic ] && [ "$1" = -H ] && [ "$2" = env ] || exit 98
shift 2
if [ "$1" = -i ]; then
[ "$2" = HOME=/srv/mosaic ] || exit 98
shift 2
fi
[[ "$1" == PATH=* ]] || exit 98
p="\${1#PATH=}"; shift
np=""; IFS=: read -ra parts <<< "$p"; for x in "\${parts[@]}"; do np+="$ROOT_DIR$x:"; done
cmd="$1"; shift
args=(); for a in "$@"; do if [[ "$a" == /* ]] && [ -e "$ROOT_DIR$a" ]; then args+=("$ROOT_DIR$a"); else args+=("$a"); fi; done
export HOME="$ROOT_DIR/srv/mosaic" PATH="$np$PATH"
[ ! -e "$ROOT_DIR$cmd" ] || cmd="$ROOT_DIR$cmd"
case_sudo_run "$HC" "$cmd" "\${args[@]}"`,
};
// Executables inside the tree, run by the fake sudo or by the script itself. Node marks a run outside
// the fake sudo, which sets HOME to the stack's home, so a test can tell whether root ran it.
const NODE_STUB = `[ "$HOME" = "$ROOT_DIR/srv/mosaic" ] || touch "$HC/node-as-root"
[ "\${1:-}" = --version ] && { cat "$HC/node-version"; exit 0; }
exec "$REAL_NODE" "$@"`;
// The real Python payload runs against a mock socket object, never an OS listener.
const PYTHON_STUB = `[ -f "$HC/peer-output" ] && { cat "$HC/peer-output"; exit "$(cat "$HC/peer-status")"; }
[ -f "$HC/peer-hang" ] && {
here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here")
echo "$here" > "$HC/peer-pid"; printf '%s %s\\n' "$here" "$sid" >> "$HC/query-pids"
trap '' TERM HUP; exec sleep 60
}
if [ -f "$HC/peer-change" ]; then cat "$HC/peer-change" > "$HC/stat-socket"; fi
if [ -f "$HC/peer-pid-change" ]; then cat "$HC/peer-pid-change" > "$HC/show-mosaic-webui.service"; fi
exec /usr/bin/python3 -I -S -c '
import os,socket,struct,sys
class Fake:
def __init__(self,*a): assert a==(socket.AF_UNIX,socket.SOCK_STREAM)
def settimeout(self,n): assert n==1
def connect(self,p): assert p.endswith("/console/tester.sock")
def getsockopt(self,*a):
assert a==(socket.SOL_SOCKET,socket.SO_PEERCRED,12)
return struct.pack("3i",*[int(x) for x in open(os.environ["HC"]+"/peer").read().split()])
def close(self): pass
socket.socket=Fake
payload=sys.argv[4]
sys.argv=["probe",sys.argv[5]]
exec(payload)
' "$@"`;
const TREE = {
"/usr/bin/python3": PYTHON_STUB,
[NODE]: NODE_STUB,
[CLAUDE]: `[ "\${1:-}" = --version ] && cat "$HC/claude-version"; exit 0`,
[CLI]: `${log("mosaic")}
case "$1" in
talk) case "$(cat "$HC/talk-mode")" in
reply) echo "sent ab12cd34 to chief"; echo "[chief -> tester] seen" ;;
noreply) echo "sent ab12cd34 to chief"; echo "no reply from chief within 300 s" >&2; exit 1 ;;
sentonly) echo "sent ab12cd34 to chief" ;;
esac ;;
launches) cat "$HC/launches.json" ;;
*) exit 2 ;;
esac`,
[PRECHECK]: `${log("precheck")}
echo "precheck: done"; exit "$(cat "$HC/precheck-rc")"`,
"/usr/local/lib/mosaic-stack/hostcheck.sh": `${log("hostcheck")}
cat "$HC/hostcheck-out"`,
};
function proc(root, pid, { ppid, uid = 997, exe, argv0 = "x", comm = "x", label }) {
const d = join(root, "proc", String(pid));
put(join(d, "status"), `Name:\t${comm}\nState:\tS (sleeping)\nPPid:\t${ppid}\nUid:\t${uid}\t${uid}\t${uid}\t${uid}\n`);
put(join(d, "cmdline"), `${argv0}\0--x\0`);
put(join(d, "comm"), `${comm}\n`);
rmSync(join(d, "exe"), { force: true });
if (exe) symlinkSync(exe, join(d, "exe"));
if (label !== undefined) put(join(d, "attr", "current"), label);
}
// A scratch Case 1 host where every F check passes and every P check is closed or accepted.
function host(t) {
const dir = tmp(t, "c1accept-");
const root = join(dir, "root"), hc = join(dir, "hc"), bin = join(dir, "bin");
const args = join(root, "etc", "mosaic-stack", "dev.args");
put(args, ARGS.join("\n") + "\n", 0o644);
put(join(root, "etc", "passwd"), PASSWD);
put(join(root, "etc", "group"), GROUP);
put(join(root, "etc", "sudoers.d", "90-localadmin"), "localadmin ALL=(ALL) NOPASSWD: ALL\n");
put(join(root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: "1" } }));
put(join(root, CRED), `CLAUDE_CODE_OAUTH_TOKEN=${CRED_VALUE}\n`, 0o600);
put(join(root, HOME, ".config", "mosaic-dev", "businesses", "mosaic-stack.json"), JSON.stringify({ id: "mosaic-stack", launch: { by: "pm" } }));
put(join(root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
proc(root, 1, { ppid: 0, uid: 0, exe: "/usr/lib/systemd/systemd", comm: "systemd" });
proc(root, 100, { ppid: 1, exe: NODE, argv0: NODE, comm: "node" });
proc(root, 200, { ppid: 1, exe: NODE, argv0: NODE, comm: "node" });
proc(root, 300, { ppid: 100, exe: "/usr/bin/bwrap", argv0: "/usr/bin/bwrap", comm: "bwrap", label: `${OUTER}\n` });
proc(root, 301, { ppid: 300, exe: NODE, argv0: NODE, comm: "node", label: `${CHILD}\n` });
proc(root, 302, { ppid: 301, exe: CLAUDE, argv0: CLAUDE, comm: "claude.exe", label: `${CHILD}\n` });
// A root bwrap with a mislabelled claude child, which F5 must pass over.
proc(root, 400, { ppid: 1, uid: 0, exe: "/usr/bin/bwrap", argv0: "/usr/bin/bwrap", comm: "bwrap", label: "unconfined\n" });
proc(root, 401, { ppid: 400, uid: 0, argv0: "claude", comm: "claude", label: "unconfined\n" });
const state = {
"[email protected]": "ActiveState=active\nUser=mosaic\nMainPID=100\n",
"show-mosaic-webui.service": "ActiveState=active\nUser=mosaic\nMainPID=200\n",
"show-mosaic-hostcheck.service": "[email protected] mosaic-webui.service multi-user.target\n",
"enabled-mosaic-hostcheck.service": "enabled\n",
"curl-rc": "0", ss: SS,
"stat-parent": "997 700 directory 9 10\n", "stat-socket": "997 600 socket 9 11\n", peer: "200 997 997\n",
"stat-cred": "mosaic 600 regular file\n", "stat-settings": "root:root 644 regular file\n",
"sudo-l-mosaic": "User mosaic is not allowed to run sudo on dev-53.\n",
"sudo-l-localadmin": "User localadmin may run the following commands on dev-53:\n (ALL : ALL) ALL\n (ALL) NOPASSWD: ALL\n",
"jarvis-bwrap": "sudo: unable to execute /usr/bin/bwrap: Permission denied\n", "jarvis-bwrap-rc": "1",
"node-version": `${NODE_PIN}\n`, "claude-version": `${CLAUDE_PIN} (Claude Code)\n`,
"talk-mode": "reply", "precheck-rc": "0", "hostcheck-out": HOSTCHECK_OK,
"launches.json": JSON.stringify([
{ instance: "pm", live: true, business: "mosaic-stack", run: "r-0002" },
{ instance: "pm", live: false, business: "mosaic-stack", run: "r-0001" },
{ instance: "dev-1", live: true, business: "mosaic-stack", run: "r-0003" },
]),
};
for (const [name, text] of Object.entries(state)) put(join(hc, name), text);
for (const [name, body] of Object.entries(FAKES)) put(join(bin, name), `#!/usr/bin/env bash\n${name === "stat" ? "" : log(name)}\n${body}\n`, 0o755);
for (const [path, body] of Object.entries(TREE)) put(join(root, path), `#!/usr/bin/env bash\n${body}\n`, 0o755);
const copyRoot = join(root, HOME, '.local/share/mosaic-stack/copies', COPY);
put(join(copyRoot, 'packages/cli/src/config.mjs'), `import fs from 'node:fs';export const loadSystem=()=>({dataRoot:fs.readFileSync(${JSON.stringify(join(hc,'data-root'))},'utf8').trim()});`);
put(join(copyRoot, 'packages/webui/src/cli.mjs'), '// MOSAIC_TESTER_SOCKET_V1\n');
put(join(hc, 'data-root'), `${HOME}/.mosaic-dev\n`);
put(join(root, HOME, '.mosaic-dev/console/tester.sock'), 'stand-in socket metadata only');
chmodSync(join(root, HOME, '.mosaic-dev/console'), 0o700);
// F9 runs node only when it and each directory above it have no group or other write, whatever the umask.
for (let d = dirname(NODE); d !== "/"; d = dirname(d)) chmodSync(join(root, d), 0o755);
chmodSync(root, 0o755);
return { dir, root, hc, bin, args };
}
function run(h, extra = {}, argv = ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN, "--root", h.root]) {
const r = spawnSync(SCRIPT, argv, {
encoding: "utf8",
env: { ...process.env, PATH: `${h.bin}:${process.env.PATH}`, HC: h.hc, ROOT_DIR: h.root, REAL_NODE: process.execPath, TMPDIR: h.dir, ...extra },
});
const lines = r.stdout.split("\n").filter(Boolean);
// Each check line as { id, path, mark, evidence }.
const checks = lines.filter((l) => /^[FP][0-9]+ /.test(l)).map((l) => {
const [id, path, ...rest] = l.split(" ");
const tail = rest.join(" ");
const mark = MARKS.find((m) => tail === m || tail.startsWith(`${m} `));
return { id, path, mark, evidence: tail.slice(mark?.length ?? 0).trim(), line: l };
});
return { status: r.status, out: r.stdout, err: r.stderr, lines, checks };
}
const calls = (h, name) => (existsSync(join(h.hc, "calls.log")) ? readFileSync(join(h.hc, "calls.log"), "utf8") : "")
.split("\n").filter((l) => l === name || l.startsWith(`${name} `)).map((l) => l.slice(name.length + 1));
const failed = (r) => r.checks.filter((c) => c.mark === "FAIL").map((c) => c.id);
const marksOf = (r, id) => r.checks.filter((c) => c.id === id).map((c) => c.mark);
const mustRun = (h, extra) => { const r = run(h, extra); assert.ok(r.status === 0 || r.status === 1, r.out + r.err); return r; };
test('row253 socket observation uses configured root, metadata and the Console peer without sending bytes', t=>{
const h=host(t), data='/srv/mosaic/custom-data';
put(join(h.hc,'data-root'),data+'\n');
put(join(h.root,data,'console/tester.sock'),'metadata stand-in');
const r=run(h);
assert.equal(r.status,0,r.out+r.err);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=observed path=\/srv\/mosaic\/custom-data\/console\/tester.sock/);
assert.match(r.out,/peer_pid=200 peer_uid=997/);
assert.match(r.out,/hostile same-uid substitution remains/);
assert.doesNotMatch(r.out,/accepted mos-1101/);
assert.deepEqual(marksOf(r,'P8'),['closed']);
});
test('row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure',t=>{
const faults={
'parent-mode': h=>put(join(h.hc,'stat-parent'),'997 755 directory 9 10\n'),
'parent-owner': h=>put(join(h.hc,'stat-parent'),'998 700 directory 9 10\n'),
'socket-mode': h=>put(join(h.hc,'stat-socket'),'997 660 socket 9 11\n'),
'socket-owner': h=>put(join(h.hc,'stat-socket'),'998 600 socket 9 11\n'),
'socket-type': h=>put(join(h.hc,'stat-socket'),'997 600 regular file 9 11\n'),
symlink:h=>{const p=join(h.root,HOME,'.mosaic-dev/console/tester.sock');rmSync(p);symlinkSync('/absent',p);},
peer:h=>put(join(h.hc,'peer'),'200 998 997\n'),
'peer-pid':h=>put(join(h.hc,'peer'),'555 997 997\n'),
'python-mode':h=>chmodSync(join(h.root,'usr/bin/python3'),0o777),
'python-missing':h=>rmSync(join(h.root,'usr/bin/python3')),
'python-not-elf':h=>put(join(h.hc,'elf'),'23212f62\n'),
'peer-malformed':h=>{put(join(h.hc,'peer-output'),'200 997 997 extra\n');put(join(h.hc,'peer-status'),'0');},
'peer-error':h=>{put(join(h.hc,'peer-output'),'200 997 997\n');put(join(h.hc,'peer-status'),'1');},
'peer-uid-fields':h=>proc(h.root,200,{ppid:1,uid:998,exe:NODE,comm:'node'}),
'changed-inode':h=>put(join(h.hc,'peer-change'),'997 600 socket 9 99\n'),
'changed-pid':h=>put(join(h.hc,'peer-pid-change'),'ActiveState=active\nUser=mosaic\nMainPID=201\n'),
floor:h=>put(join(h.root,'proc/sys/net/ipv4/ip_unprivileged_port_start'),'700\n'),
};
for(const [name,fault] of Object.entries(faults)){
const h=host(t);fault(h);const r=run(h);
assert.equal(r.status,name==='peer-uid-fields'?1:0,`${name}: ${r.out}${r.err}`);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=failed/,name);
assert.doesNotMatch(r.out,/boundary=observed/,name);
}
});
test('row253 acceptance rejects bad config and unsupported copies before talk',t=>{
const faults={
outside:h=>put(join(h.hc,'data-root'),'/srv/mosaic2/data\n'),
dot:h=>put(join(h.hc,'data-root'),'/srv/mosaic/./data\n'),
absent:h=>rmSync(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs')),
unmarked:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/webui/src/cli.mjs'),'// old copy'),
schema:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),'console.log(JSON.stringify({dataRoot:"/srv/mosaic/.mosaic-dev",socket:true,extra:1}));process.exit(0);'),
failed:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),'console.log(JSON.stringify({dataRoot:"/srv/mosaic/.mosaic-dev",socket:true}));process.exit(1);'),
};
for(const [name,fault] of Object.entries(faults)){
const h=host(t);fault(h);const r=run(h);
assert.equal(r.status,2,`${name}: ${r.out}${r.err}`);assert.equal(r.out,'');
assert.match(r.err,/tester (config query|path refuses)|support/);
assert.deepEqual(calls(h,'mosaic'),[]);
}
});
test('row253 stalled peer observation ends within its bound without leaving a reader',async t=>{
const h=host(t);put(join(h.hc,'peer-hang'),'');
const r=run(h);assert.equal(r.status,0,r.out+r.err);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=failed reason=probe/);
const pid=Number(readFileSync(join(h.hc,'peer-pid'),'utf8'));
let state='gone';
for(let n=0;n<50;n++){
try{state=readFileSync(`/proc/${pid}/stat`,'utf8').split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`peer helper ${pid} still ${state}`);
});
test('row253 P8 refuses both retired TCP and board listeners',t=>{
for(const port of [7333,7331]){
const h=host(t);put(join(h.hc,'ss'),SS+`LISTEN 0 511 127.0.0.1:${port} 0.0.0.0:* users:(("node",pid=200,fd=22))\n`);
assert.deepEqual(marksOf(run(h),'P8'),['unnamed']);
}
});
test('row253 C4 acceptance query inner reason holds under both sudo models',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];
put(join(h.hc,'session-mode'),mode);
put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),blockingQuerySource(files[1]));
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query inner-timeout\n');
assert.ok(seconds<7,`${mode}: ${seconds}s`);assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
assert.deepEqual(await pollFixtureGone(files),[]);await stop();
}
});
test('row253 C4 acceptance query outer reason discards stdout and names residual cleanup',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-config'),'');
put(join(h.hc,'stall-output'),JSON.stringify({dataRoot:HOME+'/.mosaic-dev',socket:true}));
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query outer-timeout\n');
assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s preflight slack`);
assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'R3 session remains before fixture cleanup');
killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit R3 fixture cleanup');await stop();
}
});
test('row253 C4 acceptance query overflow is output-limit in both models',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);const capture=cleanupFixture(t,files);
put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:'${HOME}/.mosaic-dev'});`);
const r=run(h);capture();assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query output-limit\n');
assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);assert.deepEqual(await pollFixtureGone(files),[]);
}
});
for(const fault of ['inner','outer','overflow'])test(`row253 C4 P10 ${fault} reason holds under both sudo models`,async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];put(join(h.hc,'session-mode'),mode);
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files,7000);
if(fault==='inner')put(join(h.hc,'peer-hang'),'');
if(fault==='outer'){put(join(h.hc,'stall-probe'),'');put(join(h.hc,'stall-output'),'200 997 997\n');}
if(fault==='overflow'){put(join(h.hc,'peer-output'),'200 997 997\n'+'X'.repeat(256));put(join(h.hc,'peer-status'),'0');}
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,0,r.out+r.err);
const reason=fault==='inner'?'inner-timeout':fault==='outer'?'outer-timeout':'output-limit';
assert.match(r.out,new RegExp('P10 tester-socket-squat unnamed boundary=failed reason=probe-'+reason+' '));
assert.doesNotMatch(r.out,/boundary=observed/);
if(fault==='inner')assert.ok(seconds<4,`${mode}: ${seconds}s includes functional checks`);
if(fault==='outer'){
assert.ok(seconds>=4&&seconds<6,`${mode}: ${seconds}s; 2s functional-check slack`);
if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'stalled supervisor is R3 residual');
for(const file of files)killRecordedPids(file);
}
assert.deepEqual(await pollFixtureGone(files),[],fault==='outer'?'explicit R3 cleanup':mode);await stop();
}
});
// One fault per F check; each makes that check, and only that one, fail.
const F_FAULTS = {
F1: (h) => proc(h.root, 100, { ppid: 1, uid: 0, exe: NODE, comm: "node" }),
F2: (h) => put(join(h.hc, "ss"), SS.replace("127.0.0.1:732 ", "0.0.0.0:732 ")),
F3: (h) => put(join(h.hc, "talk-mode"), "noreply"),
F4: (h) => put(join(h.hc, "precheck-rc"), "1"),
F5: (h) => put(join(h.root, "proc", "302", "attr", "current"), "unconfined\n"),
F6: (h) => put(join(h.hc, "claude-version"), "2.0.9 (Claude Code)\n"),
F7: (h) => put(join(h.hc, "launches.json"), JSON.stringify([{ instance: "pm", live: false, business: "mosaic-stack", run: "r-1" }])),
F8: (h) => put(join(h.hc, "stat-cred"), "mosaic 640 regular file\n"),
F9: (h) => put(join(h.root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: "0" } })),
F10: (h) => put(join(h.hc, "node-version"), "v24.0.0\n"),
F11: (h) => put(join(h.root, HOME, ".mosaic-dev", "discord", "binding.json"), "{}"),
};
test("row 195 accept: every check passing exits 0, with each P mark and counts that match the lines", (t) => {
const h = host(t);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
assert.deepEqual(r.checks.map((c) => c.id), ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11",
"P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9", "P10", "P11"]);
for (const c of r.checks.filter((x) => x.id.startsWith("F"))) assert.equal(c.mark, "pass", c.line);
for (const [id, mark] of Object.entries(P_PASS)) assert.deepEqual(marksOf(r, id), [mark], id);
const counts = r.lines.filter((l) => l.startsWith("count ")).map((l) => l.slice(6));
assert.deepEqual(counts, MARKS.map((m) => `${m} ${r.checks.filter((c) => c.mark === m).length}`));
assert.match(r.lines.at(-1), /^case1-accept: every F check passes/);
assert.match(r.out, /^F5 bwrap-label pass bwrap 300 reads 'bwrap \(enforce\)' and its claude descendant 302 reads/m);
assert.match(r.out, /^F7 pm-live pass one live launch: pm run r-0002$/m);
assert.ok(!(r.out + r.err).includes(CRED_VALUE));
assert.ok(existsSync(join(h.hc, "node-as-root")), "F9 runs the pinned, root-only node as root");
});
test("row 195 accept: exits 1 with each F check failing alone, and its line names it", (t) => {
for (const [id, fault] of Object.entries(F_FAULTS)) {
const h = host(t);
fault(h);
const r = mustRun(h);
assert.equal(r.status, 1, `${id}: ${r.out}`);
assert.deepEqual(failed(r), [id], `${id}: ${r.out}`);
assert.equal(r.lines.at(-1), "case1-accept: an F check failed, so the row fails");
}
});
test("row 195 accept: F3 fails on exit 0 with no reply line, and its evidence takes talk's error", (t) => {
let h = host(t);
put(join(h.hc, "talk-mode"), "sentonly");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F3"]);
assert.match(r.out, /^F3 tester-reply FAIL talk exited 0 with 1 lines on stdout; last: sent ab12cd34 to chief$/m);
h = host(t);
put(join(h.hc, "talk-mode"), "noreply");
r = mustRun(h);
assert.match(r.out, /^F3 tester-reply FAIL talk exited 1 with 1 lines on stdout; last: no reply from chief within 300 s$/m);
});
test("row 195 accept: F1 fails on a unit that isn't active or isn't User=mosaic", (t) => {
for (const show of ["ActiveState=failed\nUser=mosaic\nMainPID=0\n", "ActiveState=active\nUser=root\nMainPID=100\n"]) {
const h = host(t);
put(join(h.hc, "show-mosaic-webui.service"), show);
const r = mustRun(h);
assert.equal(r.status, 1);
assert.ok(failed(r).includes("F1"), r.out);
}
});
test("row 195 accept: F2 fails when healthz doesn't answer and when another pid holds the port", (t) => {
let h = host(t);
put(join(h.hc, "curl-rc"), "7");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F2"]);
assert.match(r.out, /^F2 console-loopback FAIL curl http:\/\/127\.0\.0\.1:732\/healthz exited 7$/m);
h = host(t);
put(join(h.hc, "ss"), SS.replace("pid=200,fd=21", "pid=555,fd=4"));
r = mustRun(h);
assert.deepEqual(failed(r), ["F2"]);
});
test("row 195 accept: F5 fails on unpriv_bwrap (enforce), on complain and on a trailing space", (t) => {
for (const label of ["unpriv_bwrap (enforce)\n", "bwrap//&unpriv_bwrap (complain)\n", `${CHILD} \n`]) {
const h = host(t);
put(join(h.root, "proc", "302", "attr", "current"), label);
const r = mustRun(h);
assert.deepEqual(failed(r), ["F5"], `${JSON.stringify(label)}: ${r.out}`);
}
const h = host(t);
put(join(h.root, "proc", "300", "attr", "current"), `${OUTER} \n`);
assert.deepEqual(failed(mustRun(h)), ["F5"]);
});
test("row 195 accept: F5 fails with no bwrap of mosaic's, and passes on comm alone", (t) => {
let h = host(t);
proc(h.root, 300, { ppid: 100, uid: 1001, exe: "/usr/bin/bwrap", comm: "bwrap", label: `${OUTER}\n` });
let r = mustRun(h);
assert.deepEqual(failed(r), ["F5"]);
assert.match(r.out, /^F5 bwrap-label FAIL no \/usr\/bin\/bwrap process of mosaic/m);
h = host(t);
proc(h.root, 302, { ppid: 301, argv0: "/proc/self/fd/3", comm: "claude", label: `${CHILD}\n` });
assert.deepEqual(failed(mustRun(h)), []);
});
test("row 195 accept: F6 fails on a stub that prints nothing, on a path outside claude-<pin> and on a .. segment", (t) => {
let h = host(t);
put(join(h.hc, "claude-version"), "");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F6"]);
h = host(t);
const other = CLAUDE.replace(`claude-${CLAUDE_PIN}`, "claude-2.0.9");
put(h.args, ARGS.map((a) => (a === CLAUDE ? other : a)).join("\n") + "\n", 0o644);
put(join(h.root, other), `#!/usr/bin/env bash\necho "${CLAUDE_PIN} (Claude Code)"\n`, 0o755);
r = mustRun(h);
assert.deepEqual(failed(r), ["F6"]);
assert.match(r.out, /^F6 claude-pin FAIL --claude \S+claude-2\.0\.9\S+ is not under \/opt\/mosaic-stack\/claude-2\.1\.0\/$/m);
// Under claude-<pin>/ by prefix, but the binary it names is 2.0.9's, which answers with the pin.
h = host(t);
const dotdot = CLAUDE.replace(`claude-${CLAUDE_PIN}/`, `claude-${CLAUDE_PIN}/../claude-2.0.9/`);
put(h.args, ARGS.map((a) => (a === CLAUDE ? dotdot : a)).join("\n") + "\n", 0o644);
put(join(h.root, other), `#!/usr/bin/env bash\necho "${CLAUDE_PIN} (Claude Code)"\n`, 0o755);
r = mustRun(h);
assert.deepEqual(failed(r), ["F6"], r.out);
assert.match(r.out, /^F6 claude-pin FAIL --claude \S+\/claude-2\.1\.0\/\.\.\/claude-2\.0\.9\/\S+ has a \.\. segment$/m);
});
test("row 195 accept: F7 fails on two live PM launches, one for another business, and no launch.by", (t) => {
const two = [{ instance: "pm", live: true, business: "mosaic-stack", run: "a" }, { instance: "pm", live: true, business: "mosaic-stack", run: "b" }];
for (const [file, text] of [["launches.json", JSON.stringify(two)], ["launches.json", JSON.stringify([{ instance: "pm", live: true, business: "other", run: "a" }])]]) {
const h = host(t);
put(join(h.hc, file), text);
assert.deepEqual(failed(mustRun(h)), ["F7"]);
}
const h = host(t);
put(join(h.root, HOME, ".config", "mosaic-dev", "businesses", "mosaic-stack.json"), JSON.stringify({ id: "mosaic-stack" }));
const r = mustRun(h);
assert.deepEqual(failed(r), ["F7"]);
assert.match(r.out, /^F7 pm-live FAIL the business file has no launch\.by$/m);
});
test("row 195 accept: F8 fails on 0640, on another owner and on a symlink, and never prints the credential", (t) => {
for (const fault of [
(h) => put(join(h.hc, "stat-cred"), "mosaic 640 regular file\n"),
(h) => put(join(h.hc, "stat-cred"), "root 600 regular file\n"),
(h) => { const f = join(h.root, CRED); rmSync(f); put(`${f}.real`, `X=${CRED_VALUE}\n`, 0o600); symlinkSync(`${f}.real`, f); },
]) {
const h = host(t);
fault(h);
const r = mustRun(h);
assert.deepEqual(failed(r), ["F8"], r.out);
assert.ok(!(r.out + r.err).includes(CRED_VALUE));
}
});
test("row 195 accept: F9 fails on another owner, a group-writable mode and a number for the flag", (t) => {
for (const fault of [
(h) => put(join(h.hc, "stat-settings"), "root:root 664 regular file\n"),
(h) => put(join(h.hc, "stat-settings"), "mosaic:root 644 regular file\n"),
(h) => put(join(h.root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: 1 } })),
]) {
const h = host(t);
fault(h);
assert.deepEqual(failed(mustRun(h)), ["F9"]);
}
});
test("row 195 accept: F9 won't run --node as root off the pin path or with a writable directory above it", (t) => {
// Another node that works for the stack's user: F9 refuses to run it, and F10 fails on the path.
let h = host(t);
const other = NODE.replace(NODE_PIN, "v24.0.9");
put(h.args, ARGS.map((a) => (a === NODE ? other : a)).join("\n") + "\n", 0o644);
put(join(h.root, other), `#!/usr/bin/env bash\n${NODE_STUB}\n`, 0o755);
let r = mustRun(h);
assert.deepEqual(failed(r), ["F9", "F10"], r.out);
assert.match(r.out, /^F9 autoupdater-off FAIL root won't run --node \/opt\/mosaic-stack\/node-v24\.0\.9\/bin\/node: it is not the --node-pin path$/m);
assert.ok(!existsSync(join(h.hc, "node-as-root")));
// The pinned path, under a group-writable directory and then a symlinked node: F9 alone fails, and
// root doesn't run it.
for (const [fault, bad] of [
[(x) => chmodSync(join(x.root, dirname(NODE)), 0o775), "/opt/mosaic-stack/node-v24\\.1\\.0/bin \\(uid \\d+, mode 775\\)"],
[(x) => { const f = join(x.root, NODE); renameSync(f, `${f}.real`); symlinkSync(`${f}.real`, f); }, "/opt/mosaic-stack/node-v24\\.1\\.0/bin/node \\(uid \\d+, mode 777\\)"],
]) {
h = host(t);
fault(h);
r = mustRun(h);
assert.deepEqual(failed(r), ["F9"], r.out);
assert.match(r.out, new RegExp(`^F9 autoupdater-off FAIL root won't run --node \\S+: ${bad} is not root's alone$`, "m"));
assert.ok(!existsSync(join(h.hc, "node-as-root")));
}
});
test("row 195 accept: F11 fails on one *.json under discord/ and passes on an empty directory", (t) => {
let h = host(t);
put(join(h.root, HOME, ".mosaic-dev", "discord", "chief.json"), "{}");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F11"]);
assert.match(r.out, /^F11 no-discord FAIL \/srv\/mosaic\/\.mosaic-dev\/discord holds bindings: chief\.json$/m);
h = host(t);
mkdirSync(join(h.root, HOME, ".mosaic-dev", "discord"), { recursive: true });
assert.deepEqual(failed(mustRun(h)), []);
});
// Each P check in its unnamed mark, with the line count each fault makes; none changes the exit.
const P_FAULTS = {
P1: [1, (h) => put(join(h.hc, "sudo-l-mosaic"), "User mosaic may run the following commands on dev-53:\n")],
P2: [1, (h) => put(join(h.root, HOME, ".ssh", "authorized_keys"), "ssh-ed25519 AAAA test\n")],
P5: [1, (h) => edit(join(h.root, "etc", "group"), (s) => s.replace("docker:x:999:", "docker:x:999:jarvis"))],
P6: [2, (h) => edit(join(h.root, "etc", "group"), (s) => s.replace("sudo:x:27:localadmin", "sudo:x:27:localadmin,bob").replace("lxd:x:998:", "lxd:x:998:carol"))],
P7: [1, (h) => put(join(h.root, "etc", "sudoers.d", "50-mosaic"), "mosaic ALL=(root) NOPASSWD: /usr/bin/systemctl\n")],
P8: [1, (h) => put(join(h.hc, "ss"), `${SS}LISTEN 0 511 0.0.0.0:7331 0.0.0.0:* users:(("node",pid=200,fd=23))\n`)],
P9: [3, (h) => { put(join(h.hc, "hostcheck-out"), HOSTCHECK_OK.replace("ok 2 ", "FAIL 2 ").replace("ok 5 ", "FAIL 5 ")); put(join(h.hc, "jarvis-bwrap-rc"), "0"); }],
P10: [1, (h) => put(join(h.root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "700\n")],
P11: [1, (h) => put(join(h.hc, "show-mosaic-hostcheck.service"), "[email protected] multi-user.target\n")],
};
test("row 195 accept: each P check reports unnamed on its fault, and the exit stays 0", (t) => {
for (const [id, [n, fault]] of Object.entries(P_FAULTS)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 0, `${id}: ${r.out}`);
assert.deepEqual(marksOf(r, id), Array(n).fill("unnamed"), `${id}: ${r.out}`);
}
});
test("row 195 accept: P2 is unnamed with a supplementary group, and P5 names docker", (t) => {
let h = host(t);
put(join(h.root, "etc", "group"), GROUP.replace("lxd:x:998:", "lxd:x:998:mosaic"));
let r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P2"), ["unnamed"]);
assert.match(r.out, /^P2 mosaic-groups-keys unnamed id -G is '997 998'; authorized_keys is absent$/m);
h = host(t);
P_FAULTS.P5[1](h);
r = run(h);
assert.match(r.out, /^P5 jarvis-reach unnamed jarvis is in docker$/m);
assert.match(r.out, /^P6 group-docker unnamed jarvis is a member of docker$/m);
});
test("row 195 accept: P5 is closed with no jarvis account, and P2 with an empty authorized_keys", (t) => {
const h = host(t);
put(join(h.root, "etc", "passwd"), PASSWD.replace(/^jarvis:.*\n/m, ""));
put(join(h.root, HOME, ".ssh", "authorized_keys"), "");
const r = run(h);
assert.deepEqual(marksOf(r, "P5"), ["closed"]);
assert.deepEqual(marksOf(r, "P2"), ["closed"]);
});
test("row 195 accept: P9 is unnamed once for each item missing", (t) => {
const items = {
hostcheck: (h) => put(join(h.hc, "hostcheck-out"), HOSTCHECK_OK.replace("ok 3 ", "FAIL 3 ")),
jarvis: (h) => { put(join(h.hc, "jarvis-bwrap-rc"), "0"); put(join(h.hc, "jarvis-bwrap"), ""); },
member: (h) => put(join(h.root, "etc", "group"), GROUP.replace("mosaic:x:997:", "mosaic:x:997:jarvis")),
gid: (h) => put(join(h.root, "etc", "passwd"), `${PASSWD}hermes:x:1002:997::/home/hermes:/bin/bash\n`),
};
for (const [name, fault] of Object.entries(items)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P9"), ["unnamed"], `${name}: ${r.out}`);
}
const h = host(t);
put(join(h.hc, "hostcheck-out"), "");
assert.deepEqual(marksOf(run(h), "P9"), Array(6).fill("unnamed"));
});
test("row253 accept: P10 fails its observation with another peer; P11 with the unit disabled", (t) => {
let h = host(t);
put(join(h.hc, "peer"), "555 997 997\n");
let r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P10"), ["unnamed"]);
assert.match(r.out, /P10 tester-socket-squat unnamed boundary=failed reason=peer/);
h = host(t);
put(join(h.hc, "enabled-mosaic-hostcheck.service"), "disabled\n");
r = run(h);
assert.deepEqual(marksOf(r, "P11"), ["unnamed"]);
});
test("row 195 accept: every P check unnamed together still exits 0", (t) => {
const h = host(t);
for (const [, fault] of Object.values(P_FAULTS)) fault(h);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
assert.deepEqual(failed(r), []);
for (const id of ["P1", "P2", "P5", "P6", "P7", "P8", "P9", "P10", "P11"]) assert.ok(marksOf(r, id).every((m) => m === "unnamed"), id);
assert.match(r.lines.at(-1), /^case1-accept: every F check passes/);
});
test("row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason", (t) => {
const bad = {
"group write": [(h) => chmodSync(h.args, 0o664), /dev\.args must be \S+'s with no group or other write \(it is uid \d+, mode 664\)$/],
symlink: [(h) => { put(`${h.args}.real`, ARGS.join("\n") + "\n", 0o644); rmSync(h.args); symlinkSync(`${h.args}.real`, h.args); }, /no regular file \/etc\/mosaic-stack\/dev\.args$/],
"unknown flag": [(h) => put(h.args, ["--send", ...ARGS].join("\n") + "\n", 0o644), /has --send before --, which case2-unit\.sh does not take$/],
"two copies": [(h) => put(h.args, ["--copy", COPY, ...ARGS].join("\n") + "\n", 0o644), /needs exactly one --copy before --, not 2$/],
"library profile": [(h) => put(h.args, ARGS.map((a) => (a === "factory" ? "library" : a)).join("\n") + "\n", 0o644), /has --profile 'library', not factory$/],
"another business": [(h) => put(h.args, ARGS.map((a, i) => (a === "mosaic-stack" && i < 14 ? "acme" : a)).join("\n") + "\n", 0o644), /is for business acme, not mosaic-stack$/],
"no tester socket": [(h) => put(h.args, ARGS.slice(0, -1).join("\n") + "\n", 0o644), /needs one bare --tester-socket after --$/],
"retired TCP": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\n')+'\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],
"socket value": [(h) => put(h.args, [...ARGS,'7333'].join('\n')+'\n',0o644), /unknown serve flag 7333$/],
"duplicate socket": [(h) => put(h.args, [...ARGS,'--tester-socket'].join('\n')+'\n',0o644), /needs one bare --tester-socket after --$/],
"short copy": [(h) => put(h.args, ARGS.map((a) => (a === COPY ? "abc123" : a)).join("\n") + "\n", 0o644), /lacks a plain --user, --home, --business, --node, --claude or --copy$/],
"no file": [(h) => rmSync(h.args), /no regular file \/etc\/mosaic-stack\/dev\.args$/],
};
for (const [name, [fault, why]] of Object.entries(bad)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 2, `${name}: ${r.out}${r.err}`);
assert.match(r.err.trimEnd(), /^case1-accept: refusing: /, name);
assert.match(r.err.trimEnd(), why, name);
assert.equal(r.out, "", name);
assert.ok(!existsSync(join(h.hc, "calls.log")), name);
}
});
test("row 195 accept: usage errors and a run without --root as a user exit 2", (t) => {
const h = host(t);
let r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--node-pin", NODE_PIN, "--root", h.root]);
assert.equal(r.status, 2);
assert.match(r.err, /^usage: /);
r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", "24.1.0", "--root", h.root]);
assert.equal(r.status, 2);
r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN]);
assert.equal(r.status, 2);
assert.equal(r.err, "case1-accept: refusing: run it as root\n");
assert.ok(!existsSync(join(h.hc, "calls.log")));
});
test("row 195 accept: every in-instance call goes through sudo -u mosaic -H env PATH=<node dir>:...", (t) => {
const h = host(t);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
const sudo = calls(h, "sudo");
const asMosaic = sudo.filter((a) => a.startsWith("-u mosaic"));
const isolated = asMosaic.filter(a=>a.startsWith('-u mosaic -H env -i '));
assert.equal(isolated.length, 2);
for(const a of isolated) assert.ok(a.startsWith(`-u mosaic -H env -i HOME=${HOME} PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin `),a);
for (const a of asMosaic.filter(a=>!isolated.includes(a))) assert.ok(a.startsWith(`${AS_MOSAIC} /`), a);
const cmds = asMosaic.filter(a=>!isolated.includes(a)).map((a) => a.slice(AS_MOSAIC.length + 1).split(" ")[0]);
assert.deepEqual([...new Set(cmds)].sort(), [CLI, PRECHECK, CLAUDE, NODE].sort());
assert.deepEqual(sudo.filter((a) => !a.startsWith("-u mosaic")).sort(),
["-l -U localadmin", "-l -U mosaic", "-u jarvis /usr/bin/bwrap --version"]);
const talk = calls(h, "mosaic").filter((a) => a.startsWith("talk "));
assert.equal(talk.length, 1);
assert.match(talk[0], /^talk chief case1-accept-[0-9a-f]{12}: .* --business mosaic-stack --tester --wait 300$/);
assert.deepEqual(calls(h, "precheck"), ["--business mosaic-stack --profile factory"]);
});
test("row 195 accept: the host check runs with --check and never --enforce", (t) => {
const h = host(t);
run(h);
assert.deepEqual(calls(h, "hostcheck"), ["--check"]);
});
@@ -0,0 +1,610 @@
#!/usr/bin/env bash
# Case 2 row C2-13a (design 9.3, 10 step 2c): render and install the two
# system units a hub runs as, [email protected] (enabled as
# mosaic-bus@<business>.service) and mosaic-webui.service, from the existing
# templates, which it doesn't change.
#
# case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH
# [--profile factory|library] [--pm] [--claude PATH]
# -- SERVE FLAGS
#
# Root runs it from a root-owned checkout of the reviewed commit outside
# <home>, never from the release, because <user> can write the release. It
# writes both units under /etc/systemd/system as root:root 0644, runs
# `systemctl daemon-reload`, enables both, then starts the bus and then the
# Console (Q189). Both come back after a boot on their own. It never links
# a unit file into place. Root only stats files under <home> and reads bounded
# stdout from a config/target query run as the stack user through sudo. Root
# never imports copy code. This adds sudo and GNU timeout to the installer
# dependencies. The inner query supervisor runs after the uid drop: 5 s inside,
# 7 s on root's wait. Fixed system Bash, setsid, sleep, timeout and head are
# resolved and root-validated. Only the config query uses this supervisor.
# It renders the default instance only.
#
# The library profile takes exactly these serve flags, each once:
# --port N (1..1023) --business ID (the unit's) --public-host NAME
# --auth-header HEADER --allow-user ID --send
# --proof-header NAME --proof-file PATH
# The factory profile takes --port N (1..1023) and --business ID, and an
# optional bare --tester-socket marker. Retired tester TCP refuses. Only factory takes
# --pm and --claude.
#
# On a re-install the installed units must equal the render in everything
# but the copy id, and both must name one copy, so a re-pin or rollback
# re-renders the pair together. A unit that was already running stays on its
# old copy, since `systemctl start` leaves it alone; the script says so and
# that the bus and Console must restart together, and restarts nothing.
#
# Test seams, for packages/cli/tests/case2-unit.test.mjs only:
# CASE2_UNIT_DIR the unit directory, default /etc/systemd/system;
# the root and root-owned checks apply only there
# CASE2_SYSTEMCTL the systemctl command, default systemctl
# CASE2_GETENT the getent command, default getent
# CASE2_BUS_TEMPLATE default packages/cli/systemd/mosaic-bus.service.in
# CASE2_WEBUI_TEMPLATE default packages/webui/systemd/mosaic-webui.service.in
#
# Exit 0 done, 1 refused, 4 usage.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEFAULT_UNIT_DIR=/etc/systemd/system
UNIT_DIR="${CASE2_UNIT_DIR:-$DEFAULT_UNIT_DIR}"
SYSTEMCTL="${CASE2_SYSTEMCTL:-systemctl}"
GETENT="${CASE2_GETENT:-getent}"
BUS_TEMPLATE="${CASE2_BUS_TEMPLATE:-$REPO/packages/cli/systemd/mosaic-bus.service.in}"
WEBUI_TEMPLATE="${CASE2_WEBUI_TEMPLATE:-$REPO/packages/webui/systemd/mosaic-webui.service.in}"
[email protected]
WEBUI_UNIT=mosaic-webui.service
usage() { echo 'usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS' >&2; exit 4; }
die() { echo "case2-unit: $*" >&2; exit 1; }
# A value the unit carries as itself: no space, %, $, quote, backslash, & or |.
plain() { case "$1" in *[!A-Za-z0-9/._+-]*) return 1 ;; esac; }
abs_plain() {
case "$1" in /*) ;; *) die "$2 must be an absolute path: $1" ;; esac
plain "$1" || die "$2 contains a character the unit cannot carry: $1"
case "$1/" in */./*|*/../*|?*//*) die "$2 must be a normalized path: $1" ;; esac
[ "$1" = / ] || [ "${1%/}" = "$1" ] || die "$2 must not end in /: $1"
}
PROFILE=library USER_NAME='' HOME_DIR='' BUSINESS='' COPY_ID='' NODE='' PM=0 CLAUDE='' CLAUDE_SET=0
while [ $# -gt 0 ]; do
case "$1" in
--profile|--user|--home|--business|--copy|--node|--claude)
[ $# -ge 2 ] || usage
case "$1" in
--profile) PROFILE="$2" ;; --user) USER_NAME="$2" ;; --home) HOME_DIR="$2" ;;
--business) BUSINESS="$2" ;; --copy) COPY_ID="$2" ;; --node) NODE="$2" ;;
--claude) CLAUDE="$2"; CLAUDE_SET=1 ;;
esac
shift 2 ;;
--pm) PM=1; shift ;;
--) shift; break ;;
*) usage ;;
esac
done
SERVE=("$@")
[ -n "$USER_NAME" ] && [ -n "$HOME_DIR" ] && [ -n "$BUSINESS" ] && [ -n "$COPY_ID" ] && [ -n "$NODE" ] || usage
case "$PROFILE" in factory|library) ;; *) die "--profile must be factory or library: $PROFILE" ;; esac
if [ "$PROFILE" = library ]; then
[ "$PM" = 0 ] || die "the library profile refuses --pm"
[ "$CLAUDE_SET" = 0 ] || die "the library profile refuses --claude"
fi
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
[ "$(id -u)" = 0 ] || die "writing $DEFAULT_UNIT_DIR needs root"
fi
# The user, its uid, primary group and home, from the passwd database.
[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "--user must be a user name: $USER_NAME"
PW="$("$GETENT" passwd "$USER_NAME")" || die "no user $USER_NAME in the passwd database"
IFS=: read -r _ _ USER_UID USER_GID _ PW_HOME _ <<<"$PW"
[[ "$USER_UID" =~ ^[0-9]+$ ]] && [ "$USER_UID" -ne 0 ] || die "--user must not be root: $USER_NAME"
abs_plain "$HOME_DIR" '--home'
[ "$HOME_DIR" = "$PW_HOME" ] || die "--home $HOME_DIR is not $USER_NAME's home in the passwd database ($PW_HOME)"
GR="$("$GETENT" group "$USER_GID")" || die "no group $USER_GID in the group database"
GROUP="${GR%%:*}"
[[ "$GROUP" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "the primary group of $USER_NAME is not a group name: $GROUP"
case "$BUSINESS" in [a-z0-9]*) ;; *) die "--business must be a business id: $BUSINESS" ;; esac
case "$BUSINESS" in *[!a-z0-9._-]*) die "--business must be a business id: $BUSINESS" ;; esac
[ "${#BUSINESS}" -le 64 ] || die "--business must be a business id: $BUSINESS"
# The copy: copy_root's rule (scripts/stack-copy.sh:8, :22), under <home>.
COPIES="$HOME_DIR/.local/share/mosaic-stack/copies"
[[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] || die "--copy must be a full 40-character lower-case commit id: $COPY_ID"
COPY="$COPIES/$COPY_ID"
[ -d "$COPY" ] && [ ! -L "$COPY" ] || die "no copy directory $COPY"
[ -f "$COPY/scripts/mosaic" ] || die "no copy $COPY_ID in $COPIES: $COPY/scripts/mosaic is missing"
# Owned by root and writable by no one else, the file and every directory above it.
root_only() {
local p="$1" what="$2" owner mode
while :; do
owner="$(stat -c '%u' -- "$p")" || die "cannot stat $p"
mode="$(stat -c '%a' -- "$p")" || die "cannot stat $p"
[ "$owner" = 0 ] || die "$what: $p is not owned by root"
(( (8#$mode & 8#022) == 0 )) || die "$what: $p is writable by its group or others"
[ "$p" = / ] && break
p="$(dirname -- "$p")"
done
}
# --node: an absolute path to a root-owned regular file outside <home>.
abs_plain "$NODE" '--node'
case "$NODE/" in "$HOME_DIR"/*) die "--node must be outside $HOME_DIR: $NODE" ;; esac
[ ! -L "$NODE" ] || die "--node must not be a symlink; pass its target: $NODE"
[ -f "$NODE" ] && [ -x "$NODE" ] || die "--node must be an executable regular file: $NODE"
root_only "$NODE" '--node'
# --claude follows the --node rule, so nothing running as <user> can replace
# the binary the bus runs.
if [ "$CLAUDE_SET" = 1 ]; then
abs_plain "$CLAUDE" '--claude'
case "$CLAUDE/" in "$HOME_DIR"/*) die "--claude must be outside $HOME_DIR: $CLAUDE" ;; esac
[ ! -L "$CLAUDE" ] || die "--claude must not be a symlink; pass its target: $CLAUDE"
[ -f "$CLAUDE" ] && [ -x "$CLAUDE" ] || die "--claude must be an executable regular file: $CLAUDE"
root_only "$CLAUDE" '--claude'
fi
# The checkout the templates come from, when installing for real.
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
case "$REPO/" in "$HOME_DIR"/*) die "run this from a root-owned checkout outside $HOME_DIR, not $REPO" ;; esac
root_only "$BUS_TEMPLATE" 'bus template'
root_only "$WEBUI_TEMPLATE" 'Console template'
fi
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
# row253-supervisor-begin
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
stat_fields() {
local pid="$1" raw rest
if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
[ -e "/proc/$pid/stat" ] && return 1
return 3
fi
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
[[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
}
clock_cs() {
local boot spare
read -r boot spare < /proc/uptime || return 1
[[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
}
clean_session() {
local entry pid rc live until
clock_cs || return 1
until=$((CLOCK_CS + 25))
while :; do
live=0
for entry in /proc/[0-9]*/stat; do
pid="${entry%/stat}"; pid="${pid##*/}"
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" != 3 ] || continue
[ "$rc" = 0 ] || return 1
[ "$ST_SESSION" = "$child" ] || continue
case "$ST_STATE" in Z|X) continue ;; esac
live=1
if ! kill -KILL "$pid" 2>/dev/null; then
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
fi
done
[ "$live" != 0 ] || return 0
clock_cs || return 1
[ "$CLOCK_CS" -lt "$until" ] || return 1
"$sleep_bin" 0.01
done
}
finish() {
local rc="$?"
trap - EXIT HUP INT TERM
if [ "$verified" = 1 ]; then
clean_session || rc=86
elif [ -n "$child" ]; then
kill -KILL "$child" 2>/dev/null || :
fi
[ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
"$setsid_bin" "$@" & child=$!
for ((attempt=0; attempt<100; attempt++)); do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 85
if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
tester_tools() {
local input target p st magic
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
p="$target"
while :; do
st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
[ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
[ "$p" != / ] || break
p="${p%/*}"; [ -n "$p" ] || p=/
done
magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
case "$input" in
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
/usr/bin/head) TS_HEAD="$target" ;;
esac
done
}
tester_capture_reason() {
case "$1" in
81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
*) echo inner-command ;;
esac
}
tester_capture() (
local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
tester_tools || exit 84
file="$(mktemp)" || exit 88
notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
trap 'exit 88' HUP INT TERM
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
set -o pipefail
maximum="$1"; head_bin="$2"; shift 2
"$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
# Only GNU timeout and the trusted head can write this root-side channel.
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
[ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
[ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# The serve flags, checked as scripts/webui-service.sh serve_flags checks
# them (:201-264), and kept in the order given. That script compares the
# proof file's owner to `id -u`, which is root here, so this copy has its own.
serve_flags() {
local -A seen=()
local flags='' name value
local port='' public_host='' auth_header='' allow_user='' proof_header='' proof_file='' tester_socket=0
local i=0 n=${#SERVE[@]}
while [ "$i" -lt "$n" ]; do
name="${SERVE[$i]}"
case "$name" in
--tester-port) die "case2-unit.sh retires tester TCP; use --tester-socket" ;;
--tester-socket) [ "$PROFILE" = factory ] || die "the library profile refuses --tester-socket" ;;
--checkout) die "refusing --checkout: Case 2 has no queue panel" ;;
--board) die "refusing --board: Case 2 runs no control board" ;;
--sso) die "refusing --sso: the Console signs people in through the proxy, with the preview set and the proof pair (Q175 phase 2 is a follow-up)" ;;
--port|--business) ;;
--public-host|--auth-header|--allow-user|--proof-header|--proof-file|--send)
[ "$PROFILE" = library ] || die "the factory profile refuses $name: its host has no proxy" ;;
*) die "unknown serve flag: $name" ;;
esac
[ -z "${seen[$name]:-}" ] || die "serve flag $name given twice"
seen[$name]=1
if [ "$name" = --tester-socket ]; then tester_socket=1; flags="$flags --tester-socket"; i=$((i + 1)); continue; fi
if [ "$name" = --send ]; then flags="$flags --send"; i=$((i + 1)); continue; fi
[ $((i + 1)) -lt "$n" ] || die "serve flag $name needs a value"
value="${SERVE[$((i + 1))]}"
case "$name" in
--port) port="$value"
[[ "$port" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$port" -le 1023 ] || die "--port must be 1..1023, so only the unit's capability can bind it: $port" ;;
--business) [ "$value" = "$BUSINESS" ] || die "serve --business $value is not the unit's business $BUSINESS" ;;
--public-host) public_host="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "public host must be a hostname, no port or scheme: $value" ;; esac
case "$value" in *[!A-Za-z0-9.-]*) die "public host must be a hostname, no port or scheme: $value" ;; esac ;;
--auth-header) auth_header="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "auth header must be a header name: $value" ;; esac
case "$value" in *[!A-Za-z0-9-]*) die "auth header must be a header name: $value" ;; esac ;;
--allow-user) allow_user="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "allowed user must start with a letter or digit: $value" ;; esac
case "$value" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $value" ;; esac ;;
--proof-header) proof_header="$value"
[[ "$value" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,63}$ ]] || die "invalid proxy proof header" ;;
--proof-file) proof_file="$value" ;;
esac
flags="$flags $name $value"
i=$((i + 2))
done
local required=(--port --business)
[ "$PROFILE" = factory ] || required+=(--public-host --auth-header --allow-user --send --proof-header --proof-file)
for name in "${required[@]}"; do
[ -n "${seen[$name]:-}" ] || die "serve flag $name is required"
done
if [ "$PROFILE" = factory ]; then
if [ "$tester_socket" = 1 ]; then
local query
query="$(tester_capture 5 7 4096 "$USER_NAME" "$HOME_DIR" "$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query $(tester_capture_reason "$?")"
tester_facts "$query" || die "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || die "the target does not support --tester-socket"
tester_location "$TS_DATA" "$HOME_DIR" "$USER_UID" "" || die "tester path refuses $TS_REASON"
fi
echo "${flags# }"; return 0
fi
# Lead decision 101: the uid header carries the Authentik uid.
if [ "${auth_header,,}" = x-authentik-uid ] && ! [[ "$allow_user" =~ ^[0-9a-f]{64}$ ]]; then
die "with --auth-header $auth_header the allowed user must be an Authentik uid, 64 lower-case hex characters (lead decision 101): $allow_user"
fi
case "${proof_header,,}" in host|origin|x-mosaic-tester|"${auth_header,,}") die "proxy proof header must differ from Host, Origin, tester and auth headers" ;; esac
# The proof file: metadata only, never opened (design 9.3).
local secrets="$HOME_DIR/.config/mosaic-dev/secrets" real owner mode
abs_plain "$proof_file" 'proxy proof file'
[ ! -L "$proof_file" ] || die "proxy proof file must not be a symlink: $proof_file"
[ -f "$proof_file" ] || die "proxy proof file must be a regular file: $proof_file"
real="$(realpath -e -- "$proof_file")" || die "cannot resolve proxy proof file: $proof_file"
[ "$real" = "$proof_file" ] || die "proxy proof file path must have no symlink in it: $proof_file"
case "$proof_file" in "$secrets"/*) ;; *) die "proxy proof file must be under $secrets, outside the data root: $proof_file" ;; esac
owner="$(stat -c '%u' -- "$proof_file")" || die "cannot stat proxy proof file"
mode="$(stat -c '%a' -- "$proof_file")" || die "cannot stat proxy proof file"
[ "$owner" = "$USER_UID" ] || die "proxy proof file must be owned by $USER_NAME (uid $USER_UID): $proof_file"
[ "$mode" = 600 ] || [ "$mode" = 400 ] || die "proxy proof file must be mode 0600 or 0400, not $mode: $proof_file"
echo "${flags# }"
}
# PATH: the node's directory first, then the default list without it. Unlike
# scripts/webui-service.sh:303-305 this always puts node's directory first,
# so the bus's `exec node` (scripts/mosaic) finds the same node.
NODE_DIR="$(dirname -- "$NODE")"
UNIT_PATH="$NODE_DIR"
for d in /usr/local/bin /usr/bin /bin; do [ "$d" = "$NODE_DIR" ] || UNIT_PATH="$UNIT_PATH:$d"; done
FLAGS="$(serve_flags)" || exit 1
HARDENING="ProtectSystem=strict
ReadWritePaths=$HOME_DIR
ProtectHome=yes
PrivateTmp=yes"
CAPS="AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE"
# The default instance, as scripts/instance.sh:85-107 renders it: no
# MOSAIC_CONFIG line, and the session env under %h, which becomes <home>.
SESSION_ENV='%h/.config/mosaic-dev/secrets/claude-session.env'
ENV_FILE="-$HOME_DIR/.config/mosaic-dev/secrets/claude-session.env"
PM_FLAGS=''
[ "$PM" = 0 ] || PM_FLAGS=' --pm'
CLAUDE_ENV=''
[ "$CLAUDE_SET" = 0 ] || CLAUDE_ENV="Environment=MOSAIC_CLAUDE_BIN=$CLAUDE"
# One unit from its template. kind is bus or webui.
render() {
local kind="$1" template="$2" text tokens
[ -f "$template" ] && [ ! -L "$template" ] || die "template missing or not a regular file: $template"
text="$(sed -e "s|@REPO@|$COPY|g" -e "s|@PATH@|$UNIT_PATH|g" -e "s|@NODE@|$NODE|g" \
-e "s|@PM_FLAGS@|$PM_FLAGS|g" -e "s|^@CLAUDE_ENV@\$|$CLAUDE_ENV|" \
-e '/^@CONFIG_ENV@$/d' -e "s|@SESSION_ENV@|$SESSION_ENV|g" \
-e "s|%h|$HOME_DIR|g" -- "$template")"
# Checked before @FLAGS@ goes in, since an allowed user may hold @.
tokens="$(printf '%s\n' "$text" | grep -oE '@[A-Z_]+@' || true)"
if [ "$kind" = webui ]; then
[ "$tokens" = @FLAGS@ ] || die "$template: a token other than one @FLAGS@ is left after the render: $(echo $tokens)"
text="${text//@FLAGS@/$FLAGS}"
else
[ -z "$tokens" ] || die "$template: a token is left after the render: $(echo $tokens)"
fi
if printf '%s\n' "$text" | grep -q '%h'; then die "$template: %h is left after the render"; fi
local add="User=$USER_NAME\nGroup=$GROUP\n${HARDENING//$'\n'/\\n}\n"
[ "$kind" = bus ] || add="$add${CAPS//$'\n'/\\n}\n"
# The added lines go at the end of [Service], before its trailing blank
# lines. In [Install], WantedBy=multi-user.target replaces
# WantedBy=default.target in both units, so both start at boot (Q189).
printf '%s\n' "$text" | awk -v add="$add" '
function blanks_out() { for (; blanks > 0; blanks--) print "" }
/^\[.*\]$/ {
if (sec == "[Service]") printf "%s", add
sec = $0
blanks_out(); print; next
}
$0 == "" { blanks++; next }
{ blanks_out() }
sec == "[Install]" && $0 == "WantedBy=default.target" { $0 = "WantedBy=multi-user.target" }
{ print }
END { if (sec == "[Service]") printf "%s", add; blanks_out() }'
}
# The unit's settings as section<TAB>key<TAB>value, one per line, keys
# trimmed as systemd trims them. A continued line refuses, so no setting
# hides in one.
settings() {
awk '
{ sub(/^[ \t]+/, ""); sub(/[ \t]+$/, "") }
$0 == "" || /^[#;]/ { next }
/\\$/ { print "CONTINUED"; exit }
/^\[.*\]$/ { sec = $0; next }
{
i = index($0, "="); if (i == 0) { print "NOKEY\t" $0; next }
k = substr($0, 1, i - 1); v = substr($0, i + 1)
sub(/[ \t]+$/, "", k); sub(/^[ \t]+/, "", v)
print sec "\t" k "\t" v
}'
}
# Refuses unless one unit's fixed lines hold. what names the unit in messages,
# copy is the copy directory it should run from.
check_unit() {
local kind="$1" text="$2" what="$3" copy="$4" s
s="$(printf '%s\n' "$text" | settings)"
! printf '%s\n' "$s" | grep -qx CONTINUED || die "$what: a line continues onto the next"
! printf '%s\n' "$s" | grep -q '^NOKEY' || die "$what: a line is neither a section, a comment nor a setting"
# %h in a system unit is root's home, not <user>'s.
! printf '%s\n' "$text" | grep -q '%h' || die "$what: %h is left; a system unit reads it as root's home"
local key
for key in RestrictNamespaces PrivateUsers SystemCallFilter PermissionsStartOnly; do
! printf '%s\n' "$s" | awk -F'\t' -v k="$key" '$2 == k { found = 1 } END { exit !found }' \
|| die "$what: refusing $key=, which the hub's units must not hold"
done
# An Exec*= value whose leading prefix characters hold + or ! runs with
# full privileges, whatever User= says.
local bad
bad="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^Exec/ { p = $3; sub(/[^-@:+!].*$/, "", p); if (p ~ /[+!]/) print $2 "=" $3 }')"
[ -z "$bad" ] || die "$what: refusing an Exec line that runs with full privileges: $bad"
# The fixed lines, each exactly once, in [Service].
one() {
local k="$1" v="$2" got
got="$(printf '%s\n' "$s" | awk -F'\t' -v k="$k" '$2 == k { print $1 "\t" $3 }')"
[ "$got" = "[Service] $v" ] || die "$what: $k= must be exactly once in [Service], as $k=$v"
}
none() {
! printf '%s\n' "$s" | awk -F'\t' -v k="$1" '$2 == k { found = 1 } END { exit !found }' || die "$what: refusing $1=, which this unit must not hold"
}
one User "$USER_NAME"
one Group "$GROUP"
one WorkingDirectory "$copy"
# Environment= lines: PATH, and MOSAIC_CLAUDE_BIN only with --claude.
local envs want_envs="[Service] PATH=$UNIT_PATH"
[ "$kind" = webui ] || [ -z "$CLAUDE_ENV" ] || want_envs="$want_envs
[Service] MOSAIC_CLAUDE_BIN=$CLAUDE"
envs="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "Environment" { print $1 "\t" $3 }')"
[ "$envs" = "$want_envs" ] || die "$what: the Environment= lines must be exactly: $(printf '%s' "$want_envs" | cut -f2 | tr '\n' ' ')"
one NoNewPrivileges yes
one ProtectSystem strict
one ReadWritePaths "$HOME_DIR"
one ProtectHome yes
one PrivateTmp yes
local order
order="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)$/ { printf "%s ", $2 }')"
[ "$order" = "ProtectSystem ReadWritePaths ProtectHome PrivateTmp " ] || die "$what: the hardening lines must be in the order ProtectSystem, ReadWritePaths, ProtectHome, PrivateTmp"
local wanted
wanted="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "WantedBy" { print $1 "\t" $3 }')"
[ "$wanted" = "[Install] multi-user.target" ] || die "$what: WantedBy= must be exactly once, as WantedBy=multi-user.target"
if [ "$kind" = bus ]; then
one ExecStart "$copy/scripts/mosaic bus start %i$PM_FLAGS"
one EnvironmentFile "$ENV_FILE"
none AmbientCapabilities
none CapabilityBoundingSet
else
one ExecStart "$NODE $copy/packages/webui/src/cli.mjs serve $FLAGS"
none EnvironmentFile
one AmbientCapabilities CAP_NET_BIND_SERVICE
one CapabilityBoundingSet CAP_NET_BIND_SERVICE
fi
}
BUS_TEXT="$(render bus "$BUS_TEMPLATE")" || exit 1
WEBUI_TEXT="$(render webui "$WEBUI_TEMPLATE")" || exit 1
check_unit bus "$BUS_TEXT" "rendered $BUS_UNIT" "$COPY"
check_unit webui "$WEBUI_TEXT" "rendered $WEBUI_UNIT" "$COPY"
# A re-install: each installed unit passes the same checks, both name one
# copy, and each equals the render once its copy id is the new one.
[ -d "$UNIT_DIR" ] && [ ! -L "$UNIT_DIR" ] || die "no unit directory $UNIT_DIR"
installed_ids=''
for pair in "bus:$BUS_UNIT:BUS_TEXT" "webui:$WEBUI_UNIT:WEBUI_TEXT"; do
IFS=: read -r kind name var <<<"$pair"
path="$UNIT_DIR/$name"
[ ! -L "$path" ] || die "refusing: $path is a symlink"
[ -e "$path" ] || continue
[ -f "$path" ] || die "refusing: $path is not a regular file"
text="$(cat -- "$path")"
old="$(printf '%s\n' "$text" | settings | awk -F'\t' -v c="$COPIES/" '$2 == "WorkingDirectory" && index($3, c) == 1 { print substr($3, length(c) + 1) }')"
[[ "$old" =~ ^[0-9a-f]{40}$ ]] || die "installed $path names no copy under $COPIES"
check_unit "$kind" "$text" "installed $path" "$COPIES/$old"
[ "$text" = "$(printf '%s\n' "${!var}" | sed "s|$COPY|$COPIES/$old|g")" ] \
|| die "installed $path differs from the render in more than the copy id; the node path, serve flags, --pm and --claude must equal the installed ones"
installed_ids="$installed_ids $old"
done
read -r -a ids <<<"$installed_ids"
[ "${#ids[@]}" -lt 2 ] || [ "${ids[0]}" = "${ids[1]}" ] || die "the installed units name different copies, ${ids[0]} and ${ids[1]}"
# `systemctl start` leaves a running unit on the copy it started from, so
# note which units run before the write (row 195 owns the restart).
BUS_NAME="mosaic-bus@$BUSINESS.service"
running=''
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
if "$SYSTEMCTL" is-active --quiet "$name"; then running="$running $name"; fi
done
# Write each unit to a temp file in the unit directory, then rename.
write_unit() {
local name="$1" text="$2" tmp
tmp="$(mktemp "$UNIT_DIR/.case2-unit.XXXXXX")"
printf '%s\n' "$text" >"$tmp"
chmod 0644 "$tmp"
[ "$(id -u)" != 0 ] || chown root:root "$tmp"
mv -fT -- "$tmp" "$UNIT_DIR/$name"
}
write_unit "$BUS_UNIT" "$BUS_TEXT"
write_unit "$WEBUI_UNIT" "$WEBUI_TEXT"
# Enable both, then start the bus, then the Console (design 10 step 2c).
# serve calls the bus per request, so the units carry no After= or Wants=.
"$SYSTEMCTL" daemon-reload || die "systemctl daemon-reload failed; the units are written, nothing is enabled or started"
"$SYSTEMCTL" enable "$BUS_NAME" "$WEBUI_UNIT" || die "systemctl enable $BUS_NAME $WEBUI_UNIT failed; nothing is started"
"$SYSTEMCTL" start "$BUS_NAME" || die "systemctl start $BUS_NAME failed; the Console is not started"
"$SYSTEMCTL" start "$WEBUI_UNIT" || die "systemctl start $WEBUI_UNIT failed; the bus is running"
if [ -z "$running" ]; then
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID; enabled and started $BUS_NAME, then $WEBUI_UNIT"
exit 0
fi
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID and enabled both"
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
case " $running " in
*" $name "*) echo "case2-unit: $name was already running, so it is still on ${ids[0]:+copy }${ids[0]:-the copy it started from}, not copy $COPY_ID" ;;
*) echo "case2-unit: started $name on copy $COPY_ID" ;;
esac
done
echo "case2-unit: restart $BUS_NAME and $WEBUI_UNIT together to put both on copy $COPY_ID; until then they may run different copies"
@@ -0,0 +1,484 @@
#!/usr/bin/env bash
# Case 2 row C2-13a (design 9.3, 10 step 2c): render and install the two
# system units a hub runs as, [email protected] (enabled as
# mosaic-bus@<business>.service) and mosaic-webui.service, from the existing
# templates, which it doesn't change.
#
# case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH
# [--profile factory|library] [--pm] [--claude PATH]
# -- SERVE FLAGS
#
# Root runs it from a root-owned checkout of the reviewed commit outside
# <home>, never from the release, because <user> can write the release. It
# writes both units under /etc/systemd/system as root:root 0644, runs
# `systemctl daemon-reload`, enables both, then starts the bus and then the
# Console (Q189). Both come back after a boot on their own. It never links
# a unit file into place. Root only stats files under <home> and reads bounded
# stdout from a config/target query run as the stack user through sudo. Root
# never imports copy code. This adds sudo and GNU timeout to the installer
# dependencies. It renders the default instance only.
#
# The library profile takes exactly these serve flags, each once:
# --port N (1..1023) --business ID (the unit's) --public-host NAME
# --auth-header HEADER --allow-user ID --send
# --proof-header NAME --proof-file PATH
# The factory profile takes --port N (1..1023) and --business ID, and an
# optional bare --tester-socket marker. Retired tester TCP refuses. Only factory takes
# --pm and --claude.
#
# On a re-install the installed units must equal the render in everything
# but the copy id, and both must name one copy, so a re-pin or rollback
# re-renders the pair together. A unit that was already running stays on its
# old copy, since `systemctl start` leaves it alone; the script says so and
# that the bus and Console must restart together, and restarts nothing.
#
# Test seams, for packages/cli/tests/case2-unit.test.mjs only:
# CASE2_UNIT_DIR the unit directory, default /etc/systemd/system;
# the root and root-owned checks apply only there
# CASE2_SYSTEMCTL the systemctl command, default systemctl
# CASE2_GETENT the getent command, default getent
# CASE2_BUS_TEMPLATE default packages/cli/systemd/mosaic-bus.service.in
# CASE2_WEBUI_TEMPLATE default packages/webui/systemd/mosaic-webui.service.in
#
# Exit 0 done, 1 refused, 4 usage.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEFAULT_UNIT_DIR=/etc/systemd/system
UNIT_DIR="${CASE2_UNIT_DIR:-$DEFAULT_UNIT_DIR}"
SYSTEMCTL="${CASE2_SYSTEMCTL:-systemctl}"
GETENT="${CASE2_GETENT:-getent}"
BUS_TEMPLATE="${CASE2_BUS_TEMPLATE:-$REPO/packages/cli/systemd/mosaic-bus.service.in}"
WEBUI_TEMPLATE="${CASE2_WEBUI_TEMPLATE:-$REPO/packages/webui/systemd/mosaic-webui.service.in}"
[email protected]
WEBUI_UNIT=mosaic-webui.service
usage() { echo 'usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS' >&2; exit 4; }
die() { echo "case2-unit: $*" >&2; exit 1; }
# A value the unit carries as itself: no space, %, $, quote, backslash, & or |.
plain() { case "$1" in *[!A-Za-z0-9/._+-]*) return 1 ;; esac; }
abs_plain() {
case "$1" in /*) ;; *) die "$2 must be an absolute path: $1" ;; esac
plain "$1" || die "$2 contains a character the unit cannot carry: $1"
case "$1/" in */./*|*/../*|?*//*) die "$2 must be a normalized path: $1" ;; esac
[ "$1" = / ] || [ "${1%/}" = "$1" ] || die "$2 must not end in /: $1"
}
PROFILE=library USER_NAME='' HOME_DIR='' BUSINESS='' COPY_ID='' NODE='' PM=0 CLAUDE='' CLAUDE_SET=0
while [ $# -gt 0 ]; do
case "$1" in
--profile|--user|--home|--business|--copy|--node|--claude)
[ $# -ge 2 ] || usage
case "$1" in
--profile) PROFILE="$2" ;; --user) USER_NAME="$2" ;; --home) HOME_DIR="$2" ;;
--business) BUSINESS="$2" ;; --copy) COPY_ID="$2" ;; --node) NODE="$2" ;;
--claude) CLAUDE="$2"; CLAUDE_SET=1 ;;
esac
shift 2 ;;
--pm) PM=1; shift ;;
--) shift; break ;;
*) usage ;;
esac
done
SERVE=("$@")
[ -n "$USER_NAME" ] && [ -n "$HOME_DIR" ] && [ -n "$BUSINESS" ] && [ -n "$COPY_ID" ] && [ -n "$NODE" ] || usage
case "$PROFILE" in factory|library) ;; *) die "--profile must be factory or library: $PROFILE" ;; esac
if [ "$PROFILE" = library ]; then
[ "$PM" = 0 ] || die "the library profile refuses --pm"
[ "$CLAUDE_SET" = 0 ] || die "the library profile refuses --claude"
fi
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
[ "$(id -u)" = 0 ] || die "writing $DEFAULT_UNIT_DIR needs root"
fi
# The user, its uid, primary group and home, from the passwd database.
[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "--user must be a user name: $USER_NAME"
PW="$("$GETENT" passwd "$USER_NAME")" || die "no user $USER_NAME in the passwd database"
IFS=: read -r _ _ USER_UID USER_GID _ PW_HOME _ <<<"$PW"
[[ "$USER_UID" =~ ^[0-9]+$ ]] && [ "$USER_UID" -ne 0 ] || die "--user must not be root: $USER_NAME"
abs_plain "$HOME_DIR" '--home'
[ "$HOME_DIR" = "$PW_HOME" ] || die "--home $HOME_DIR is not $USER_NAME's home in the passwd database ($PW_HOME)"
GR="$("$GETENT" group "$USER_GID")" || die "no group $USER_GID in the group database"
GROUP="${GR%%:*}"
[[ "$GROUP" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "the primary group of $USER_NAME is not a group name: $GROUP"
case "$BUSINESS" in [a-z0-9]*) ;; *) die "--business must be a business id: $BUSINESS" ;; esac
case "$BUSINESS" in *[!a-z0-9._-]*) die "--business must be a business id: $BUSINESS" ;; esac
[ "${#BUSINESS}" -le 64 ] || die "--business must be a business id: $BUSINESS"
# The copy: copy_root's rule (scripts/stack-copy.sh:8, :22), under <home>.
COPIES="$HOME_DIR/.local/share/mosaic-stack/copies"
[[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] || die "--copy must be a full 40-character lower-case commit id: $COPY_ID"
COPY="$COPIES/$COPY_ID"
[ -d "$COPY" ] && [ ! -L "$COPY" ] || die "no copy directory $COPY"
[ -f "$COPY/scripts/mosaic" ] || die "no copy $COPY_ID in $COPIES: $COPY/scripts/mosaic is missing"
# Owned by root and writable by no one else, the file and every directory above it.
root_only() {
local p="$1" what="$2" owner mode
while :; do
owner="$(stat -c '%u' -- "$p")" || die "cannot stat $p"
mode="$(stat -c '%a' -- "$p")" || die "cannot stat $p"
[ "$owner" = 0 ] || die "$what: $p is not owned by root"
(( (8#$mode & 8#022) == 0 )) || die "$what: $p is writable by its group or others"
[ "$p" = / ] && break
p="$(dirname -- "$p")"
done
}
# --node: an absolute path to a root-owned regular file outside <home>.
abs_plain "$NODE" '--node'
case "$NODE/" in "$HOME_DIR"/*) die "--node must be outside $HOME_DIR: $NODE" ;; esac
[ ! -L "$NODE" ] || die "--node must not be a symlink; pass its target: $NODE"
[ -f "$NODE" ] && [ -x "$NODE" ] || die "--node must be an executable regular file: $NODE"
root_only "$NODE" '--node'
# --claude follows the --node rule, so nothing running as <user> can replace
# the binary the bus runs.
if [ "$CLAUDE_SET" = 1 ]; then
abs_plain "$CLAUDE" '--claude'
case "$CLAUDE/" in "$HOME_DIR"/*) die "--claude must be outside $HOME_DIR: $CLAUDE" ;; esac
[ ! -L "$CLAUDE" ] || die "--claude must not be a symlink; pass its target: $CLAUDE"
[ -f "$CLAUDE" ] && [ -x "$CLAUDE" ] || die "--claude must be an executable regular file: $CLAUDE"
root_only "$CLAUDE" '--claude'
fi
# The checkout the templates come from, when installing for real.
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
case "$REPO/" in "$HOME_DIR"/*) die "run this from a root-owned checkout outside $HOME_DIR, not $REPO" ;; esac
root_only "$BUS_TEMPLATE" 'bus template'
root_only "$WEBUI_TEMPLATE" 'Console template'
fi
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
tester_capture() (
local seconds="$1" maximum="$2" file child="" rc=0; shift 2
file="$(mktemp)" || exit 1
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
trap 'exit 1' HUP INT TERM
timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
[ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# The serve flags, checked as scripts/webui-service.sh serve_flags checks
# them (:201-264), and kept in the order given. That script compares the
# proof file's owner to `id -u`, which is root here, so this copy has its own.
serve_flags() {
local -A seen=()
local flags='' name value
local port='' public_host='' auth_header='' allow_user='' proof_header='' proof_file='' tester_socket=0
local i=0 n=${#SERVE[@]}
while [ "$i" -lt "$n" ]; do
name="${SERVE[$i]}"
case "$name" in
--tester-port) die "case2-unit.sh retires tester TCP; use --tester-socket" ;;
--tester-socket) [ "$PROFILE" = factory ] || die "the library profile refuses --tester-socket" ;;
--checkout) die "refusing --checkout: Case 2 has no queue panel" ;;
--board) die "refusing --board: Case 2 runs no control board" ;;
--sso) die "refusing --sso: the Console signs people in through the proxy, with the preview set and the proof pair (Q175 phase 2 is a follow-up)" ;;
--port|--business) ;;
--public-host|--auth-header|--allow-user|--proof-header|--proof-file|--send)
[ "$PROFILE" = library ] || die "the factory profile refuses $name: its host has no proxy" ;;
*) die "unknown serve flag: $name" ;;
esac
[ -z "${seen[$name]:-}" ] || die "serve flag $name given twice"
seen[$name]=1
if [ "$name" = --tester-socket ]; then tester_socket=1; flags="$flags --tester-socket"; i=$((i + 1)); continue; fi
if [ "$name" = --send ]; then flags="$flags --send"; i=$((i + 1)); continue; fi
[ $((i + 1)) -lt "$n" ] || die "serve flag $name needs a value"
value="${SERVE[$((i + 1))]}"
case "$name" in
--port) port="$value"
[[ "$port" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$port" -le 1023 ] || die "--port must be 1..1023, so only the unit's capability can bind it: $port" ;;
--business) [ "$value" = "$BUSINESS" ] || die "serve --business $value is not the unit's business $BUSINESS" ;;
--public-host) public_host="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "public host must be a hostname, no port or scheme: $value" ;; esac
case "$value" in *[!A-Za-z0-9.-]*) die "public host must be a hostname, no port or scheme: $value" ;; esac ;;
--auth-header) auth_header="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "auth header must be a header name: $value" ;; esac
case "$value" in *[!A-Za-z0-9-]*) die "auth header must be a header name: $value" ;; esac ;;
--allow-user) allow_user="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "allowed user must start with a letter or digit: $value" ;; esac
case "$value" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $value" ;; esac ;;
--proof-header) proof_header="$value"
[[ "$value" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,63}$ ]] || die "invalid proxy proof header" ;;
--proof-file) proof_file="$value" ;;
esac
flags="$flags $name $value"
i=$((i + 2))
done
local required=(--port --business)
[ "$PROFILE" = factory ] || required+=(--public-host --auth-header --allow-user --send --proof-header --proof-file)
for name in "${required[@]}"; do
[ -n "${seen[$name]:-}" ] || die "serve flag $name is required"
done
if [ "$PROFILE" = factory ]; then
if [ "$tester_socket" = 1 ]; then
local query
query="$(tester_capture 5 4096 sudo -u "$USER_NAME" -H env -i "HOME=$HOME_DIR" "PATH=$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query failed"
tester_facts "$query" || die "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || die "the target does not support --tester-socket"
tester_location "$TS_DATA" "$HOME_DIR" "$USER_UID" "" || die "tester path refuses $TS_REASON"
fi
echo "${flags# }"; return 0
fi
# Lead decision 101: the uid header carries the Authentik uid.
if [ "${auth_header,,}" = x-authentik-uid ] && ! [[ "$allow_user" =~ ^[0-9a-f]{64}$ ]]; then
die "with --auth-header $auth_header the allowed user must be an Authentik uid, 64 lower-case hex characters (lead decision 101): $allow_user"
fi
case "${proof_header,,}" in host|origin|x-mosaic-tester|"${auth_header,,}") die "proxy proof header must differ from Host, Origin, tester and auth headers" ;; esac
# The proof file: metadata only, never opened (design 9.3).
local secrets="$HOME_DIR/.config/mosaic-dev/secrets" real owner mode
abs_plain "$proof_file" 'proxy proof file'
[ ! -L "$proof_file" ] || die "proxy proof file must not be a symlink: $proof_file"
[ -f "$proof_file" ] || die "proxy proof file must be a regular file: $proof_file"
real="$(realpath -e -- "$proof_file")" || die "cannot resolve proxy proof file: $proof_file"
[ "$real" = "$proof_file" ] || die "proxy proof file path must have no symlink in it: $proof_file"
case "$proof_file" in "$secrets"/*) ;; *) die "proxy proof file must be under $secrets, outside the data root: $proof_file" ;; esac
owner="$(stat -c '%u' -- "$proof_file")" || die "cannot stat proxy proof file"
mode="$(stat -c '%a' -- "$proof_file")" || die "cannot stat proxy proof file"
[ "$owner" = "$USER_UID" ] || die "proxy proof file must be owned by $USER_NAME (uid $USER_UID): $proof_file"
[ "$mode" = 600 ] || [ "$mode" = 400 ] || die "proxy proof file must be mode 0600 or 0400, not $mode: $proof_file"
echo "${flags# }"
}
# PATH: the node's directory first, then the default list without it. Unlike
# scripts/webui-service.sh:303-305 this always puts node's directory first,
# so the bus's `exec node` (scripts/mosaic) finds the same node.
NODE_DIR="$(dirname -- "$NODE")"
UNIT_PATH="$NODE_DIR"
for d in /usr/local/bin /usr/bin /bin; do [ "$d" = "$NODE_DIR" ] || UNIT_PATH="$UNIT_PATH:$d"; done
FLAGS="$(serve_flags)" || exit 1
HARDENING="ProtectSystem=strict
ReadWritePaths=$HOME_DIR
ProtectHome=yes
PrivateTmp=yes"
CAPS="AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE"
# The default instance, as scripts/instance.sh:85-107 renders it: no
# MOSAIC_CONFIG line, and the session env under %h, which becomes <home>.
SESSION_ENV='%h/.config/mosaic-dev/secrets/claude-session.env'
ENV_FILE="-$HOME_DIR/.config/mosaic-dev/secrets/claude-session.env"
PM_FLAGS=''
[ "$PM" = 0 ] || PM_FLAGS=' --pm'
CLAUDE_ENV=''
[ "$CLAUDE_SET" = 0 ] || CLAUDE_ENV="Environment=MOSAIC_CLAUDE_BIN=$CLAUDE"
# One unit from its template. kind is bus or webui.
render() {
local kind="$1" template="$2" text tokens
[ -f "$template" ] && [ ! -L "$template" ] || die "template missing or not a regular file: $template"
text="$(sed -e "s|@REPO@|$COPY|g" -e "s|@PATH@|$UNIT_PATH|g" -e "s|@NODE@|$NODE|g" \
-e "s|@PM_FLAGS@|$PM_FLAGS|g" -e "s|^@CLAUDE_ENV@\$|$CLAUDE_ENV|" \
-e '/^@CONFIG_ENV@$/d' -e "s|@SESSION_ENV@|$SESSION_ENV|g" \
-e "s|%h|$HOME_DIR|g" -- "$template")"
# Checked before @FLAGS@ goes in, since an allowed user may hold @.
tokens="$(printf '%s\n' "$text" | grep -oE '@[A-Z_]+@' || true)"
if [ "$kind" = webui ]; then
[ "$tokens" = @FLAGS@ ] || die "$template: a token other than one @FLAGS@ is left after the render: $(echo $tokens)"
text="${text//@FLAGS@/$FLAGS}"
else
[ -z "$tokens" ] || die "$template: a token is left after the render: $(echo $tokens)"
fi
if printf '%s\n' "$text" | grep -q '%h'; then die "$template: %h is left after the render"; fi
local add="User=$USER_NAME\nGroup=$GROUP\n${HARDENING//$'\n'/\\n}\n"
[ "$kind" = bus ] || add="$add${CAPS//$'\n'/\\n}\n"
# The added lines go at the end of [Service], before its trailing blank
# lines. In [Install], WantedBy=multi-user.target replaces
# WantedBy=default.target in both units, so both start at boot (Q189).
printf '%s\n' "$text" | awk -v add="$add" '
function blanks_out() { for (; blanks > 0; blanks--) print "" }
/^\[.*\]$/ {
if (sec == "[Service]") printf "%s", add
sec = $0
blanks_out(); print; next
}
$0 == "" { blanks++; next }
{ blanks_out() }
sec == "[Install]" && $0 == "WantedBy=default.target" { $0 = "WantedBy=multi-user.target" }
{ print }
END { if (sec == "[Service]") printf "%s", add; blanks_out() }'
}
# The unit's settings as section<TAB>key<TAB>value, one per line, keys
# trimmed as systemd trims them. A continued line refuses, so no setting
# hides in one.
settings() {
awk '
{ sub(/^[ \t]+/, ""); sub(/[ \t]+$/, "") }
$0 == "" || /^[#;]/ { next }
/\\$/ { print "CONTINUED"; exit }
/^\[.*\]$/ { sec = $0; next }
{
i = index($0, "="); if (i == 0) { print "NOKEY\t" $0; next }
k = substr($0, 1, i - 1); v = substr($0, i + 1)
sub(/[ \t]+$/, "", k); sub(/^[ \t]+/, "", v)
print sec "\t" k "\t" v
}'
}
# Refuses unless one unit's fixed lines hold. what names the unit in messages,
# copy is the copy directory it should run from.
check_unit() {
local kind="$1" text="$2" what="$3" copy="$4" s
s="$(printf '%s\n' "$text" | settings)"
! printf '%s\n' "$s" | grep -qx CONTINUED || die "$what: a line continues onto the next"
! printf '%s\n' "$s" | grep -q '^NOKEY' || die "$what: a line is neither a section, a comment nor a setting"
# %h in a system unit is root's home, not <user>'s.
! printf '%s\n' "$text" | grep -q '%h' || die "$what: %h is left; a system unit reads it as root's home"
local key
for key in RestrictNamespaces PrivateUsers SystemCallFilter PermissionsStartOnly; do
! printf '%s\n' "$s" | awk -F'\t' -v k="$key" '$2 == k { found = 1 } END { exit !found }' \
|| die "$what: refusing $key=, which the hub's units must not hold"
done
# An Exec*= value whose leading prefix characters hold + or ! runs with
# full privileges, whatever User= says.
local bad
bad="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^Exec/ { p = $3; sub(/[^-@:+!].*$/, "", p); if (p ~ /[+!]/) print $2 "=" $3 }')"
[ -z "$bad" ] || die "$what: refusing an Exec line that runs with full privileges: $bad"
# The fixed lines, each exactly once, in [Service].
one() {
local k="$1" v="$2" got
got="$(printf '%s\n' "$s" | awk -F'\t' -v k="$k" '$2 == k { print $1 "\t" $3 }')"
[ "$got" = "[Service] $v" ] || die "$what: $k= must be exactly once in [Service], as $k=$v"
}
none() {
! printf '%s\n' "$s" | awk -F'\t' -v k="$1" '$2 == k { found = 1 } END { exit !found }' || die "$what: refusing $1=, which this unit must not hold"
}
one User "$USER_NAME"
one Group "$GROUP"
one WorkingDirectory "$copy"
# Environment= lines: PATH, and MOSAIC_CLAUDE_BIN only with --claude.
local envs want_envs="[Service] PATH=$UNIT_PATH"
[ "$kind" = webui ] || [ -z "$CLAUDE_ENV" ] || want_envs="$want_envs
[Service] MOSAIC_CLAUDE_BIN=$CLAUDE"
envs="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "Environment" { print $1 "\t" $3 }')"
[ "$envs" = "$want_envs" ] || die "$what: the Environment= lines must be exactly: $(printf '%s' "$want_envs" | cut -f2 | tr '\n' ' ')"
one NoNewPrivileges yes
one ProtectSystem strict
one ReadWritePaths "$HOME_DIR"
one ProtectHome yes
one PrivateTmp yes
local order
order="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)$/ { printf "%s ", $2 }')"
[ "$order" = "ProtectSystem ReadWritePaths ProtectHome PrivateTmp " ] || die "$what: the hardening lines must be in the order ProtectSystem, ReadWritePaths, ProtectHome, PrivateTmp"
local wanted
wanted="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "WantedBy" { print $1 "\t" $3 }')"
[ "$wanted" = "[Install] multi-user.target" ] || die "$what: WantedBy= must be exactly once, as WantedBy=multi-user.target"
if [ "$kind" = bus ]; then
one ExecStart "$copy/scripts/mosaic bus start %i$PM_FLAGS"
one EnvironmentFile "$ENV_FILE"
none AmbientCapabilities
none CapabilityBoundingSet
else
one ExecStart "$NODE $copy/packages/webui/src/cli.mjs serve $FLAGS"
none EnvironmentFile
one AmbientCapabilities CAP_NET_BIND_SERVICE
one CapabilityBoundingSet CAP_NET_BIND_SERVICE
fi
}
BUS_TEXT="$(render bus "$BUS_TEMPLATE")" || exit 1
WEBUI_TEXT="$(render webui "$WEBUI_TEMPLATE")" || exit 1
check_unit bus "$BUS_TEXT" "rendered $BUS_UNIT" "$COPY"
check_unit webui "$WEBUI_TEXT" "rendered $WEBUI_UNIT" "$COPY"
# A re-install: each installed unit passes the same checks, both name one
# copy, and each equals the render once its copy id is the new one.
[ -d "$UNIT_DIR" ] && [ ! -L "$UNIT_DIR" ] || die "no unit directory $UNIT_DIR"
installed_ids=''
for pair in "bus:$BUS_UNIT:BUS_TEXT" "webui:$WEBUI_UNIT:WEBUI_TEXT"; do
IFS=: read -r kind name var <<<"$pair"
path="$UNIT_DIR/$name"
[ ! -L "$path" ] || die "refusing: $path is a symlink"
[ -e "$path" ] || continue
[ -f "$path" ] || die "refusing: $path is not a regular file"
text="$(cat -- "$path")"
old="$(printf '%s\n' "$text" | settings | awk -F'\t' -v c="$COPIES/" '$2 == "WorkingDirectory" && index($3, c) == 1 { print substr($3, length(c) + 1) }')"
[[ "$old" =~ ^[0-9a-f]{40}$ ]] || die "installed $path names no copy under $COPIES"
check_unit "$kind" "$text" "installed $path" "$COPIES/$old"
[ "$text" = "$(printf '%s\n' "${!var}" | sed "s|$COPY|$COPIES/$old|g")" ] \
|| die "installed $path differs from the render in more than the copy id; the node path, serve flags, --pm and --claude must equal the installed ones"
installed_ids="$installed_ids $old"
done
read -r -a ids <<<"$installed_ids"
[ "${#ids[@]}" -lt 2 ] || [ "${ids[0]}" = "${ids[1]}" ] || die "the installed units name different copies, ${ids[0]} and ${ids[1]}"
# `systemctl start` leaves a running unit on the copy it started from, so
# note which units run before the write (row 195 owns the restart).
BUS_NAME="mosaic-bus@$BUSINESS.service"
running=''
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
if "$SYSTEMCTL" is-active --quiet "$name"; then running="$running $name"; fi
done
# Write each unit to a temp file in the unit directory, then rename.
write_unit() {
local name="$1" text="$2" tmp
tmp="$(mktemp "$UNIT_DIR/.case2-unit.XXXXXX")"
printf '%s\n' "$text" >"$tmp"
chmod 0644 "$tmp"
[ "$(id -u)" != 0 ] || chown root:root "$tmp"
mv -fT -- "$tmp" "$UNIT_DIR/$name"
}
write_unit "$BUS_UNIT" "$BUS_TEXT"
write_unit "$WEBUI_UNIT" "$WEBUI_TEXT"
# Enable both, then start the bus, then the Console (design 10 step 2c).
# serve calls the bus per request, so the units carry no After= or Wants=.
"$SYSTEMCTL" daemon-reload || die "systemctl daemon-reload failed; the units are written, nothing is enabled or started"
"$SYSTEMCTL" enable "$BUS_NAME" "$WEBUI_UNIT" || die "systemctl enable $BUS_NAME $WEBUI_UNIT failed; nothing is started"
"$SYSTEMCTL" start "$BUS_NAME" || die "systemctl start $BUS_NAME failed; the Console is not started"
"$SYSTEMCTL" start "$WEBUI_UNIT" || die "systemctl start $WEBUI_UNIT failed; the bus is running"
if [ -z "$running" ]; then
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID; enabled and started $BUS_NAME, then $WEBUI_UNIT"
exit 0
fi
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID and enabled both"
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
case " $running " in
*" $name "*) echo "case2-unit: $name was already running, so it is still on ${ids[0]:+copy }${ids[0]:-the copy it started from}, not copy $COPY_ID" ;;
*) echo "case2-unit: started $name on copy $COPY_ID" ;;
esac
done
echo "case2-unit: restart $BUS_NAME and $WEBUI_UNIT together to put both on copy $COPY_ID; until then they may run different copies"
@@ -0,0 +1,850 @@
// Case 2 row C2-13a (#1732): scripts/case2-unit.sh renders and installs the hub's two system units,
// and scripts/case2-precheck.sh checks the host before they start. Brief: docs/plans/2026-10-20_case2-rows.md
// "C2-13a". Every case runs against a scratch unit directory, a scratch home and fake systemctl,
// getent, sudo, id, node, bwrap and claude. Nothing here reads or writes /etc/systemd,
// ~/.mosaic-dev or ~/.config/mosaic-dev, and no fixture value is a real credential.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createServer } from "node:net";
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { businessDoc, writeJson } from "../../business/tests/helpers.mjs";
import { binds, prepareRun, sandboxArgv, sandboxProblem } from "../../seat/src/sandbox.mjs";
import { tmp } from "./helpers.mjs";
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const UNIT = join(REPO, "scripts", "case2-unit.sh");
const PRECHECK = join(REPO, "scripts", "case2-precheck.sh");
const BUS_TEMPLATE = join(REPO, "packages", "cli", "systemd", "mosaic-bus.service.in");
const WEBUI_TEMPLATE = join(REPO, "packages", "webui", "systemd", "mosaic-webui.service.in");
const COPY_A = "a".repeat(40);
const COPY_B = "b".repeat(40);
const UID = process.getuid();
const GID = process.getgid();
// A made-up key value; no output may hold it.
const FIXTURE_KEY = "fixture-zai-key-3c9e51";
// A root-owned executable outside /usr/local/bin, /usr/bin and /bin, for the PATH case.
function outsideNode() {
const dir = "/usr/lib/systemd";
for (const name of readdirSync(dir).sort()) {
const p = join(dir, name);
const s = statSync(p, { throwIfNoEntry: false });
if (s?.isFile() && s.uid === 0 && (s.mode & 0o111) && (s.mode & 0o022) === 0 && realpathSync(p) === p) return p;
}
throw new Error(`no root-owned executable in ${dir}`);
}
const USR_NODE = realpathSync("/usr/bin/node");
const TRUE_BIN = realpathSync("/usr/bin/true");
function script(file, text) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, `#!/usr/bin/env bash\n${text}\n`);
chmodSync(file, 0o755);
return file;
}
// A scratch hub: a home with two copies and a proof file, a unit directory, and fakes.
function hub(t, { uid = UID } = {}) {
const root = tmp(t, "case2-unit-");
const home = join(root, "home", "mosaic");
for (const id of [COPY_A, COPY_B]) {
mkdirSync(join(home, ".local", "share", "mosaic-stack", "copies", id, "scripts"), { recursive: true });
writeFileSync(join(home, ".local", "share", "mosaic-stack", "copies", id, "scripts", "mosaic"), "");
}
const secrets = join(home, ".config", "mosaic-dev", "secrets");
mkdirSync(secrets, { recursive: true, mode: 0o700 });
const proof = join(secrets, "proxy-proof");
writeFileSync(proof, "made-up-proof\n");
chmodSync(proof, 0o600);
const units = join(root, "units");
mkdirSync(units);
const bin = join(root, "bin");
const log = join(root, "systemctl.log");
script(join(bin, "getent"), `case "$1" in
passwd) [ "$2" = mosaic ] && echo "mosaic:x:${uid}:${GID}::${home}:/bin/bash" ;;
group) [ "$2" = "${GID}" ] && echo "mosaic:x:${GID}:" ;;
*) exit 2 ;;
esac`);
// is-active answers yes for each unit named in CASE2_TEST_ACTIVE.
script(join(bin, "systemctl"), `echo "$*" >>'${log}'
if [ "$1" = is-active ]; then case " \${CASE2_TEST_ACTIVE:-} " in *" $3 "*) exit 0 ;; *) exit 3 ;; esac; fi
[ "$*" != "\${CASE2_TEST_FAIL:-}" ]`);
const dataRoot = join(home, '.mosaic-dev'), query = join(root, 'query.json'), sudoLog = join(root, 'sudo.log');
mkdirSync(dataRoot, { recursive: true });
writeJson(query, { dataRoot });
for (const copy of [COPY_A, COPY_B]) {
const src = join(home, '.local/share/mosaic-stack/copies', copy, 'packages');
mkdirSync(join(src, 'cli/src'), { recursive: true }); mkdirSync(join(src, 'webui/src'), { recursive: true });
writeFileSync(join(src, 'cli/src/config.mjs'), `import fs from 'node:fs';export const loadSystem=()=>JSON.parse(fs.readFileSync(${JSON.stringify(query)},'utf8'));`);
writeFileSync(join(src, 'webui/src/cli.mjs'), '// MOSAIC_TESTER_SOCKET_V1\n');
}
script(join(bin, 'sudo'), `${CASE_SUDO_SESSION}
echo "$*" >>'${sudoLog}'
[ "$1" = -u ] && [ "$2" = mosaic ] && [ "$3" = -H ] || exit 97
shift 3
[ "$1" = env ] && [ "$2" = -i ] && [ "$3" = 'HOME=${home}' ] || exit 98
if [ -f '${root}/query-hang' ]; then
trap '' TERM
(trap '' TERM; echo "$BASHPID" >>'${root}/query-pids'; exec sleep 60) &
echo "$BASHPID" >>'${root}/query-pids'; wait; exit
fi
if [ -f '${root}/query-orphan' ]; then
(trap '' TERM; here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here"); printf '%s %s\\n' "$here" "$sid" >'${root}/query-owned'; exec sleep 60) </dev/null >/dev/null 2>&1 &
echo "$!" >'${root}/query-pids'
cat '${root}/query-output'; exit 0
fi
if [ -f '${root}/query-output' ]; then cat '${root}/query-output'; exit "$(cat '${root}/query-status')"; fi
case_sudo_run '${root}' "$@"`);
return { root, home, secrets, proof, units, bin, log, dataRoot, query, sudoLog };
}
const libraryFlags = (h) => ["--port", "443", "--business", "acme", "--public-host", "books.example", "--auth-header", "X-Auth-User",
"--allow-user", "melanie", "--send", "--proof-header", "X-Proxy-Proof", "--proof-file", h.proof];
const factoryFlags = () => ["--port", "443", "--business", "acme", "--tester-socket"];
function unit(h, { profile = "library", node = USR_NODE, copy = COPY_A, extra = [], flags, env = {} } = {}) {
const serve = flags ?? (profile === "factory" ? factoryFlags() : libraryFlags(h));
const args = ["--user", "mosaic", "--home", h.home, "--business", "acme", "--copy", copy, "--node", node, "--profile", profile, ...extra, "--", ...serve];
const r = spawnSync(UNIT, args, {
encoding: "utf8",
timeout: 30000,
env: { PATH: `${h.bin}:${process.env.PATH}`, CASE2_UNIT_DIR: h.units, CASE2_SYSTEMCTL: join(h.bin, "systemctl"), CASE2_GETENT: join(h.bin, "getent"), ...env },
});
return { code: r.status, out: r.stdout, err: r.stderr };
}
const busFile = (h) => join(h.units, "[email protected]");
const webuiFile = (h) => join(h.units, "mosaic-webui.service");
const read = (f) => readFileSync(f, "utf8");
const lines = (text, key) => text.split("\n").filter((l) => l.startsWith(`${key}=`)).map((l) => l.slice(key.length + 1));
const copyDir = (h, id = COPY_A) => join(h.home, ".local", "share", "mosaic-stack", "copies", id);
const HARDENING = (h) => ["ProtectSystem=strict", `ReadWritePaths=${h.home}`, "ProtectHome=yes", "PrivateTmp=yes"];
test('row253 factory socket uses actual config and target-uid query without widening hardening', t=>{
const h=hub(t),custom=join(h.home,'custom');mkdirSync(custom);writeJson(h.query,{dataRoot:custom});
const r=unit(h,{profile:'factory'});assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(lines(read(webuiFile(h)),'ExecStart'),[`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${factoryFlags().join(' ')}`]);
assert.deepEqual(read(webuiFile(h)).split('\n').filter(l=>/^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)=/.test(l)),HARDENING(h));
assert.match(read(h.sudoLog),new RegExp(`^-u mosaic -H env -i HOME=${h.home} PATH=`, 'm'));
assert.equal(existsSync(join(custom,'console')),false,'installer never creates the socket parent');
});
test('row253 installer refuses unsafe location and query facts before unit writes',t=>{
const cases=[
['sibling prefix',h=>writeJson(h.query,{dataRoot:h.home+'2/data'}),/outside.*home/],
['dot',h=>writeJson(h.query,{dataRoot:h.home+'/./data'}),/normalized/],
['dotdot',h=>writeJson(h.query,{dataRoot:h.home+'/x/../data'}),/normalized/],
['double slash',h=>writeJson(h.query,{dataRoot:h.home+'//data'}),/normalized/],
['trailing slash',h=>writeJson(h.query,{dataRoot:h.home+'/data/'}),/normalized/],
['newline',h=>writeJson(h.query,{dataRoot:h.home+'/x\nP10 forged closed'}),/query/],
['non ASCII',h=>writeJson(h.query,{dataRoot:h.home+'/é'}),/query/],
['middle symlink',h=>{symlinkSync(h.dataRoot,join(h.home,'link'));writeJson(h.query,{dataRoot:join(h.home,'link','nested')});},/symlink/],
['long path',h=>writeJson(h.query,{dataRoot:join(h.home,'a'.repeat(108))}),/too.long/],
['parent file',h=>writeFileSync(join(h.dataRoot,'console'),'x',{mode:0o600}),/parent-type/],
['parent symlink',h=>symlinkSync(h.home,join(h.dataRoot,'console')),/symlink/],
['parent mode',h=>{mkdirSync(join(h.dataRoot,'console'));chmodSync(join(h.dataRoot,'console'),0o755);},/parent/],
['foreign parent',h=>{mkdirSync(join(h.dataRoot,'console'),{mode:0o700});},/parent/],
['unsupported',h=>writeFileSync(join(copyDir(h),'packages/webui/src/cli.mjs'),'// old copy'),/support/],
['schema',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true,extra:true}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['wrong type',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:'true'}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['noisy query',h=>{writeFileSync(join(h.root,'query-output'),'noise\n'+JSON.stringify({dataRoot:h.dataRoot,socket:true}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['failed valid query',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));writeFileSync(join(h.root,'query-status'),'1');},/query/],
];
for(const [name,change,why] of cases){
const h=hub(t,{uid:name==='foreign parent'?UID+4242:UID});change(h);
const r=unit(h,{profile:'factory'});refuses(r,why);
assert.equal(existsSync(busFile(h)),false,name);assert.equal(existsSync(webuiFile(h)),false,name);assert.equal(existsSync(h.log),false,name);
assert.equal((r.out+r.err).includes('P10 forged closed'),false,name);
}
});
test('row253 query timeout kills its process group and leaves no running child',async t=>{
const h=hub(t);writeFileSync(join(h.root,'query-hang'),'');
const r=unit(h,{profile:'factory'});refuses(r,/query/);
const pids=read(join(h.root,'query-pids')).trim().split('\n').map(Number);assert.ok(pids.length>=2);
for(const pid of pids){
let state='gone';
for(let n=0;n<50;n++){
try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`query descendant ${pid} still ${state}`);
}
assert.equal(existsSync(h.log),false);assert.equal(existsSync(busFile(h)),false);
});
test('row253 protected homes refuse before any filesystem access',()=>{
const source=read(UNIT), body=source.slice(source.indexOf('tester_location() {'),source.indexOf('# tester-location-end'));
for(const home of ['/home/mosaic','/root/mosaic','/run/user/997','/tmp/mosaic','/var/tmp/mosaic']){
const r=spawnSync('/bin/bash',['-c',body+'\nif tester_location "$1/data" "$1" 997 ""; then exit 99; fi; printf "%s" "$TS_REASON"','--',home],{encoding:'utf8'});
assert.equal(r.status,0);assert.equal(r.stdout,'protected-private-home');
}
});
// C4 cases run through both sudo process models. Exec shares the runner's
// session, so its session-wide check would be vacuous; exact recorded PIDs
// are checked in both modes. Hop additionally isolates the fake session.
for(const kind of ['sleep','signals','member']) test(`row253 C4 ${kind} query has exact inner reason and cleans all members`,async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),sessions=join(h.root,'session-pids'),queries=join(h.root,'query-pids'),files=[sessions,queries];
writeFileSync(join(h.root,'session-mode'),mode);
const member=join(h.root,'q ) '+String.fromCharCode(10)+'mem'),proof=join(h.root,'comm-proof');
if(kind==='member')symlinkSync('/usr/bin/sleep',member);
writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),blockingQuerySource(queries,kind,member,proof));
const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
refuses(r,/^case2-unit: tester config query inner-timeout\n$/);
assert.ok(seconds<7,`${kind}/${mode}: ${seconds}s exceeds outer bound`);
assert.deepEqual(await pollFixtureGone(files),[],mode);
if(kind==='member')assert.ok(read(proof).includes('('+member.split('/').at(-1)+') '),'member comm with newline and closing parenthesis was observed');
assert.equal(existsSync(h.log),false,mode);await stopGuard();
}
});
test('row253 C4 overflow is output-limit in both sudo modes',async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),files=[join(h.root,'session-pids')];writeFileSync(join(h.root,'session-mode'),mode);const capture=cleanupFixture(t,files);
writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:${JSON.stringify(h.dataRoot)}});`);
const r=unit(h,{profile:'factory'});capture();refuses(r,/^case2-unit: tester config query output-limit\n$/);
assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
assert.deepEqual(await pollFixtureGone(files),[]);
}
});
test('row253 C4 stalled supervisor uses the outer reason and explicit residual cleanup',async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),file=join(h.root,'session-pids'),files=[file];writeFileSync(join(h.root,'session-mode'),mode);
writeFileSync(join(h.root,'stall-config'),'');writeFileSync(join(h.root,'stall-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
refuses(r,/^case2-unit: tester config query outer-timeout\n$/);
assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s slack includes root preflight`);
assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
if(mode==='hop')assert.ok(recordedLivePids(file).length>0,'R3 residual exists before fixture cleanup');
killRecordedPids(file);assert.deepEqual(await pollFixtureGone(files),[], 'explicit fixture cleanup of R3 residual');await stopGuard();
}
});
test('row253 C4 inline supervisor is byte-identical in both host scripts',()=>{
const extract=s=>s.split('# row253-supervisor-begin\n')[1]?.split('# row253-supervisor-end')[0];
const a=extract(read(UNIT)),b=extract(read(join(REPO,'scripts/case1-accept.sh')));
assert.ok(a?.length>100);assert.equal(a,b);
});
test('row253 query reaps its group even when a child closes stdout before the query returns',async t=>{
const h=hub(t);writeFileSync(join(h.root,'query-orphan'),'');
writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
let pid;
const capture=cleanupFixture(t,[join(h.root,'query-owned')]);
const r=unit(h,{profile:'factory'});capture();
pid=Number(read(join(h.root,'query-pids')).trim());
assert.equal(r.code,0,r.out+r.err);
let state='gone';
for(let n=0;n<50;n++){
try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`query descendant ${pid} still ${state}`);
});
test('row253 config parser is strict',()=>{
const source=read(UNIT), body=source.slice(source.indexOf('tester_facts() {'),source.indexOf('tester_location() {'));
const valid=spawnSync('/bin/bash',['-c',body+'\ntester_facts "$1" && printf "%s %s" "$TS_DATA" "$TS_MARKED"','--','{"dataRoot":"/srv/mosaic/data","socket":true}'],{encoding:'utf8'});
assert.equal(valid.status,0);assert.equal(valid.stdout,'/srv/mosaic/data true');
const facts=['{"dataRoot":"/srv/mosaic/data","socket":true,"extra":1}','{"dataRoot":4,"socket":true}','{"dataRoot":"/srv/mosaic/data","socket":"true"}','{"dataRoot":"/srv/mosaic/data","socket":true}\nP10 forged closed'];
for(const fact of facts){
const r=spawnSync('/bin/bash',['-c',body+'\ntester_facts "$1"','--',fact]);
assert.notEqual(r.status,0,fact);
}
});
function refuses(r, pattern) {
assert.equal(r.code, 1, `expected a refusal, got ${r.code}: ${r.out}${r.err}`);
assert.match(r.err, pattern);
}
// A template copy with one edit, for the template cases.
function template(h, from, name, edit) {
const file = join(h.root, name);
writeFileSync(file, edit(read(from)));
return file;
}
test("a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console", (t) => {
const h = hub(t);
const r = unit(h);
assert.equal(r.code, 0, r.err);
for (const f of [busFile(h), webuiFile(h)]) assert.equal(statSync(f).mode & 0o777, 0o644);
const bus = read(busFile(h));
const webui = read(webuiFile(h));
assert.deepEqual(lines(bus, "Environment"), ["PATH=/usr/bin:/usr/local/bin:/bin"]);
assert.deepEqual(lines(bus, "ExecStart"), [`${copyDir(h)}/scripts/mosaic bus start %i`]);
assert.deepEqual(lines(bus, "EnvironmentFile"), [`-${h.home}/.config/mosaic-dev/secrets/claude-session.env`]);
assert.deepEqual(lines(webui, "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${libraryFlags(h).join(" ")}`]);
assert.deepEqual(lines(webui, "EnvironmentFile"), []);
for (const text of [bus, webui]) {
assert.deepEqual(lines(text, "WantedBy"), ["multi-user.target"]);
assert.deepEqual(lines(text, "User"), ["mosaic"]);
assert.deepEqual(lines(text, "Group"), ["mosaic"]);
assert.deepEqual(lines(text, "NoNewPrivileges"), ["yes"]);
const hardening = text.split("\n").filter((l) => /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)=/.test(l));
assert.deepEqual(hardening, HARDENING(h));
assert.doesNotMatch(text, /@[A-Z_]+@|%h|MOSAIC_CONFIG=|MOSAIC_CLAUDE_BIN|After=|Wants=/);
}
assert.deepEqual(lines(bus, "AmbientCapabilities"), []);
assert.deepEqual(lines(bus, "CapabilityBoundingSet"), []);
assert.deepEqual(lines(webui, "AmbientCapabilities"), ["CAP_NET_BIND_SERVICE"]);
assert.deepEqual(lines(webui, "CapabilityBoundingSet"), ["CAP_NET_BIND_SERVICE"]);
assert.match(r.out, /enabled and started mosaic-bus@acme\.service, then mosaic-webui\.service\n$/);
assert.deepEqual(read(h.log).trim().split("\n"), [
"is-active --quiet [email protected]",
"is-active --quiet mosaic-webui.service",
"daemon-reload",
"enable [email protected] mosaic-webui.service",
"start [email protected]",
"start mosaic-webui.service",
]);
});
test("PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node", (t) => {
const h = hub(t);
const node = outsideNode();
assert.equal(unit(h, { node }).code, 0);
for (const f of [busFile(h), webuiFile(h)]) assert.deepEqual(lines(read(f), "Environment"), [`PATH=${dirname(node)}:/usr/local/bin:/usr/bin:/bin`]);
assert.match(lines(read(webuiFile(h)), "ExecStart")[0], new RegExp(`^${node} `));
});
test("a failed bus start refuses with the Console not started", (t) => {
const h = hub(t);
const r = unit(h, { env: { CASE2_TEST_FAIL: "start [email protected]" } });
refuses(r, /start mosaic-bus@acme\.service failed; the Console is not started/);
assert.deepEqual(read(h.log).trim().split("\n").slice(2), ["daemon-reload", "enable [email protected] mosaic-webui.service", "start [email protected]"]);
});
test("the script never links a unit file", () => {
assert.doesNotMatch(read(UNIT), /systemctl link/);
});
test("factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty", (t) => {
const h = hub(t);
assert.equal(unit(h, { profile: "factory", extra: ["--pm", "--claude", TRUE_BIN] }).code, 0);
const bus = read(busFile(h));
const exec = lines(bus, "ExecStart");
assert.equal(exec.length, 1);
assert.match(exec[0], / bus start %i --pm$/);
assert.equal(exec[0].split("--pm").length, 2);
assert.deepEqual(lines(bus, "Environment"), ["PATH=/usr/bin:/usr/local/bin:/bin", `MOSAIC_CLAUDE_BIN=${TRUE_BIN}`]);
assert.deepEqual(lines(read(webuiFile(h)), "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${factoryFlags().join(" ")}`]);
const plain = hub(t);
assert.equal(unit(plain, { profile: "factory" }).code, 0);
const text = read(busFile(plain));
assert.deepEqual(lines(text, "ExecStart"), [`${copyDir(plain)}/scripts/mosaic bus start %i`]);
assert.doesNotMatch(text, /MOSAIC_CLAUDE_BIN|@PM_FLAGS@|@CLAUDE_ENV@/);
});
test("a re-install over running units says they are still on the old copy and must restart together, never that it started them", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
const both = unit(h, { copy: COPY_B, env: { CASE2_TEST_ACTIVE: "[email protected] mosaic-webui.service" } });
assert.equal(both.code, 0, both.err);
assert.doesNotMatch(both.out, /started/);
assert.match(both.out, new RegExp(`mosaic-bus@acme\\.service was already running, so it is still on copy ${COPY_A}, not copy ${COPY_B}`));
assert.match(both.out, new RegExp(`mosaic-webui\\.service was already running, so it is still on copy ${COPY_A}, not copy ${COPY_B}`));
assert.match(both.out, new RegExp(`restart mosaic-bus@acme\\.service and mosaic-webui\\.service together to put both on copy ${COPY_B}`));
assert.deepEqual(read(h.log).trim().split("\n").slice(-2), ["start [email protected]", "start mosaic-webui.service"]);
// Only the Console ran (the bus had stopped): the bus starts on the new copy, the Console stays.
const one = unit(h, { copy: COPY_A, env: { CASE2_TEST_ACTIVE: "mosaic-webui.service" } });
assert.equal(one.code, 0, one.err);
assert.match(one.out, new RegExp(`started mosaic-bus@acme\\.service on copy ${COPY_A}`));
assert.match(one.out, new RegExp(`mosaic-webui\\.service was already running, so it is still on copy ${COPY_B}, not copy ${COPY_A}`));
assert.doesNotMatch(one.out, /started mosaic-webui/);
assert.match(one.out, /restart mosaic-bus@acme\.service and mosaic-webui\.service together/);
});
test("a re-install to another copy re-renders both units, and an edited installed unit refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
assert.equal(unit(h, { copy: COPY_B }).code, 0);
assert.match(read(busFile(h)), new RegExp(`copies/${COPY_B}/scripts/mosaic`));
assert.doesNotMatch(read(busFile(h)) + read(webuiFile(h)), new RegExp(COPY_A));
const edits = [
[busFile, (s) => s.replace("User=mosaic", "User=root"), /User= must be exactly once in \[Service\], as User=mosaic/],
[webuiFile, (s) => s.replace(/^ExecStart=(.*)$/m, "ExecStart=$1 --board http://127.0.0.1:1"), /ExecStart= must be exactly once/],
[busFile, (s) => s.replace("ProtectHome=yes\n", ""), /ProtectHome= must be exactly once/],
[webuiFile, (s) => s.replace("PrivateTmp=yes\n", ""), /PrivateTmp= must be exactly once/],
[busFile, (s) => s.replace("Type=simple", "Type=simple\nX-Note=%h"), /%h is left/],
];
for (const [file, edit, pattern] of edits) {
const before = read(file(h));
writeFileSync(file(h), edit(before));
refuses(unit(h, { copy: COPY_B }), pattern);
writeFileSync(file(h), before);
}
assert.equal(unit(h, { copy: COPY_B }).code, 0);
});
test("a template with a namespace, user or system call restriction refuses", (t) => {
const h = hub(t);
for (const line of ["RestrictNamespaces=yes", "PrivateUsers=yes", "SystemCallFilter=@system-service"]) {
const key = line.split("=")[0];
for (const [from, env] of [[BUS_TEMPLATE, "CASE2_BUS_TEMPLATE"], [WEBUI_TEMPLATE, "CASE2_WEBUI_TEMPLATE"]]) {
const file = template(h, from, `t-${key}.in`, (s) => s.replace("NoNewPrivileges=yes", `NoNewPrivileges=yes\n${line}`));
refuses(unit(h, { env: { [env]: file } }), new RegExp(`refusing ${key}=`));
}
}
assert.equal(existsSync(busFile(h)), false);
});
test("the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none", (t) => {
const h = hub(t);
const gone = template(h, BUS_TEMPLATE, "no-env.in", (s) => s.replace(/^EnvironmentFile=.*\n/m, ""));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: gone } }), /EnvironmentFile= must be exactly once/);
const elsewhere = template(h, BUS_TEMPLATE, "env-elsewhere.in", (s) => s.replace("EnvironmentFile=-@SESSION_ENV@", "EnvironmentFile=-/etc/other.env"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: elsewhere } }), /EnvironmentFile= must be exactly once/);
const added = template(h, WEBUI_TEMPLATE, "webui-env.in", (s) => s.replace("NoNewPrivileges=yes", "NoNewPrivileges=yes\nEnvironmentFile=-/etc/other.env"));
refuses(unit(h, { env: { CASE2_WEBUI_TEMPLATE: added } }), /refusing EnvironmentFile=/);
});
test("a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses", (t) => {
const h = hub(t);
const token = template(h, BUS_TEMPLATE, "token.in", (s) => s.replace("Type=simple", "Type=simple\nX-Note=@SECRET@"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: token } }), /a token is left after the render: @SECRET@/);
const webuiToken = template(h, WEBUI_TEMPLATE, "wtoken.in", (s) => s.replace("Type=simple", "Type=simple\nX-Note=@SECRET@"));
refuses(unit(h, { env: { CASE2_WEBUI_TEMPLATE: webuiToken } }), /a token other than one @FLAGS@/);
const pso = template(h, BUS_TEMPLATE, "pso.in", (s) => s.replace("Type=simple", "Type=simple\nPermissionsStartOnly=yes"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: pso } }), /refusing PermissionsStartOnly=/);
for (const pre of ["+/srv/mosaic/x", "-+/srv/mosaic/x"]) {
const file = template(h, BUS_TEMPLATE, "pre.in", (s) => s.replace("Type=simple", `Type=simple\nExecStartPre=${pre}`));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: file } }), /an Exec line that runs with full privileges: ExecStartPre=/);
}
});
test("a symlink at either unit path refuses", (t) => {
for (const name of ["[email protected]", "mosaic-webui.service"]) {
const h = hub(t);
const target = join(h.root, "elsewhere");
writeFileSync(target, "");
symlinkSync(target, join(h.units, name));
refuses(unit(h), new RegExp(`${name.replace(".", "\\.")} is a symlink`));
}
});
test("a --node that is relative, not root-owned or under <home> refuses", (t) => {
const h = hub(t);
refuses(unit(h, { node: "bin/node" }), /--node must be an absolute path/);
const mine = script(join(h.root, "opt", "node"), "exit 0");
refuses(unit(h, { node: mine }), /--node: .* is not owned by root/);
const inside = script(join(h.home, "bin", "node"), "exit 0");
refuses(unit(h, { node: inside }), /--node must be outside/);
});
test("a re-install with a different --node or different serve flags refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
// A new node moves PATH too, so the installed unit fails its own Environment= check first.
refuses(unit(h, { node: outsideNode() }), /installed .*: the Environment= lines must be exactly/);
const flags = libraryFlags(h).map((f) => (f === "melanie" ? "someone" : f));
refuses(unit(h, { flags }), /installed .*: ExecStart= must be exactly once/);
// A line check_unit doesn't fix still has to equal the render.
writeFileSync(busFile(h), read(busFile(h)).replace(/^Description=.*$/m, "Description=edited"));
refuses(unit(h), /differs from the render in more than the copy id/);
});
test("an installed pair whose units name different copies refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
writeFileSync(webuiFile(h), read(webuiFile(h)).replaceAll(COPY_A, COPY_B));
refuses(unit(h), new RegExp(`the installed units name different copies, ${COPY_A} and ${COPY_B}`));
});
test("a copy id that is short, uppercase, holds / or .., or names no directory refuses", (t) => {
const h = hub(t);
for (const copy of ["a".repeat(39), "A".repeat(40), `${"a".repeat(19)}/${"a".repeat(20)}`, `..${"a".repeat(38)}`]) {
refuses(unit(h, { copy }), /--copy must be a full 40-character lower-case commit id/);
}
refuses(unit(h, { copy: "c".repeat(40) }), /no copy directory/);
});
test("library refuses a privileged-floor miss, a bad proof pair and every flag outside its set", (t) => {
const h = hub(t);
const base = libraryFlags(h);
const without = (...names) => {
const out = [];
for (let i = 0; i < base.length; i++) {
if (names.includes(base[i])) { if (base[i] !== "--send") i++; continue; }
out.push(base[i]);
}
return out;
};
refuses(unit(h, { flags: base.map((f) => (f === "443" ? "7332" : f)) }), /--port must be 1\.\.1023/);
refuses(unit(h, { flags: without("--proof-header", "--proof-file") }), /serve flag --proof-header is required/);
refuses(unit(h, { flags: base.map((f) => (f === "X-Auth-User" ? "X-Authentik-Uid" : f)) }), /must be an Authentik uid/);
assert.equal(unit(h, { flags: base.map((f) => (f === "X-Auth-User" ? "X-Authentik-Uid" : f === "melanie" ? "0".repeat(64) : f)) }).code, 0);
rmSync(busFile(h));
rmSync(webuiFile(h));
for (const [flag, value, pattern] of [
["--tester-port", "7333", /case2-unit.sh retires tester TCP; use --tester-socket/],
["--tester-socket", null, /the library profile refuses --tester-socket/],
["--checkout", "/srv/x", /refusing --checkout/],
["--board", "http://127.0.0.1:7331", /refusing --board/],
["--sso", null, /refusing --sso/],
["--nonsense", "1", /unknown serve flag: --nonsense/],
]) {
refuses(unit(h, { flags: [...base, flag, ...(value === null ? [] : [value])] }), pattern);
}
refuses(unit(h, { flags: base.map((f) => (f === "acme" ? "other" : f)) }), /serve --business other is not the unit's business acme/);
refuses(unit(h, { extra: ["--pm"] }), /the library profile refuses --pm/);
refuses(unit(h, { extra: ["--claude", TRUE_BIN] }), /the library profile refuses --claude/);
chmodSync(h.proof, 0o644);
refuses(unit(h), /mode 0600 or 0400, not 644/);
chmodSync(h.proof, 0o600);
const link = join(h.secrets, "proof-link");
symlinkSync(h.proof, link);
refuses(unit(h, { flags: base.map((f) => (f === h.proof ? link : f)) }), /must not be a symlink/);
const dataRoot = join(h.home, ".mosaic-dev");
mkdirSync(dataRoot, { recursive: true });
const outside = join(dataRoot, "proof");
writeFileSync(outside, "made-up-proof\n");
chmodSync(outside, 0o600);
refuses(unit(h, { flags: base.map((f) => (f === h.proof ? outside : f)) }), /must be under .*secrets, outside the data root/);
assert.equal(existsSync(busFile(h)), false);
});
// A non-root test can't make a root-owned file under the scratch home, so the
// owner case names another uid in the passwd database instead.
test("library refuses a proof file not owned by <user>'s uid", (t) => {
const h = hub(t, { uid: UID + 4242 });
refuses(unit(h), new RegExp(`proxy proof file must be owned by mosaic \\(uid ${UID + 4242}\\)`));
});
test("factory refuses duplicate socket, a missing --port or --business, and each proxy flag", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), '--tester-socket'] }), /given twice/);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), '--port', '443'] }), /given twice/);
refuses(unit(h, { profile: "factory", flags: ["--business", "acme"] }), /serve flag --port is required/);
refuses(unit(h, { profile: "factory", flags: ["--port", "443"] }), /serve flag --business is required/);
for (const [flag, value] of [["--public-host", "x.example"], ["--auth-header", "X-A"], ["--allow-user", "u"], ["--proof-header", "X-P"], ["--proof-file", h.proof], ["--send", null]]) {
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), new RegExp(`the factory profile refuses ${flag}`));
}
for (const [flag, value, pattern] of [["--checkout", "/srv/x", /refusing --checkout/], ["--board", "http://127.0.0.1:7331", /refusing --board/], ["--sso", null, /refusing --sso/], ["--nonsense", "1", /unknown serve flag/]]) {
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), pattern);
}
});
test("row253 factory refuses retired TCP and a value after the socket marker", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);
refuses(unit(h, { profile: 'factory', flags: [...factoryFlags(), '7333'] }), /unknown serve flag: 7333/);
});
test("factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", extra: ["--claude", "bin/claude"] }), /--claude must be an absolute path/);
const mine = script(join(h.root, "opt", "claude"), "exit 0");
refuses(unit(h, { profile: "factory", extra: ["--claude", mine] }), /--claude: .* is not owned by root/);
const inside = script(join(h.home, "bin", "claude"), "exit 0");
refuses(unit(h, { profile: "factory", extra: ["--claude", inside] }), /--claude must be outside/);
assert.equal(unit(h, { profile: "factory", extra: ["--pm", "--claude", TRUE_BIN] }).code, 0);
refuses(unit(h, { profile: "factory", extra: ["--claude", TRUE_BIN] }), /--pm and --claude must equal the installed ones|ExecStart= must be exactly once/);
refuses(unit(h, { profile: "factory", extra: ["--pm"] }), /Environment= lines must be exactly|differs from the render/);
refuses(unit(h, { profile: "factory", extra: ["--pm", "--claude", USR_NODE] }), /Environment= lines must be exactly|differs from the render/);
});
// ---- case2-precheck.sh ----
// A scratch user for the precheck: a config, a business file, an installed Console unit,
// fakes on PATH and a fake /proc/sys.
function host(t, { roles = {}, vars = {}, keyFile = `ZAI_API_KEY=${FIXTURE_KEY}\n`, port = "443", busClaude = null } = {}) {
const root = tmp(t, "case2-pre-");
const home = join(root, "home");
const configDir = join(home, ".config", "mosaic-dev");
const config = writeJson(join(configDir, "config.json"), {
configVersion: 1,
environment: "production",
dataRoot: join(home, ".mosaic-dev"),
execution: { backend: "docker", provider: "zai", model: "glm-5.3-flash", adapter: "pi" },
});
const doc = businessDoc(root);
doc.roles.cto.vars = { harness: "pi" };
for (const [name, v] of Object.entries(roles)) doc.roles[name].vars = v;
Object.assign(doc.vars, vars);
writeJson(join(configDir, "businesses", "acme.json"), doc);
const secrets = join(configDir, "secrets");
mkdirSync(secrets, { recursive: true, mode: 0o700 });
if (keyFile !== null) {
writeFileSync(join(secrets, "claude-session.env"), keyFile);
chmodSync(join(secrets, "claude-session.env"), 0o600);
}
const units = join(root, "units");
mkdirSync(units);
writeFileSync(join(units, "mosaic-webui.service"), `[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --port ${port} --business acme\n`);
writeFileSync(join(units, "[email protected]"), `[Service]\nEnvironment=PATH=/usr/bin\n${busClaude ? `Environment=MOSAIC_CLAUDE_BIN=${busClaude}\n` : ""}`);
const sys = join(root, "sys");
mkdirSync(join(sys, "net", "ipv4"), { recursive: true });
mkdirSync(join(sys, "kernel"), { recursive: true });
writeFileSync(join(sys, "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
writeFileSync(join(sys, "kernel", "apparmor_restrict_unprivileged_userns"), "1\n");
const bin = join(root, "bin");
const me = spawnSync("id", ["-un"], { encoding: "utf8" }).stdout.trim();
script(join(bin, "getent"), `[ "$1" = passwd ] && [ "$2" = '${me}' ] && echo '${me}:x:${UID}:${GID}::${home}:/bin/bash'`);
script(join(bin, "sudo"), "exit 1");
script(join(bin, "id"), `if [ "$1" = -G ]; then echo "\${CASE2_TEST_GROUPS:-${GID}}"; else exec /usr/bin/id "$@"; fi`);
script(join(bin, "node"), `if [ "$1" = --version ]; then echo "\${CASE2_TEST_NODE:-v24.3.0}"; exit 0; fi\nexec '${process.execPath}' "$@"`);
script(join(bin, "bwrap"), `exit "\${CASE2_TEST_BWRAP:-0}"`);
return { root, home, configDir, config, secrets, units, sys, bin, dataRoot: join(home, ".mosaic-dev") };
}
// A fake claude: replace answers with the marker and a small prompt; append answers with a large one;
// nomarker answers with a small prompt and no marker; text answers with plain text, not JSON.
function fakeClaude(dir, kind) {
return script(join(dir, "claude"), `[ "$1" = --version ] && { echo "9.9.9 (Claude Code, ${kind} fake)"; exit 0; }
file=''
while [ $# -gt 0 ]; do [ "$1" = --system-prompt-file ] && file="$2"; shift; done
marker="$(awk '{ print $NF }' "$file")"
${kind === "nomarker" ? "marker=something-else" : ""}
${kind === "text" ? 'echo "$marker"; exit 0' : ""}
tokens=${kind === "append" ? 9000 : 40}
printf '{"result":"%s","usage":{"input_tokens":%s,"cache_read_input_tokens":0}}\\n' "$marker" "$tokens"`);
}
// This host may have a claude in /usr/bin, so this PATH links every /usr/bin entry but that one.
function pathWithoutClaude(x) {
const usr = join(x.root, "usr-bin");
mkdirSync(usr);
for (const name of readdirSync("/usr/bin")) if (name !== "claude") symlinkSync(join("/usr/bin", name), join(usr, name));
return `${x.bin}:${usr}`;
}
function precheck(x, { profile = "library", env = {}, path } = {}) {
const r = spawnSync(PRECHECK, ["--business", "acme", "--profile", profile], {
encoding: "utf8",
timeout: 60000,
env: {
PATH: path ?? `${x.bin}:/usr/bin:/bin`,
HOME: x.home,
MOSAIC_CONFIG: x.config,
CASE2_UNIT_DIR: x.units,
CASE2_PROC_SYS: x.sys,
CASE2_GETENT: join(x.bin, "getent"),
...env,
},
});
const out = r.stdout + r.stderr;
assert.doesNotMatch(out, new RegExp(FIXTURE_KEY), "the precheck printed the fixture key");
return { code: r.status, out };
}
function blocked(r, pattern) {
assert.equal(r.code, 1, r.out);
assert.match(r.out, pattern);
}
test("the precheck passes on a good host and prints free memory, without the key", (t) => {
const x = host(t);
const r = precheck(x);
assert.equal(r.code, 0, r.out);
assert.match(r.out, /ok {6}config validates/);
assert.match(r.out, /ok {6}bus\.tester is off/);
assert.match(r.out, /ok {6}net\.ipv4\.ip_unprivileged_port_start 1024 is above the Console's port 443/);
assert.match(r.out, /claude-session\.env sets ZAI_API_KEY/);
assert.match(r.out, /free memory: \d+ MiB/);
assert.doesNotMatch(r.out, /claude .*fake|marker/);
});
test("the precheck refuses each failed host check", (t) => {
blocked(precheck(host(t), { env: { CASE2_TEST_NODE: "v22.22.2" } }), /REFUSED node v22\.22\.2 is older than 24/);
blocked(precheck(host(t), { env: { CASE2_TEST_GROUPS: `${GID} 998` } }), /REFUSED id -G prints more than one group/);
const sudo = host(t);
script(join(sudo.bin, "sudo"), "exit 0");
blocked(precheck(sudo), /REFUSED sudo -n true succeeds/);
blocked(precheck(host(t), { env: { XDG_DATA_HOME: "/elsewhere" } }), /REFUSED XDG_DATA_HOME is \/elsewhere/);
const home = host(t);
assert.equal(precheck(home, { env: { XDG_DATA_HOME: `${home.home}/.local/share` } }).code, 0);
blocked(precheck(host(t), { env: { HOME: "/tmp" } }), /REFUSED HOME is \/tmp/);
const runs = host(t);
mkdirSync(join(runs.dataRoot, "launches", "acme"), { recursive: true, mode: 0o755 });
chmodSync(join(runs.dataRoot, "launches", "acme"), 0o755);
blocked(precheck(runs), /REFUSED run root .* is not a 0700 directory \(mode 755\)/);
chmodSync(join(runs.dataRoot, "launches", "acme"), 0o700);
assert.match(precheck(runs).out, /ok {6}run root .* is 0700/);
const bad = host(t);
writeFileSync(bad.config, "{ not json");
blocked(precheck(bad), /REFUSED config:/);
});
test("the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root", (t) => {
const nouser = host(t);
script(join(nouser.bin, "getent"), "exit 2");
blocked(precheck(nouser), /REFUSED user: no .* in the passwd database/);
const unparsed = host(t);
writeFileSync(join(unparsed.configDir, "businesses", "acme.json"), "{ not json");
blocked(precheck(unparsed), /REFUSED business file: cannot read .*acme\.json/);
const gone = host(t);
rmSync(join(gone.configDir, "businesses", "acme.json"));
blocked(precheck(gone), /REFUSED business file: cannot read .*acme\.json: ENOENT/);
const invalid = host(t);
const file = join(invalid.configDir, "businesses", "acme.json");
const doc = JSON.parse(read(file));
doc.notAField = true;
writeJson(file, doc);
const r = precheck(invalid);
blocked(r, /REFUSED business file: /);
assert.doesNotMatch(r.out, /REFUSED business file: cannot read/);
// stat follows a link, so a link to a 0700 directory would read as 0700.
const link = host(t);
const target = join(link.root, "elsewhere");
mkdirSync(target, { mode: 0o700 });
chmodSync(target, 0o700);
mkdirSync(join(link.dataRoot, "launches"), { recursive: true });
symlinkSync(target, join(link.dataRoot, "launches", "acme"));
blocked(precheck(link), /REFUSED run root .* is a symlink/);
});
test("a bwrap that fails refuses and prints the sysctl and where bwrap is", (t) => {
const x = host(t);
blocked(precheck(x, { env: { CASE2_TEST_BWRAP: "1" } }), new RegExp(`REFUSED bwrap .* failed; bwrap: ${join(x.bin, "bwrap")}; kernel\\.apparmor_restrict_unprivileged_userns: 1`));
});
test("an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit", (t) => {
for (const floor of ["443", "80"]) {
const x = host(t);
writeFileSync(join(x.sys, "net", "ipv4", "ip_unprivileged_port_start"), `${floor}\n`);
blocked(precheck(x), new RegExp(`REFUSED net\\.ipv4\\.ip_unprivileged_port_start is ${floor}, at or below the Console's port 443`));
}
const x = host(t);
rmSync(join(x.units, "mosaic-webui.service"));
blocked(precheck(x), /REFUSED no installed Console unit/);
const noPort = host(t);
writeFileSync(join(noPort.units, "mosaic-webui.service"), "[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --business acme\n");
blocked(precheck(noPort), /REFUSED .*mosaic-webui\.service has no --port in its ExecStart/);
const noFloor = host(t);
rmSync(join(noFloor.sys, "net", "ipv4", "ip_unprivileged_port_start"));
blocked(precheck(noFloor), /REFUSED cannot read net\.ipv4\.ip_unprivileged_port_start/);
});
test("bus.tester: library refuses it on, factory needs it on, and factory refuses a library business", (t) => {
blocked(precheck(host(t, { vars: { "bus.tester": true } })), /REFUSED bus\.tester is on; the library profile needs it off/);
blocked(precheck(host(t), { profile: "factory" }), /REFUSED bus\.tester is off; the factory profile needs it on/);
const on = precheck(host(t, { vars: { "bus.tester": true } }), { profile: "factory" });
assert.equal(on.code, 0, on.out);
const lib = host(t, { vars: { "bus.tester": true } });
const file = join(lib.configDir, "businesses", "acme.json");
const doc = JSON.parse(read(file));
doc.projects.stack.kind = "library";
writeJson(file, doc);
blocked(precheck(lib, { profile: "factory" }), /REFUSED profile-library-business: .* projects stack are library projects/);
});
test("the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong", (t) => {
// No claude-code instance and no claude anywhere on PATH: the precheck passes.
const none = host(t);
const r = precheck(none, { path: pathWithoutClaude(none) });
assert.equal(r.code, 0, r.out);
assert.doesNotMatch(r.out, /--system-prompt-file|claude is installed/);
const good = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(good.bin, "replace");
const pass = precheck(good);
assert.equal(pass.code, 0, pass.out);
assert.match(pass.out, /claude .*9\.9\.9 \(Claude Code, replace fake\)/);
assert.match(pass.out, /ok {6}claude replaces its prompt with --system-prompt-file \(40 input tokens\)/);
const bad = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(bad.bin, "append");
blocked(precheck(bad), /REFUSED claude adds to its own prompt with --system-prompt-file: 9000 input tokens/);
const nomarker = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(nomarker.bin, "nomarker");
blocked(precheck(nomarker), /REFUSED claude's reply to the marker prompt lacks the marker \(40 input tokens\)/);
const text = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(text.bin, "text");
blocked(precheck(text), /REFUSED claude gave no JSON reply to the marker prompt/);
const missing = host(t, { roles: { cto: { harness: "claude-code" } } });
blocked(precheck(missing, { path: pathWithoutClaude(missing) }), /REFUSED an instance runs claude-code and no claude is installed/);
});
test("the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH", (t) => {
const pinned = join(tmp(t, "case2-claude-"), "pinned");
const replace = fakeClaude(pinned, "replace");
const x = host(t, { roles: { cto: { harness: "claude-code" } }, busClaude: replace });
fakeClaude(x.bin, "append");
const r = precheck(x);
assert.equal(r.code, 0, r.out);
assert.match(r.out, /replace fake/);
const pinnedBad = join(tmp(t, "case2-claude-"), "pinned");
const append = fakeClaude(pinnedBad, "append");
const y = host(t, { roles: { cto: { harness: "claude-code" } }, busClaude: append });
fakeClaude(y.bin, "replace");
blocked(precheck(y), /REFUSED claude adds to its own prompt/);
});
test("the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line", (t) => {
blocked(precheck(host(t, { keyFile: null })), /REFUSED an instance's provider is zai and .*claude-session\.env is missing/);
const open = host(t);
chmodSync(join(open.secrets, "claude-session.env"), 0o644);
blocked(precheck(open), /REFUSED .*claude-session\.env must be owned by .* at 0600 \(mode 644\)/);
const link = host(t, { keyFile: null });
const real = join(link.root, "real.env");
writeFileSync(real, `ZAI_API_KEY=${FIXTURE_KEY}\n`);
chmodSync(real, 0o600);
symlinkSync(real, join(link.secrets, "claude-session.env"));
blocked(precheck(link), /REFUSED .*claude-session\.env is a symlink/);
blocked(precheck(host(t, { keyFile: "ZAI_API_KEY=\n" })), /REFUSED .*sets no nonempty ZAI_API_KEY/);
blocked(precheck(host(t, { keyFile: `ZAI_API_KEY=${FIXTURE_KEY}\nZAI_API_KEY=\n` })), /REFUSED .*sets no nonempty ZAI_API_KEY/);
const quoted = precheck(host(t, { keyFile: `ZAI_API_KEY=\nZAI_API_KEY="${FIXTURE_KEY}"\n` }));
assert.equal(quoted.code, 0, quoted.out);
});
// ---- the bind test (mos-1100) ----
// A session can't take the Console's port: inside the sandbox a real session gets, a bind below
// 1024 fails with EACCES, and the same bind on a free port of 1024 or above succeeds.
const sandboxSkip = sandboxProblem() ?? false;
const LOW_PORT = 980;
test("inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds", { skip: sandboxSkip, timeout: 30000 }, async (t) => {
const floor = Number(read("/proc/sys/net/ipv4/ip_unprivileged_port_start").trim());
assert.ok(floor > LOW_PORT, `this host's net.ipv4.ip_unprivileged_port_start is ${floor}, at or below the test port ${LOW_PORT}, so the test can't show the sandbox refuses it`);
const root = tmp(t, "c2b-");
const runDir = join(root, "run");
const workspace = join(root, "ws");
mkdirSync(runDir);
mkdirSync(workspace);
mkdirSync(join(runDir, "bundle"));
writeFileSync(join(runDir, "session.json"), "{}");
const adapter = join(root, "adapter.sh");
const brokerSocket = join(root, "broker.sock");
writeFileSync(adapter, "");
writeFileSync(brokerSocket, "");
prepareRun(runDir);
const list = binds({ runDir, workspace, adapter, harnessMounts: [], brokerSocket });
const high = await new Promise((done) => {
const s = createServer().listen(0, "127.0.0.1", () => { const { port } = s.address(); s.close(() => done(port)); });
});
const bindIn = (port) => {
const code = `require("net").createServer().on("error", (e) => { console.log(e.code); process.exit(0); }).listen(${port}, "127.0.0.1", function () { console.log("bound"); this.close(); });`;
const argv = sandboxArgv(list, runDir, [process.execPath, "-e", code], { business: "acme", role: "coder" });
const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf8", timeout: 10000 });
assert.equal(r.status, 0, r.stderr);
return r.stdout.trim();
};
assert.equal(bindIn(LOW_PORT), "EACCES");
assert.equal(bindIn(high), "bound");
});
@@ -0,0 +1,463 @@
# Deferred list
One line per item, written from evidence, never from inertia. Add a line
when a gap is found and not fixed in the current piece; strike it (move to
Done at the bottom with the commit) when a piece closes it. Nothing here
starts on its own; a line becomes a piece only when Jason calls it. Check it
at every gate. Started 2026-09-12 during the control board MVP.
## Open
- **Inherited Discord engine test intermittency during #1512 review.** Frozen
serialized run recorded a tool-turn engine assertion failure and then timed
out at 120s. A later full TAP run with a 15s test deadline passed 351/351.
Rocko isolated the busy/settled-event test race and missing failure cleanup;
Darkwing independently reproduced the event-split assertion. Rocko also reported
a timeout-log test race and a separate engine followUp-after-timeout defect.
No live incident established. Row21 owner orch-01 notified; no Discord source
takeover. Not assumed identical to earlier concurrent hangs.
Integration held pending owner disposition; logs and scope in
`agents/darkwing/work/relaunch-activity/r1-review.md`. (2026-09-15)
Outcome 2026-09-26: 1685deb4 fixed the busy/settled race and the engine no
longer sends a follow-up (held prompts). A re-run of the frozen candidate
against 21e3e908 passed the serial acceptance command 397/397 three times.
Two engine failures remain when tests run concurrently, and they reproduce on
clean HEAD without the #1512 files, so they are #1509's (entry below). Sage
ruled that #1512 integrates on the serial evidence; it landed in af4203ca.
Record: `agents/darkwing/work/relaunch-activity/rerun-2026-09-26.md`.
- **Registration has no "set by" field.** `mosaic seat task` records the
task and `updatedAt` but not who set it, so a human session and the
orchestrator look the same on the board. One-line record change; rides
with the orchestrator assignment piece. (2026-09-12, #1504)
- **Usage-limit notice was missed.** Jason reports Rocko exhausted usage without
Jason seeing the notice, and is now available again. Investigate where the
notice appeared and what operator notification is missing as part of Claude
console integration. User-reported, not independently reproduced; no root
cause established. Keep outside #1512's relaunch-display scope. (2026-09-15)
- **Claude Code seats register but get no board row.** Rocko has no Pi
session log, and rows come only from Pi logs. Needs a second row source
or a Claude-side log reader. (2026-09-12, darkwing's #1504 report)
2026-09-15 #1511 live-test preflight reconfirmed no Rocko row: expected
`.pi/state/rocko/sessions` discovery directory absent. Its existing repo
registration names dead PID 185602, while the verified native lock owner is
3707667. No registration repair, discovery expansion or relaunch performed;
test write refused before mutation. Evidence:
`agents/darkwing/work/task-attribution/backend-live-verification.json`.
- **Fleet seats (`~/.mosaic`) stay on the old launchers.** No registration,
no board reply, no `seat task`. Moving them is the stress test and comes
after the WebUI and fleet launch work. (Jason, 2026-09-12)
- **Comms skill has no rule for senders without a pane.** The board trailer
covers it today; if another pane-less sender appears, put the rule in
`skills/ms-communications` instead of a second trailer. (2026-09-12, #1505)
- **Board: multiple sessions per seat, stopping seats, auth beyond
loopback.** Out of scope for the MVP by the plan; listed so they are not
forgotten. (plan page, Boundaries)
- **`MOSAIC-STACK-D-001` is Answered, not Completed.** Mark `--completed` in
jarvis-brain once Jason says the board answers "who is waiting on me".
(2026-09-12)
- **Uncommitted working tree.** `AGENTS.md` autonomy section,
`agents/README.md` Sage row, Dewey's `work/wui` docs and evidence,
`docs/plans/reviews/2026-09-08..10_*`, `skills/ms-{arena,blast-radius,
board,executive-update,git-operations,technical-writing}`. Jason's edits
from before 2026-09-12; they ride along in the next commit that touches
them unless staged on purpose. (2026-09-12)
- **Fleet debt (orch-01, `~/.mosaic`):** `sweep-commit.sh` deny-list regex
skips `launch.env`; needs the fix plus a regression test. (2026-09-12)
- **Parked, not cancelled:** Dewey D05 dashboard mockup work beyond design 3,
registry increment 3 (headless identity-env leak), new seat directories.
(plan page, 2026-09-12 parking note)
- **Gitea helper response-file hardening.** Predictable `/tmp/gitea-api-response.$$`
has no explicit 0600 mode; use `mktemp` with cleanup in a separately reviewed
pass. Credential validation and later reread also have a race window. Other
non-blocking ledger review notes remain in
`reviews/2026-09-12_ledger-verdict.md`. No change beyond the reviewed GET
cleanup fix in this piece. Filbert, LEDGER-1506-R1, 2026-09-12, #1506.
- **One shared checkout for every seat and the coordinator.** Any `git add`
of a shared file (CURRENT.md, SESSIONS.md, the plan page) commits another
seat's in-progress edit under the wrong commit; it happened at f80404f5
(dewey's CURRENT.md status note went in with the piece 5 brief, harmless
but unowned). Fix candidates: per-seat worktrees, or shared files edited
only by the seat named in CURRENT.md. (2026-09-12)
- **Console mockup fields absent from the board API.** Controller, generation,
config fingerprint, turn count, goal/report and activity stream are omitted.
Workspace paths are shown as board data, not invented registered workspace
nodes. Launch, observe, stop, full transcript navigation, command palette and
other screens are absent. Existing palette/appearance switches remain on the
single page. Revisit only with a new brief. (2026-09-13 UTC, #1507)
- **Board send can turn into a Pi slash command.** `tools/tmux`
`send-message.sh` pastes onto whatever is already in Pi's composer without
clearing it. On Jason's 2026-09-26 20:10:57Z send, a leftover "/" landed in
front of the agent-send header. Pi read it as plain text that time, but the
same path could run a board reply as a slash command. Belongs to CHAT-03I/B3.
No tools/tmux change until that piece. Found by Dewey in the CHAT-02 brief
evidence. (2026-09-26, #1507)
- **Discord wedge restarts have no cumulative stop.** Since 6b R2, a wedged
pi ends the connector with exit 1, and the unit restarts it. With
turnTimeoutSeconds 600, one cycle takes at least 645 s, longer than the
600 s StartLimit window. So a pi that wedges on every prompt restarts once
per prompt and never hits the limit. It only happens when someone sends a
message, and that person gets an error reply. Sage accepted it for now. The
fix is a separate policy (a wedge count kept across restarts). (2026-09-26,
#1509, Rocko 6b R2 review)
- **Connector does not journal the source it started from.** The unit runs
from the live checkout, so an automatic restart loads whatever Discord
source is in the tree, even uncommitted source. Rocko recommends a startup
journal line with HEAD, the dirty state of `packages/discord` and a digest
of the runtime files. Until then the rule is that unreviewed Discord source
stays uncommitted in the tree only as long as a review needs it.
(2026-09-26, #1509)
- **Index-export suite runs leave Docker networks behind.** Each export
directory becomes a compose project, and its `<dir>_default` network
stays after the run. On 2026-09-26 the host ran out of address pools. The
task and release suites then failed 31 and 3 tests with "all predefined
address pools have been fully subnetted". Sage removed its own five stale
networks. The dw-* ones are still there. Fix: the export recipe, or the
suites, removes the project network on exit. (2026-09-26)
2026-09-27: it happened again on the Piece D candidate (task 31 and
release 3 failures). Sage removed 12 leftover export networks from
other seats, each checked for zero attached containers first, and both
suites then passed. Still open.
- **Ledger T3 read: snapshot isolation has no test.** `readT3` runs every
query in one read transaction, but no test proves a write landing during
the read stays invisible. Filbert's note 4 on the Gate F build (e47ec6da).
Notes 1 to 3 go to Darkwing's follow-up. (2026-09-26, #1506)
- **A T3 sender role with spaces counts as human.** The SetSpark lead's
reply to Sage (21:41:24Z) used the role "main content-engine agent". The
6a rule requires a one-word role, so the ledger counts that message as
human in Sage's row. Gate F reads only Filbert's row, so it is not
affected. Filbert found it through the live diagnostic (14 went to 15).
Sage asked the SetSpark lead to use a one-word role. The rule stays as
it is. (2026-09-26, #1506)
- **Queue tests: the cold-start failure came back (reopened).** Row 33
closed this as not reproduced in 20 idle cold runs. On 2026-10-04 Sage's
index export of the row 33 candidate ran the nine suites, then
`node --test packages/queue/tests/`, and got 147 passed, 1 failed. Sage's
recipe threw the output away, so again nobody knows which test failed.
Ten more runs all passed 148/148: three reruns in the same export, four
under parallel suite load, and three fresh exports run straight after
`test-queue.sh`. It needs a first run on a fresh tree after a full
suite pass, and even then it's rare. Darkwing's n2 on row 33 (the cold
runs were idle) was right. Next time: Sage's export recipe keeps the
node test output in a file, so the first failure has a name. Then fix it
as its own row. (2026-10-04, #1508)
- **Queue: `add --issue N` also sets closes to N.** Follow-up rows under
an umbrella issue, like rows 31 and 33 under #1508, would close the issue
when they finish. Sage narrowed both by hand (revs 36 and 43). Either
`add` takes `--closes` or the lead checks closes on every add. Not
urgent while Sage adds the rows. (2026-10-04, #1508)
- **Ledger metric call can orphan curl.** The metric call in
`packages/ledger/src/ledger.mjs` still relies on execFileSync's
timeout, which kills the helper but can leave curl running. E's queue
calls already use `timeout -s KILL`; the metric call should match.
(2026-09-27, #1508)
- **Ledger: a missing `timeout` command reads as a Gitea failure.** With
no `timeout` on PATH the queue issue calls report "credential or Gitea
request failure". Optional test and clearer message. Filbert, E r2.
(2026-09-27, #1508)
- **`review record` doesn't check which issue the verdict comment is on.**
Row 5 round 1's request (26671) and Filbert's verdict (26683) are on
#1507. Darkwing's verdict 26681 went to #1508, and `review record`
accepted it, because only `review resolve` reads the comment back
(`checkComment` in `packages/queue/src/review.mjs`). The receipt
points at a real comment on the wrong issue. Sage posted a pointer on
#1507 (comment 26685). Fix: have `record` check the comment's issue and author the way
`resolve` does, or refuse a comment id it can't verify. Found by
Filbert. (2026-10-04, #1508)
- **Host seats launch Pi with `--approve`.** A future `.pi/SYSTEM.md` or
`.pi/settings.json` in the checkout would load in every host seat. This
is latent, since neither file exists today. Fix in
`scripts/agent-host-dev.sh`: pass `--no-approve` and an explicit system
prompt. Filbert, meta-harness survey section 8.4. (2026-10-04)
- **Workers can read provider keys.** The worker container passes
`ZAI_API_KEY` and `ANTHROPIC_API_KEY` in the environment, which Pi's
`bash` inherits, and Pi's `read` can open the mounted `auth.json`.
Slice 1 role tokens stay in the broker (lead decision 47). The workers'
own exposure needs a separate piece. Filbert, survey section 8.6.
(2026-10-04)
- **CHAT-03 I1 round 2 follow-ups, for the I3 brief.** Neither reviewer
held any of these as blocking (Darkwing comment 26690, Filbert comment
26694 on #1507). Four of the five are closed by slice 1 S5 (see Done);
this one stays open.
- No test proves that the processes are listed only after the cgroup
is frozen. The code waits for `frozen 1` in `cgroup.events`, and
both Filbert and Dewey confirmed it by reading it. Mutants r2-B5b and
C4 survive. Proposed: a test-only hold after `members`, or a FUSE or
privileged fixture. Filbert F1.
(2026-10-04, #1507)
- **CHAT-03 `engine.envKeys` accepts founder credential names.**
`CREDENTIAL_NAME` in `packages/conversation/src/pi-pin.mjs` matches any
`*_API_KEY` or `*_TOKEN`, including `GITEA_TOKEN` and `GH_TOKEN`, which
the S6 runner refuses as founder credentials (REQ-CRED-2). Nothing
builds a Controller outside the tests yet. Whoever wires the entry point
narrows the pattern to provider names or checks the founder list.
Filbert N2 on #1522. (2026-10-10)
- **`test-task.sh` makes a live Pi call without the Docker guard.** The
"user recall" block near line 478 runs `run-task.sh` with no
availability check, so a run with Docker unavailable fails "user recall
run succeeds" and "recalled user name" (26 pass, 2 fail). The other
live groups skip. Rocko hit it during the S2 suites. Wrap the block in
the same guard as the other live groups, so a seat without provider
access gets clean skips. (2026-10-04, #1519)
- **`node --test <dir>/` fails on Node 24.** Node 24 reads a directory
argument as a module, so every package's test command in that form
fails there, while Node 26 accepts it. The glob form
(`node --test 'packages/x/tests/*.test.mjs'`) works on both. Package
READMEs, `docs/TOOLS.md` and Sage's suite recipe should use the glob
form wherever Node 24 is a target. Darkwing, S1 packet. (2026-10-04,
#1518)
- **`stack-upgrade.sh` doesn't check `$REPO`.** Nothing confirms the
source checkout is root-owned or that its tree is clean before the
upgrade builds from it. Filbert N2 on row 233 (29922, #1735).
(2026-10-21)
- **`stack-upgrade.sh` has no signal handling between A2 and A5.** A HUP,
INT or TERM in that stretch leaves the run failed closed, but recovery
is by hand. A trap that writes the stop point and the recovery step to
the log would make it a sheet step. Filbert N3 on row 233 (29922,
#1735). (2026-10-21)
- **`checkCreative` doesn't refuse `credentials`.** Row 213's
`checkCreative` (packages/business/src/role.mjs:143 at 2d2ad2760)
limits a creative role's tools, authority and messaging, but a role
file that gave companion or research a credential would still load.
The business file could then map a credential to that instance
(business.mjs:157). Nobody has traced whether a mapped credential
reaches the run's environment. The decision paths stay closed (M6),
but a creative role with `web` holding a live token is a wider channel
than design 4 means. The fix: refuse any `credentials` in
`checkCreative`, with a role.test.mjs case. It goes after row 213
lands, as a follow-up row or in the next row that edits role.mjs.
Webby N1 on row 213 r3 (REVIEW-r3.md, 3ab3c010b), mos-1160.
(2026-10-21)
- **The boot host check orders only one business's bus.**
`mosaic-hostcheck.service` (row 240) has
`[email protected]`. `case2-unit.sh` enables the
bus as `mosaic-bus@<business>.service`, so on a host whose business id
isn't `mosaic-stack` nothing orders the two. The bus can start before
the boot stop runs, and after a boot-time drift role sessions can use
bwrap until it does. The fix: `After=mosaic-hostcheck.service` in
`packages/cli/systemd/mosaic-bus.service.in`, which orders every
instance and does nothing where the unit isn't installed. It goes in
before the first install whose business id isn't `mosaic-stack`.
Filbert N1 on row 240 (29945, #1746). (2026-10-21)
- **The run sheet writes the apt hook before the first `--check`.**
Section 4 installs `80mosaic-hostcheck`, then runs
`hostcheck.sh --check`. If that check fails, the next package run
closes bwrap before anyone has read the failure. It fails closed, but
it surprises an installer. The fix: run `--check` first, or say what a
failure at that step means. Doc-only, for the next row that edits
`docs/guides/host-run-sheet.md`. Filbert N2 on row 240 (29945, #1746).
Filbert's N3, that a closed bwrap appends to `DRIFT` on every later
apt run and boot, is by design; ops-01's STATE read should expect the
repeats. (2026-10-21)
- **The sso-config first-step hint has no business.** When the target
copy has no `console.sso`, `stack-release.sh` (`:938`) refuses with
"activate $TO without --sso first, then ... activate $TO --sso". The
usage line is `activate ID BUSINESS` (`:47`), so pasting either command
as printed gives a usage error, not the first step. It fails safe,
since the usage error changes nothing. The fix: add `$BUSINESS`
(set at `:2138`) to both commands, and the business (`acme`)
to the three expected strings in `stack-release.test.mjs`
(`:1538`, `:1689`, `:1691`). For a later row that edits
`stack-release.sh`. Webby N1 on row 238 r1 (REVIEW-r1.md, 433a95292,
#1744). (2026-10-21)
- **Lane image prep inspects a base that BuildKit never tagged.**
(N7 of the row 197 proof.) After the Compose build,
`scripts/testing/lane-image.mjs:41` (`prepareImage`) runs
`docker image inspect node:24-bookworm-slim` to put the base digest
in the receipt. BuildKit resolves the base from the registry
without tagging it in the local store, so on a host without the tag
the inspect throws "No such image" after a good build. That
happened in Sage's proof try 1 (2026-10-21 09:03Z, run cf48efccf083).
The throw fails closed: the run's terminal record is unclean, both
lanes quarantine, and `gate-lane.sh recover` clears them. Pulling the
base first fixes it, and the receipt digest then matches what the
build used (91acba91 both ways). The fix: before the build, inspect
the base and refuse with a named reason if it's missing, or pull it.
After the build, refuse if the base digest changed. Add a test with a
fake Docker that has no base tag. Kept out of row 197 r6 by Sage
(sage-1883). Mos asked for this record in mos-1162. (2026-10-21)
- **Case 1 acceptance script: root parses with the node it's checking, and the header says less than root does.**
(Duckworth N1-N3 on the row 195 manifest, REVIEW-m1 6afcee2a5.)
N1: F9 runs `$ROOT$NODE` as root (`scripts/case1-accept.sh:298`)
before F10 checks that `--node` is the `/opt/mosaic-stack/node-<pin>`
path (:303-304). On a host installed as designed, `case2-unit.sh:128-133`
refused a non-root-owned `--node` and the args file is root's, so no
escalation path exists today. Run F9's parse through `AS_USER`, or
apply F10's path check first. N2: the header (:10) and the TOOLS.md
entry say root only reads, but root also runs `curl` (F2), `sudo -l`
(P1, P3), the jarvis probe (P9), `hostcheck.sh --check` and
`systemctl`. Doc-only. N3: F6's prefix match (:261) accepts a
`--claude` with `..` after `claude-<pin>/`. That claude runs only as
`mosaic`, and F10 uses equality. Darkwing has a follow-up on
`darkwing/row195-n1` 1e3c4767c, not yet run; it gets its own row now
that 195 has landed (b65014647), Duckworth reviews. (2026-10-21)
- **Morgana's row 213 mutation set: the B3 pattern no longer matches `bundle.mjs`.**
(Webby N3 on row 221 round 1, REVIEW-r1 a0a00630e.) Row 221's
candidate changes `packages/harness/src/bundle.mjs`, and the B3
anchor at Morgana's 213 `mutants.py:48` stops matching on that
base. This matters only if someone reruns that set. Fix the anchor
in that tooling before any rerun, and check that a stale anchor
refuses instead of skipping the mutant (sage-1941). (2026-10-21)
- **Grant executor test X5 starts a `sleep 30` that can outlive its test.**
(Webby N1 on row 248 r1, webby-0080; deferred by Duckworth in the row
249 review, REVIEW-r1 28174fe08, #1751 comment 30070.) The test at
`packages/bus/tests/grant-executor.test.mjs:989` (:934 before row
249) runs `sleep 30` on the sshd side, where an abort doesn't reach it. Fix it with a stub
that exits once its test dir is gone, as row 248 did for the hold
stubs, or with a shorter sleep. This binds the next row that touches
that test file. Until then, a lane cleanup that runs after this file
waits 30 s before it calls a lone `sleep 30` a leak. The row 197
lanes scan only at lane end, about 15 minutes after pkg-bus, so they
can't see it (mos-1169). (2026-10-21)
- **Row 66 live checks: the unslop notice on a live managed session, both harnesses.**
(mos-1186, Vikunja #454.) Row 66's gate asked for one landing session per
harness with a prompt that draws a known tell. Mos changed that gate. The
Claude Code session ran before landing on a scratch root from the
candidate tree (run r4ab24630d323, landed 2c38325ec). Pi ran launch only there (run r1dd94171bede):
this host has no credential pi reads until P19 #383 settles. Two live
checks remain. Claude Code: in the first re-pin whose tree includes row 66,
backup first (Q173). Pi: one full session with a tell after #383. Each
attaches the operator note on the RESULT, the next-turn correction and the
`unslop.jsonl` counts. (2026-10-21)
- **Console tester listener: `testerNoSignIn` is a dead parameter.**
(Duckworth N2 on the row 196 manifest, REVIEW-m1; carried as C1 in
REVIEW-m2 cdbf7b64e, #1694 comment 30156.) Row 196 landed 7247d80c7
with the parameter and its comment still at `serve.mjs:472-475` and
passed from `webui/src/cli.mjs:109` and `sso.test.mjs:392`. Nothing
in `serve.mjs` reads it after the destructure. Remove it, or wire it
with a test. This binds the next row that touches
`packages/webui/src/serve.mjs`. (2026-10-21)
- **Console library routes: the tester listener and `library.sync`.**
(Duckworth N5 on row 196 round 2, REVIEW-m2 cdbf7b64e.) At 7247d80c7
`startServer` (`serve.mjs:475`) takes none of `libraryFile`,
`libraryStatus` or `librarySync`, so the callbacks `cli.mjs` passes
do nothing yet. The row that adds the Console library routes decides
whether the tester listener gets `library.sync`. Duckworth's view is
that it shouldn't, because sync is a human write. (2026-10-21)
- **Console library routes: four library codes still map to 502.**
(Dewey N2 on the row 254 manifest, #1759 comment 30173.) Row 254
gave six library codes a STATUS entry in `packages/webui/src/bus.mjs`.
`library-mapping-invalid`, `library-remote-invalid`,
`library-state-invalid` and `library-git` still fall through
`busStatus` to 502. The row that adds the Console library routes
decides their statuses and gives its `humanCall` for `library.sync`
a timeout over 130 seconds. (2026-10-21)
- **T27 sweep test leaves two `sleep 60` children running.**
(Darkwing row 251 slot, darkwing-0113 and darkwing-0117; Dewey on
r2, comment 30168.) The sweep test spawns two detached shells,
`planted` at `host-run.test.mjs:528` and `decoy` at `:536`, each
running `sleep 60; :` with the run dir as `$0`. `sweepRunDir` kills
the planted shell by pid (`host-run.mjs:238`), and `t.after` kills
only the decoy shell. Neither kill reaches the `sleep` child, so each
one lives for up to 60 s after the test and trips any busy check that
follows. The fix is to kill the process group in both `t.after`
hooks: `process.kill(-pid, 'SIGKILL')`. Each shell is spawned
detached, so it leads its own group. `exec sleep 60` doesn't work.
The sweep matches the run dir in `/proc/<pid>/cmdline`
(`host-run.mjs:236`), and exec drops the `$0` that carries it, so the
planted process would survive and the "planted process killed" assert
would fail. This binds the next row that touches
`packages/bus/tests/host-run.test.mjs`. (2026-10-21)
- **Real-model suite cases run outside the gate.** Row 244 moved
`test-task.sh` fork recall (M11) and user recall (M16) behind
`MOSAIC_LIVE_PROVIDER=1`, and `test-release.sh` activations to the mock
adapter. Before any re-pin whose tree changes `packages/harness` or the
session fork code (`scripts/mosaic-task.mjs` session and
`sessionForkFrom` handling, the pi adapter), an operator runs
`MOSAIC_LIVE_PROVIDER=1 bash scripts/test-task.sh` with the real pi
credential and a real `release.sh activate`, and records both results on
the re-pin row. Morgana's wording (row 244 BUILD.md), Webby N2 on row 244
r1 (#1757). (2026-10-21)
## Queue
Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
## Done
- Ledger (piece 3, #1506): shipped cd0aa5fb, Gate D written 2026-09-12 (18.9 human messages per closed issue).
- Relaunched seat shows the old last message (darkwing, Step 3 log, 2026-09-12): closed by #1512 relaunch activity, af4203ca. Filbert authored it; Darkwing and Dewey reviewed. The live board restart and Gate F remain separate.
- Sage launcher hook uncommitted (2026-09-12): closed by the agents/sage launch files commit, Dewey review R1 revise, R2 approve (`agents/dewey/work/sage-launch-review-2026-09-26.md`), launcher test now covers sage.
- Ledger counts T3 agent messages as human (2026-09-26, #1506): fixed in ef0020ad (Darkwing 6a, Filbert reviewed). `messageKind` now counts a T3 header as agent and the control-board sender as board. It changes no current count, because no source the ledger reads has a T3 header yet. That waits for the T3 thread source (Gate F).
- Control board checked the bind address but not Host or Origin (2026-09-26, #1507, found by Dewey): closed by the board guard commit. `foreignRequest` refuses a non-loopback Host, a wrong port, userinfo or a path in Host, and any foreign Origin on every route. Dewey authored it, and Rocko approved it (`agents/rocko/work/board-guard-review-2026-09-26.md`, 5a12f08e). Rocko's one low finding: URL normalization accepts a root slash, empty userinfo and an empty query in Host, so the notes' wording overstates what the raw-syntax check refuses. No bypass was found.
- Discord engine: leaked fake pi and the timeout gap in `busy` (2026-09-26, #1509): fixed in 3edb15eb (Darkwing 6b R2, Rocko approved, review ed5510a0). Tests stop the engine in `finally`. A turn pi never started now stops pi with exit 1 instead of guessing, and the unit restarts it. Known limit: see the open entry on wedge restarts.
- Combined concurrent test hangs (2026-09-14, #1509): closed on evidence from 3edb15eb. At default concurrency the union passed 406/406 three times on the R2 snapshot and 408/408 on the commit's index, where clean HEAD had hung four runs out of four. The inherited-intermittency entry from 2026-09-15 stays open until a later run shows it is the same cause.
- A nested `node --test` passes even when its tests fail (2026-09-26, #1508): closed. Queue A1 (34a72af9) clears NODE_TEST_CONTEXT on its own path. N13 fixes `test-foundation.sh` and `test-discord.sh`, each with a check that fails the suite if the clearing is lost. Darkwing wrote it (n13.patch 00868b2f) and Filbert approved it (e464be6c). Sage's grep on 2026-09-27 finds no other nested `node --test`: in scripts, only the two suites, `test-queue.sh` and `queue-commit.sh`, and in JS, only the queue test that covers the fix.
- SetSpark service accepted any string as a decision approver (2026-09-26, #1509): closed. Fixed in shared-signals cc74d92 (Rocko approved R2, lead decision 24). The operations seat (T3 12fe8cda) deployed it to VM 1022 at 00:23Z on 2026-09-27. The running `service.py` hash matches cc74d92, and `/healthz` returns 200 inside the network and through the public tunnel. The read-only README survey found one row breaking the rule (DEC-009, Proposed, version 1), one open request that isn't ready (DEC-009's), and no Accepted decision with more than 16 approvers. No rows changed. DEC-009 stays as stored (lead decisions 21 and 28). The service now refuses approvals on it, so nothing can go wrong silently. Side effect: `up -d` also recreated caddy, about 20 s of edge downtime for all hosts. That is for the shared-signals deploy procedure to fix, not Mosaic.
- Queue suite failed its live checks outside the canonical checkout
(2026-09-27, #1508): closed by Piece D, f539466f. `test-queue.sh` now
skips `verify` and `render --check` with a printed reason when the
toplevel isn't the recorded `canonicalRoot`. An index export passed
27/0, and the canonical checkout 29/0.
- Queue: an owner listed as a reviewer blocked the row (2026-09-27,
#1508): closed by row 31, bb2fa970. `add`, `set reviewers` and `assign`
now refuse the owner as a reviewer with exit 2 and write nothing. Rocko
built it in the Gate G run, and Filbert approved round 1 (#1508 comment
26629, 8 of 8 mutants caught).
- Queue follow-ups from rows 12, 13 and 31 (2026-10-04, #1508): closed by
row 33, 6fb50cc0. Filbert built it, and Darkwing approved round 1
(#1508 comment 26651).
- `genesis` refuses a map row whose owner is one of its reviewers (exit
2, nothing written). The check is on the map, not in replay, so a log
from before the rule still loads.
- The `assign` refusal now reads "can't assign row N to SEAT: SEAT is
one of its reviewers".
- `queue-commit.sh` compares HEAD with H before each canary and again if
the clean run fails. A moved HEAD exits 2 with "HEAD moved since H was
recorded"; the guard message is kept for a real guard failure.
- Every time and date the validator accepts must format back to itself,
so month 13, day 32, 2026-02-30, 2027-02-29 and 24:00 are refused.
The live log at rev 45 still replays.
- The cold-start failure (2026-09-27): not reproduced in 20 cold runs.
Each ran `node --test packages/queue/tests/` in a fresh tree, 10
shared clones and 10 exports, one after another. All 20 passed
148/148 (2960 of 2960). The 300 ms deadline test is unchanged. Counts:
`agents/filbert/work/queue-33/cold-runs.txt`.
- CHAT-03 I1 round 2 follow-ups (2026-10-04, #1507): four of five closed
by slice 1 S5 (row 40, #1522; Dewey authored, Darkwing and Filbert
reviewing). The commit is named in the row's review record when it
lands.
- Engine command and `preArgs`: `engine` takes only `extraArgs`, `cwd`
and `envKeys`. The controller always launches the pinned Pi and
checks that at construction and at bind. The fake engine comes in
through a symbol key that JSON config can't carry (N24b).
- Engine environment: an explicit list (`ENGINE_ENV` in `pi-pin.mjs`)
plus `*_API_KEY` and `*_TOKEN` names from `engine.envKeys`. Nothing
else is inherited. `HOME` passes, so Pi reads `~/.pi/agent` unless
`PI_CODING_AGENT_DIR` is set (Darkwing F3).
- `escalating` is cleared when the force-stop fence throws (Darkwing F2,
races.test.mjs).
- Takeover then Enter in one chunk: input after Ctrl-T or Ctrl-O waits
for that action, in the same chunk or a later one, and also while an
earlier action is still pending, so it is judged as if typed one key
at a time (Filbert F2; Darkwing and Filbert T1 on #1522;
flows.test.mjs).
The frozen-cgroup test (Filbert F1) stays open above.
- Gate runs mount the host's pi auth file (Webby N4 on the row 197 proof
driver, 059577886): closed by row 244 (#1757), landing e69260a3c. The
suites and the lane's `cleanEnv` export a 0600 fixture `PI_AUTH_FILE`,
`guardDocker` refuses a Compose run without one, and
`packages/cli/tests/gate-lane-auth.test.mjs` proves no gate step mounts
the host default. The real-model cases this moved off the gate are open
above.
## Row253 F5: legacy Case AS_USER supervision
Tracked as row 276, Gitea #1781 (sage-2331), owner Gander, reviewer Duckworth, after row253.
Brief: docs/plans/2026-10-22_case-sudo-legacy-followup.md, “Brief: supervised legacy AS_USER calls”.
case1-accept.sh's legacy timeout330 around AS_USER talk and its AS_USER precheck, Claude version, PM launches and Node version diagnostics predate the new socket metadata/probe helpers. sudo's pty monitor can move them to a separate session; the old outer timeout does not prove their descendants are killed. The C4 repair in row253 covers only its new config query and peer probe. These legacy paths need the reviewed supervisor or a header explicitly explaining why each remains unsupervised. The follow-up must preserve the 300-second talk/reply contract, concurrent F5 scan, diagnostics and secret-output refusals, with fake-only session-hop tests and Sage's slot. No host or live unit change is authorized.
Row 276 / #1781 also owns the case1 root-side capture/tools mutant counterparts (duckworth-0112 N1) and documenting the setuid-member cleanup refusal in the headers (N2). A member that changes uid and cannot be killed causes inner-cleanup refusal, never successful observation. N4 is addressed in row253 by allowing SID confirmation until the existing inner watchdog expires, instead of a separate 200 ms scheduling margin.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,76 @@
--- a/scripts/case2-unit.sh
+++ b/scripts/case2-unit.sh
@@ -163,5 +163,3 @@
- local pid="$1" raw rest
- if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
- [ -e "/proc/$pid/stat" ] && return 1
- return 3
- fi
+ local pid="$1" raw="" rest
+ # EOF is expected: stat has no NUL. Decide on content, not read's status.
+ IFS= read -r -d '' raw < "/proc/$pid/stat" 2>/dev/null || :
@@ -181,3 +179 @@
- local entry pid rc live until
- clock_cs || return 1
- until=$((CLOCK_CS + 25))
+ local entry pid rc live until=""
@@ -200,0 +197,2 @@
+ # Inventory pass 1 is outside the bounded confirmation/retry budget.
+ [ -n "$until" ] || until=$((CLOCK_CS + 25))
@@ -221 +219,2 @@
-for ((attempt=0; attempt<100; attempt++)); do
+# Allow session creation the whole inner budget, rather than a 200 ms margin.
+while :; do
--- a/scripts/case1-accept.sh
+++ b/scripts/case1-accept.sh
@@ -121,5 +121,3 @@
- local pid="$1" raw rest
- if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
- [ -e "/proc/$pid/stat" ] && return 1
- return 3
- fi
+ local pid="$1" raw="" rest
+ # EOF is expected: stat has no NUL. Decide on content, not read's status.
+ IFS= read -r -d '' raw < "/proc/$pid/stat" 2>/dev/null || :
@@ -139,3 +137 @@
- local entry pid rc live until
- clock_cs || return 1
- until=$((CLOCK_CS + 25))
+ local entry pid rc live until=""
@@ -158,0 +155,2 @@
+ # Inventory pass 1 is outside the bounded confirmation/retry budget.
+ [ -n "$until" ] || until=$((CLOCK_CS + 25))
@@ -179 +177,2 @@
-for ((attempt=0; attempt<100; attempt++)); do
+# Allow session creation the whole inner budget, rather than a 200 ms margin.
+while :; do
--- a/packages/cli/tests/case2-unit.test.mjs
+++ b/packages/cli/tests/case2-unit.test.mjs
@@ -236,0 +237,22 @@
+});
+
+test('row253 C4 cleanup budget starts after inventory pass one',()=>{
+ // A deterministic clock models a slow inventory without host-load timing.
+ // All process reads and signals are stand-ins; no real PID is signalled.
+ const source=read(UNIT);
+ const body=source.slice(source.indexOf('stat_fields() {'),source.indexOf('finish() {'));
+ const standins=`
+child=424242; sleep_bin=:; seen=0; clocks=0; kills=0
+stat_fields() {
+ seen=$((seen+1)); ST_STATE=R; ST_SESSION=0
+ [ "$seen" != 1 ] || ST_SESSION="$child"
+ return 0
+}
+clock_cs() { clocks=$((clocks+1)); CLOCK_CS=$((clocks*1000)); }
+kill() { kills=$((kills+1)); return 0; }
+clean_session; rc=$?
+printf '%s' "$kills"
+exit "$rc"
+`;
+ const r=spawnSync('/bin/bash',['-c',body+standins],{encoding:'utf8'});
+ assert.equal(r.status,0,r.stderr);assert.equal(r.stdout,'1');
--- a/docs/plans/DEFERRED.md
+++ b/docs/plans/DEFERRED.md
@@ -461,0 +462,2 @@
+
+Row 276 / #1781 also owns the case1 root-side capture/tools mutant counterparts (duckworth-0112 N1) and documenting the setuid-member cleanup refusal in the headers (N2). A member that changes uid and cannot be killed causes inner-cleanup refusal, never successful observation. N4 is addressed in row253 by allowing SID confirmation until the existing inner watchdog expires, instead of a separate 200 ms scheduling margin.
@@ -0,0 +1,9 @@
{
"scripts/case2-unit.sh": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"scripts/case1-accept.sh": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"packages/cli/tests/case-sudo-fixture.mjs": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
"packages/cli/tests/case2-unit.test.mjs": "1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44",
"packages/cli/tests/case1-accept.test.mjs": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901",
"packages/webui/README.md": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9",
"docs/plans/DEFERRED.md": "3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f"
}
@@ -0,0 +1,282 @@
[
{
"id": "M1",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "4de5294507816ddc4114c418a6bb7c197b3fefe92f19b3b18cb95384e23de594"
},
{
"id": "M2",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "caf88ee7ad50b89169a0f57e12b696fe3828238af2ef1008ba6b6016ea7b2528"
},
{
"id": "M3",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "4401886e41fdb4c27610198b66f12d0b3103c5422e0862e9cea30a6f67c93fde"
},
{
"id": "M4",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "ef9a92bb24c72832bdba4d782bf4662394ee05e06a12b750a23d488d9181cd1d"
},
{
"id": "M5",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "4ab80b9db782a97c935f732bec87ac3bb45b58bf9076bc9f9abb4970fef1cada"
},
{
"id": "M6",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "7064e1926c77aedb5453c7771cd42091a5e90fc546821ddbb75d39e6a87c5a31"
},
{
"id": "M7",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "aece2a211aefa1dc4cfb6a3540f922ee5e7f23de26ce974b7655f8fe1722ee16"
},
{
"id": "M8",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "076d4f8f73570a21ad512b12eb5f6f494cb246b2a9d071fea27cc3c126efca2b"
},
{
"id": "M9",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "244cee7128d3b6e380ff24ab9e31f6c456d5153dddf88bb4719028277f523276"
},
{
"id": "M10",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "0e7d369f34cd41708d7b5104113093ebac0296d08902193ced03a1a2bbb83567"
},
{
"id": "M11",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "84181be89249975dd48e1a5daa51eab4bae7b483ea8edf5d6c0cdf373459c046"
},
{
"id": "M12",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "7f8b45e467b197bc00fb740a6507ee85cae332e404e8ffdd8c749b4264239e97"
},
{
"id": "M13",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "85ec11381f4e6662ec1729d84c57022e2f573a2f4c2682250fa005803cdfd63c"
},
{
"id": "M14",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "131f78c490b5ec26142c0703be0b5341551dd9d154c13e519604215fe1240b67"
},
{
"id": "M15",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "077b2eb210c853a03152733f4aa99e450ef0fa17d28f97f8adb78229b02ef147"
},
{
"id": "M16",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "2439d201b902c13fe3903a2d936c8700f0124a430b7d9b2877d485e1f381d3fb"
},
{
"id": "M17",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "1be36e62095e97c64c0bdc5d50b4a3a2e6dd18f1c45cfe1da0fe66cbb8aebd61"
},
{
"id": "M18",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "f5e61c286c51e5a2b26acab22fc5a32c35add0cb8126dc25b5ce747677e34888"
},
{
"id": "M19",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "2f337c03acc60978f1850275a2c8ba6d1b2c74befeb8d73f94d2b477e43b8c56"
},
{
"id": "M20",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "99689c22c6eb73ff9d599ba7305da552357baa99145305ca400f083017bbe3b1"
},
{
"id": "M21",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "bff90f2b1f59086575f98ff96dbd51bacb0265342f595b4bee96cb228d67c030"
},
{
"id": "M22",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "7a8cb7ce47774ab80a55331fc831b7458487be7187b10128746321b375dc83ff"
},
{
"id": "M23",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "e6814801188a6eb98f705d93ac123e988e4daf43afbe96464451f925b1c98d1c"
},
{
"id": "M24",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "ea8d8875f0b4503a6cf4fb96bd88184af65a7f61d1236b9b49baacc748b0c741"
},
{
"id": "M25",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "cfed9261a03eafcdcfb903226b0ddb3fba779d1105e3b4c411b66c10e1672d11"
},
{
"id": "M26",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "1a245b5403b1b9b5f25dfb8cc8ee24acf99121ff063277d52c928ba18543f997"
},
{
"id": "M27",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "efaa027cf7ec3252a675be80219e40fa7b5a3317b28c21fe02363d23ec6e5323"
},
{
"id": "M28",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "bb734cc1a42c0853068f77154dddaaee8eeef5696f67de2e27d1a898409f3b44"
},
{
"id": "M29",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "a6f353aa419862a473eee28e41c5208b464a378ae23e14a73b62ed92bd36cd1f"
},
{
"id": "M30",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "d22cf0f492178b3071604abd550355573c28bf4c26f4a3582f6895710ad3d788"
},
{
"id": "M31",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "3e7450a19cf152653bf6e73be39ef9fb552ec7a656564ca35e44bc12f1586ef5"
},
{
"id": "M32",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "ff11fc1013d70d861643cfd70aa964422cb6a82fb8b4b959bae04703803838b9"
},
{
"id": "M33",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "1c2a58bef2369d8af90b65f9473dd7a112f484db983d8460e2524cb8a673f18b"
},
{
"id": "M34",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "13c1ab861017f3c3c65f50d34a64d7028482e3bc11a8e5908632ec6685416025"
},
{
"id": "M35",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "8f05c1a3a18c5294c63a5853317ce5a5066303145410039629697b5d15d99bfe"
},
{
"id": "M36",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "c1247a7cab2c24e0ab017c49f66797ed16149d3f52cfa41c0968f154261d0d00"
},
{
"id": "M37",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "f210b9c796322333849bc4897b9db3e44bd573fef24c4ba757151981b891f11b"
},
{
"id": "M38",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "736418014b96a09ebe82d996ff6f45b9eb1f3c1e0ada274c505f89fbbd732783"
},
{
"id": "M39",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "1486b8907be4f327bc17b64ddf615cd8b3db1193c185f7934bcba1c7ccc5ec31"
},
{
"id": "M40",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "2f654a76d887ca62525e01c307a2a8f828ee426087dc63a2d4df6a07dc525d53"
}
]
@@ -0,0 +1,142 @@
// Shared fake process model for the two Case host-script tests. No real sudo.
// Call case_sudo_run RECORD_DIR COMMAND... after the caller's argv checks.
// Missing session-mode preserves the original exec-style fake.
import { spawn } from 'node:child_process';
import { readFileSync, readdirSync } from 'node:fs';
export const CASE_SUDO_SESSION = String.raw`
case_sudo_entry() {
local record="$1" here sid; shift
here="$BASHPID"
read -r sid < <(/bin/ps -o sid= -p "$here")
printf "%s %s\n" "$here" "$sid" >> "$record/session-pids"
if { [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
{ [ -f "$record/stall-probe" ] && [[ "$*" == *SO_PEERCRED* ]]; }; then
trap '' TERM HUP
[ ! -f "$record/stall-output" ] || /bin/cat "$record/stall-output"
exec /bin/sleep 60
fi
exec "$@"
}
case_sudo_run() {
local record="$1" child="" rc=0 mode=exec; shift
[ ! -f "$record/session-mode" ] || mode="$(<"$record/session-mode")"
if [ "$mode" != hop ]; then case_sudo_entry "$record" "$@"; return "$?"; fi
# The fake stays as parent, just as sudo does on its pty path. KILL cannot
# be caught or forwarded. The child creates the separate terminal session.
trap '[ -z "$child" ] || kill -TERM -- "-$child" 2>/dev/null || :' TERM
trap '[ -z "$child" ] || kill -HUP -- "-$child" 2>/dev/null || :' HUP
trap '[ -z "$child" ] || kill -INT -- "-$child" 2>/dev/null || :' INT
export -f case_sudo_entry
/usr/bin/setsid /bin/bash -c 'case_sudo_entry "$@"' -- "$record" "$@" &
child=$!
# An interrupted wait returns while the child still runs; keep waiting.
while :; do
wait "$child"; rc=$?
kill -0 "$child" 2>/dev/null || break
done
return "$rc"
}
`;
export function liveRecordedSessions(recordFile, readFile, procDirs) {
// Exec fake entries share the test runner's session. Only self-led sessions
// are isolated and safe to scan here; exec coverage also checks exact PIDs.
const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number)).filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
const live=[];
for(const entry of procDirs()){
if(!/^\d+$/.test(entry))continue;
try{
const stat=readFile(`/proc/${entry}/stat`), fields=stat.slice(stat.lastIndexOf(') ')+2).split(' ');
if(sessions.has(Number(fields[3])) && fields[0]!=='Z') live.push(Number(entry));
}catch{}
}
return live;
}
export function recordedPids(file) {
try{return readFileSync(file,'utf8').trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number));}
catch(error){if(error.code==='ENOENT')return [];throw error;}
}
export function recordedLivePids(file) {
return sessionPairs(recordedPids(file)).filter(([pid,sid])=>{
try{
const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
return Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X';
}catch(error){if(error.code==='ENOENT'||error.code==='ESRCH')return false;throw error;}
}).map(([pid])=>pid);
}
export function killRecordedPids(file) {
killPairs(recordedPids(file));
}
function killPairs(pairs) {
for(const [pid,sid] of sessionPairs(pairs)){
try{
const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
if(Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X')process.kill(pid,'SIGKILL');
}catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
}
}
function sessionPairs(pairs) {
// Only self-led records authorize a session scan. Exec-mode records belong
// to the runner's session and authorize checking their exact PIDs only.
const sessions=new Set(pairs.filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
const found=new Map(pairs.map(pair=>[pair[0],pair]));
if(sessions.size)for(const entry of readdirSync('/proc')){
if(!/^\d+$/.test(entry))continue;
try{
const raw=readFileSync(`/proc/${entry}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' '),sid=Number(fields[3]);
if(sessions.has(sid))found.set(Number(entry),[Number(entry),sid]);
}catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
}
return [...found.values()];
}
// Safety net for a removed-outer-bound change. It is fixture cleanup, never
// product supervision. The guard reads only fixture records and checks sid
// immediately before every kill. Normal outer-bound cases return before it.
export function fixtureGuard(t,files,milliseconds=9500) {
const program=`const fs=require('node:fs');setTimeout(()=>{for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}},Number(process.argv[2]));`;
const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds)],{stdio:'ignore'});
const stop=async()=>{
if(child.exitCode!==null||child.signalCode!==null)return;
const exited=new Promise(resolve=>child.once('exit',resolve));
child.kill('SIGKILL');await exited;
};
t.after(stop);return stop;
}
export function cleanupFixture(t,files) {
let captured=[];
const capture=()=>{captured=files.flatMap(recordedPids);};
// tmp(t)'s directory-removal hook may run first. Retain PID/SID pairs in
// memory before assertions, then verify the kernel sid again before killing.
t.after(()=>killPairs([...captured,...files.flatMap(recordedPids)]));
return capture;
}
export async function pollFixtureGone(files) {
for(let n=0;n<50;n++){
if(files.every(file=>recordedLivePids(file).length===0))return [];
await new Promise(resolve=>setTimeout(resolve,10));
}
return files.flatMap(recordedLivePids);
}
export function blockingQuerySource(records,kind='signals',member,proof) {
const py=`import os,sys\nos.setpgid(0,0)\nwith open(sys.argv[2],"a") as f:f.write(str(os.getpid())+" "+str(os.getsid(0))+chr(10))\nos.execv(sys.argv[1],[sys.argv[1],"60"])`;
return `import fs from 'node:fs';import {spawn} from 'node:child_process';
const records=${JSON.stringify(records)};
function record(pid){const s=fs.readFileSync('/proc/'+pid+'/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);fs.appendFileSync(records,pid+' '+sid+String.fromCharCode(10));}
record(process.pid);
export function loadSystem(){
${kind==='sleep'?`const c=spawn('/usr/bin/sleep',['60'],{stdio:'inherit'});record(c.pid);`:''}
${kind==='signals'?`process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});`:''}
${kind==='member'?`const c=spawn('/usr/bin/python3',['-I','-S','-c',${JSON.stringify(py)},${JSON.stringify(member)},records],{stdio:'inherit'});for(let n=0;n<200;n++){try{const s=fs.readFileSync('/proc/'+c.pid+'/stat','utf8');if(s.includes('('+${JSON.stringify(member?.split('/').at(-1))}+') ')){fs.writeFileSync(${JSON.stringify(proof)},s);break;}}catch{}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,5);}`:''}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
}`;
}
@@ -0,0 +1,706 @@
#!/usr/bin/env bash
# Row 195 (#1693): the Case 1 acceptance on 10.1.1.53, design r7 section 5
# (agents/darkwing/work/queue-195/DESIGN.md). Deviations and decisions: agents/darkwing/work/queue-195/BUILD.md.
#
# case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N
#
# Root runs it on the Case 1 host after the install window, from a root-owned checkout. It reads the
# user, home, copy, node, claude, main port and tester socket marker from the args file, which must pass stack-upgrade.sh's
# rules and be a factory file for --business. Every command that runs the stack's code runs as
# `sudo -u USER -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin` from /. Root itself reads ss,
# /proc, the account database and file metadata, and runs these: systemctl show and is-enabled (F1,
# P11), curl on the Console's loopback healthz (F2), sudo -l (P1, P3), sudo -u jarvis bwrap --version
# and hostcheck.sh --check (P9), and node to parse managed-settings.json (F9). F9 runs node only when
# --node is the pinned path and it and every directory above it are root's with no group or other
# write. The script changes nothing on the host but the message F3 sends and its reply, and it never
# opens claude-session.env. A bounded config/target query imports the copy only as the stack
# user, with sudo and an empty environment. Root parses its two facts in bash. P10 uses the
# fixed system Python, also as the stack user, for a read-only SO_PEERCRED observation.
# The config/probe supervisor is fixed root-validated system Bash plus setsid
# and sleep, after the uid drop: query inner/outer 5/7 s, probe 2/4 s. Root
# independently bounds its sudo wait. Legacy AS_USER talk and diagnostics below
# retain their old behavior and are NOT covered by this supervisor (F5 follow-up).
#
# One line per check, `<id> <path> <mark> <evidence>`, then `count <mark> N` for each mark and a
# result line. F checks are `pass` or `FAIL`. P checks are `closed`, `accepted <ruling>` or
# `unnamed`, which means no Q182, Q183 or Mos ruling names the path; each unnamed line goes to Mos.
#
# Exit 0 when every F check passes, 1 when one fails, 2 on usage or a refusal (a bad args file, or
# --root misused). A P check never changes the exit.
#
# Test seam, for packages/cli/tests/case1-accept.test.mjs only. Root takes none:
# --root DIR prefixes the args file, /etc, /proc, the stack's home, the host check and node, and
# reads the account database from DIR/etc/passwd and DIR/etc/group instead of getent
set -uo pipefail
usage() { echo "usage: case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N" >&2; exit 2; }
refuse() { echo "case1-accept: refusing: $*" >&2; exit 2; }
BUSINESS="" ARGS_FILE="" CLAUDE_PIN="" NODE_PIN="" ROOT=""
while [ $# -gt 0 ]; do
case "$1" in
--business) [ $# -ge 2 ] && [[ "$2" =~ ^[a-z0-9][a-z0-9-]*$ ]] || usage; BUSINESS="$2"; shift 2 ;;
--args) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] || usage; ARGS_FILE="$2"; shift 2 ;;
--claude-pin) [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; CLAUDE_PIN="$2"; shift 2 ;;
--node-pin) [ $# -ge 2 ] && [[ "$2" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; NODE_PIN="$2"; shift 2 ;;
--root) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [ -d "$2" ] || usage; ROOT="${2%/}"; shift 2 ;;
*) usage ;;
esac
done
[ -n "$BUSINESS" ] && [ -n "$ARGS_FILE" ] && [ -n "$CLAUDE_PIN" ] && [ -n "$NODE_PIN" ] || usage
if [ "$(id -u)" = 0 ]; then
[ -z "$ROOT" ] || refuse "--root is a test seam, and root takes none"
PATH=/usr/sbin:/usr/bin:/sbin:/bin
else
[ -n "$ROOT" ] || refuse "run it as root"
fi
cd / || refuse "cannot cd to /"
# --- The args file, by stack-upgrade.sh's read_args rules, plus the serve ports ---
f="$ROOT$ARGS_FILE"
[ -f "$f" ] && [ ! -L "$f" ] || refuse "no regular file $ARGS_FILE"
st="$(stat -c '%u %a' -- "$f")" || refuse "cannot stat $ARGS_FILE"
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) ||
refuse "$ARGS_FILE must be $(id -un)'s with no group or other write (it is uid ${st% *}, mode ${st#* })"
mapfile -t ARGS < "$f" || refuse "cannot read $ARGS_FILE"
S_USER="" S_HOME="" A_BUSINESS="" NODE="" COPY_ID="" CLAUDE="" PROFILE="" PORT="" TESTER_SOCKET=0
copies=0 past=""
for ((i = 0; i < ${#ARGS[@]}; i++)); do
a="${ARGS[i]}" v="${ARGS[i + 1]:-}"
if [ -n "$past" ]; then
case "$a" in
--port) PORT="$v"; i=$((i + 1)) ;;
--business) [ "$v" = "$BUSINESS" ] || refuse "serve business differs"; i=$((i + 1)) ;;
--tester-socket) TESTER_SOCKET=$((TESTER_SOCKET + 1)) ;;
--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;
*) refuse "unknown serve flag $a" ;;
esac
continue
fi
case "$a" in
--) past=1 ;;
--pm) ;;
--user) S_USER="$v"; i=$((i + 1)) ;;
--home) S_HOME="$v"; i=$((i + 1)) ;;
--business) A_BUSINESS="$v"; i=$((i + 1)) ;;
--node) NODE="$v"; i=$((i + 1)) ;;
--claude) CLAUDE="$v"; i=$((i + 1)) ;;
--profile) PROFILE="$v"; i=$((i + 1)) ;;
--copy) COPY_ID="$v"; copies=$((copies + 1)); i=$((i + 1)) ;;
*) refuse "$ARGS_FILE has $a before --, which case2-unit.sh does not take" ;;
esac
done
[ "$copies" = 1 ] || refuse "$ARGS_FILE needs exactly one --copy before --, not $copies"
[[ "$S_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] && [[ "$S_HOME" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$A_BUSINESS" =~ ^[a-z0-9][a-z0-9-]*$ ]] &&
[[ "$NODE" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] && [[ "$CLAUDE" =~ ^/[A-Za-z0-9/._+@-]+$ ]] ||
refuse "$ARGS_FILE lacks a plain --user, --home, --business, --node, --claude or --copy"
[ "$PROFILE" = factory ] || refuse "$ARGS_FILE has --profile '${PROFILE}', not factory"
[ "$A_BUSINESS" = "$BUSINESS" ] || refuse "$ARGS_FILE is for business $A_BUSINESS, not $BUSINESS"
[[ "$PORT" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$PORT" -le 1023 ] || refuse "$ARGS_FILE lacks a privileged numeric --port after --"
[ "$TESTER_SOCKET" = 1 ] || refuse "$ARGS_FILE needs one bare --tester-socket after --"
COPY="$S_HOME/.local/share/mosaic-stack/copies/$COPY_ID"
DATA_ROOT="$S_HOME/.mosaic-dev"
CRED="$S_HOME/.config/mosaic-dev/secrets/claude-session.env"
BUSINESS_FILE="$S_HOME/.config/mosaic-dev/businesses/$BUSINESS.json"
BUS_UNIT="mosaic-bus@$BUSINESS.service" CON_UNIT=mosaic-webui.service
HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh
SETTINGS=/etc/claude-code/managed-settings.json
LABEL_OUTER='bwrap (enforce)' LABEL_CHILD='bwrap//&unpriv_bwrap (enforce)'
AS_USER=(sudo -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin")
WORK="$(mktemp -d)" || refuse "cannot make a temp dir"
trap 'rm -rf -- "$WORK"' EXIT
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
# row253-supervisor-begin
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
stat_fields() {
local pid="$1" raw="" rest
# EOF is expected: stat has no NUL. Decide on content, not read's status.
IFS= read -r -d '' raw < "/proc/$pid/stat" 2>/dev/null || :
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
[[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
}
clock_cs() {
local boot spare
read -r boot spare < /proc/uptime || return 1
[[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
}
clean_session() {
local entry pid rc live until=""
while :; do
live=0
for entry in /proc/[0-9]*/stat; do
pid="${entry%/stat}"; pid="${pid##*/}"
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" != 3 ] || continue
[ "$rc" = 0 ] || return 1
[ "$ST_SESSION" = "$child" ] || continue
case "$ST_STATE" in Z|X) continue ;; esac
live=1
if ! kill -KILL "$pid" 2>/dev/null; then
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
fi
done
[ "$live" != 0 ] || return 0
clock_cs || return 1
# Inventory pass 1 is outside the bounded confirmation/retry budget.
[ -n "$until" ] || until=$((CLOCK_CS + 25))
[ "$CLOCK_CS" -lt "$until" ] || return 1
"$sleep_bin" 0.01
done
}
finish() {
local rc="$?"
trap - EXIT HUP INT TERM
if [ "$verified" = 1 ]; then
clean_session || rc=86
elif [ -n "$child" ]; then
kill -KILL "$child" 2>/dev/null || :
fi
[ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
"$setsid_bin" "$@" & child=$!
# Allow session creation the whole inner budget, rather than a 200 ms margin.
while :; do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 85
if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
tester_tools() {
local input target p st magic
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
p="$target"
while :; do
st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
[ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
[ "$p" != / ] || break
p="${p%/*}"; [ -n "$p" ] || p=/
done
magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
case "$input" in
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
/usr/bin/head) TS_HEAD="$target" ;;
esac
done
}
tester_capture_reason() {
case "$1" in
81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
*) echo inner-command ;;
esac
}
tester_capture() (
local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
tester_tools || exit 84
file="$(mktemp)" || exit 88
notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
trap 'exit 88' HUP INT TERM
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
set -o pipefail
maximum="$1"; head_bin="$2"; shift 2
"$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
# Only GNU timeout and the trusted head can write this root-side channel.
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
[ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
[ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
[ "${5:-}" != observe ] || return 0
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# --- Output ---
MARKS=(pass FAIL closed "accepted Q182+Q183" "accepted Q183" "accepted mos-1096" unnamed)
declare -A COUNT=()
F_FAILED=0
# Text the stack's user wrote, made safe to print: printable ASCII, one line, at most 200 bytes.
clean() { tr -c '[:print:]' ' ' <<< "$1" | cut -c1-200 | sed 's/ *$//'; }
line() { local id="$1" path="$2" mark="$3"; shift 3; echo "$id $path $mark $*"; COUNT[$mark]=$(( ${COUNT[$mark]:-0} + 1 )); }
fpass() { line "$1" "$2" pass "${@:3}"; }
ffail() { line "$1" "$2" FAIL "${@:3}"; F_FAILED=1; }
# --- The account database: getent on the host, the seam's files under --root ---
db() {
if [ -n "$ROOT" ]; then
if [ $# -gt 1 ]; then awk -F: -v k="$2" '$1 == k' "$ROOT/etc/$1"; else cat "$ROOT/etc/$1"; fi
else
getent "$@"
fi
}
uid_of() { db passwd "$1" | cut -d: -f3; }
gid_of() { db passwd "$1" | cut -d: -f4; }
# The group ids USER belongs to, as `id -G USER` prints them: the primary gid, then each group
# whose member list names USER.
groups_of() {
local g; g="$(gid_of "$1")"
[ -n "$g" ] || return 0
{ echo "$g"; db group | awk -F: -v u="$1" '{ n = split($4, m, ","); for (i = 1; i <= n; i++) if (m[i] == u) print $3 }'; } | awk '!seen[$0]++'
}
names_of_groups() { local g; for g in "$@"; do db group | awk -F: -v g="$g" '$3 == g { print $1 }'; done; }
MOSAIC_UID="$(uid_of "$S_USER")"
[[ "$MOSAIC_UID" =~ ^[1-9][0-9]*$ ]] || refuse "stack account has no non-root uid"
[ "$(db passwd "$S_USER" | cut -d: -f6)" = "$S_HOME" ] || refuse "saved home differs from account home"
QUERY="$(tester_capture 5 7 4096 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query $(tester_capture_reason "$?")"
tester_facts "$QUERY" || refuse "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || refuse "target does not support --tester-socket"
tester_location "$TS_DATA" "$S_HOME" "$MOSAIC_UID" "$ROOT" observe || refuse "tester path refuses $TS_REASON"
TESTER_PATH="$TS_SOCKET"
# --- systemd, /proc and listeners ---
# KEY=VALUE lines for UNIT's properties.
unit_show() { local u="$1"; shift; systemctl show "$u" "${@/#/-p}" 2>/dev/null; }
prop() { sed -n "s/^$2=//p" <<< "$1" | tail -n 1; }
# The real, effective, saved and filesystem uids of PID, space-separated.
proc_uids() { awk '$1 == "Uid:" { print $2, $3, $4, $5 }' "$ROOT/proc/$1/status" 2>/dev/null; }
proc_label() { local l; l="$(cat "$ROOT/proc/$1/attr/current" 2>/dev/null)" || return 1; printf '%s' "${l%$'\n'}"; }
# One `ADDRESS PIDS` line per TCP listener on port $1, PIDS comma-separated.
listeners() {
ss -Hltnp 2>/dev/null | awk -v p="$1" '{
n = split($4, a, ":"); if (a[n] != p) next
addr = substr($4, 1, length($4) - length(p) - 1); pids = ""; s = $0
while (match(s, /pid=[0-9]+/)) { pids = pids (pids == "" ? "" : ",") substr(s, RSTART + 4, RLENGTH - 4); s = substr(s, RSTART + RLENGTH) }
print addr, pids
}'
}
# 0 when port $1 has a listener and every one is on 127.0.0.1, held by pid $2 alone.
loopback_by() {
local addr pids p any=0
[ -n "$2" ] && [ "$2" != 0 ] || return 1
while read -r addr pids; do
[ -n "$addr" ] || continue
any=1
[ "$addr" = 127.0.0.1 ] || return 1
for p in ${pids//,/ }; do [ "$p" = "$2" ] || return 1; done
[ -n "$pids" ] || return 1
done <<< "$(listeners "$1")"
[ "$any" = 1 ]
}
listener_text() { local t; t="$(listeners "$1" | tr '\n' ';')"; echo "${t:-none}"; }
BUS_SHOW="$(unit_show "$BUS_UNIT" ActiveState User MainPID)"
CON_SHOW="$(unit_show "$CON_UNIT" ActiveState User MainPID)"
CON_PID="$(prop "$CON_SHOW" MainPID)"
# --- F checks ---
check_f1() {
local u show state user pid uids why=""
for u in "$BUS_UNIT" "$CON_UNIT"; do
if [ "$u" = "$BUS_UNIT" ]; then show="$BUS_SHOW"; else show="$CON_SHOW"; fi
state="$(prop "$show" ActiveState)" user="$(prop "$show" User)" pid="$(prop "$show" MainPID)"
uids="$(proc_uids "${pid:-0}")"
[ "$state" = active ] || { why+="$u is ${state:-unknown}; "; continue; }
[ "$user" = "$S_USER" ] || { why+="$u has User=${user:-unset}; "; continue; }
[ -n "$MOSAIC_UID" ] && [ "$uids" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] ||
why+="$u MainPID ${pid:-none} has uids '${uids:-unreadable}', not $S_USER's ${MOSAIC_UID:-unknown}; "
done
if [ -z "$why" ]; then fpass F1 units-user "$BUS_UNIT and $CON_UNIT are active as $S_USER (uid $MOSAIC_UID)"
else ffail F1 units-user "${why%; }"; fi
}
check_f2() {
local rc=0
curl -fsS --max-time 10 -o /dev/null "http://127.0.0.1:$PORT/healthz" 2>/dev/null || rc=$?
if [ "$rc" != 0 ]; then ffail F2 console-loopback "curl http://127.0.0.1:$PORT/healthz exited $rc"
elif ! loopback_by "$PORT" "$CON_PID"; then ffail F2 console-loopback "port $PORT listeners are '$(listener_text "$PORT")', not 127.0.0.1 by the Console's MainPID ${CON_PID:-none}"
else fpass F2 console-loopback "healthz answers, and $PORT is bound to 127.0.0.1 only, by MainPID $CON_PID"; fi
}
# F5's scan: an outer /usr/bin/bwrap owned by the stack's user with a claude descendant. Sets F5_OK
# when one pair has both labels exactly, F5_WHY to the last mismatch otherwise.
F5_OK="" F5_WHY="no /usr/bin/bwrap process of $S_USER with a claude descendant was seen while the reply was pending"
scan_f5() {
local pid ppid uids kids q c argv0 comm lo lc
local -A parent=() children=() bw=()
# One read of every PPid line and one of every exe link, so a scan stays short on a busy host.
while read -r pid ppid; do
parent[$pid]="$ppid" children[$ppid]+=" $pid"
done < <(grep -H '^PPid:' "$ROOT"/proc/[0-9]*/status 2>/dev/null | sed -n 's#^.*/proc/\([0-9]*\)/status:PPid:[[:space:]]*\([0-9]*\)$#\1 \2#p')
while read -r pid; do
[[ "$pid" =~ ^[0-9]+$ ]] && [ -n "${parent[$pid]:-}" ] && bw[$pid]=1
done < <(find "$ROOT/proc" -mindepth 2 -maxdepth 2 -name exe -lname /usr/bin/bwrap -printf '%h\n' 2>/dev/null | sed 's#.*/##')
for pid in "${!bw[@]}"; do
[ -z "${bw[${parent[$pid]}]:-}" ] || continue
uids="$(proc_uids "$pid")"
[ -n "$MOSAIC_UID" ] && [ "${uids%% *}" = "$MOSAIC_UID" ] || continue
# Every descendant, breadth first.
kids=() q=("$pid")
while [ ${#q[@]} -gt 0 ]; do
for c in ${children[${q[0]}]:-}; do kids+=("$c") q+=("$c"); done
q=("${q[@]:1}")
done
for c in "${kids[@]}"; do
argv0="$(tr '\0' '\n' < "$ROOT/proc/$c/cmdline" 2>/dev/null | sed -n 1p)"
comm="$(cat "$ROOT/proc/$c/comm" 2>/dev/null)"
[[ "${argv0##*/}" == claude* ]] || [[ "$comm" == claude* ]] || continue
lo="$(proc_label "$pid")" lc="$(proc_label "$c")"
if [ "$lo" = "$LABEL_OUTER" ] && [ "$lc" = "$LABEL_CHILD" ]; then
F5_OK="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc'"; return 0
fi
F5_WHY="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc', not '$LABEL_OUTER' and '$LABEL_CHILD'"
done
done
return 1
}
# F3 and F5 together: the message goes out in the background and the scan runs until it is answered.
check_f3_f5() {
local marker text rc=0 out err n tpid
marker="case1-accept-$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"
text="$marker: this is the Case 1 acceptance; reply with one short line."
timeout 330 "${AS_USER[@]}" "$COPY/scripts/mosaic" talk chief "$text" --business "$BUSINESS" --tester --wait 300 > "$WORK/talk.out" 2> "$WORK/talk.err" &
tpid=$!
while :; do
scan_f5 && break
kill -0 "$tpid" 2>/dev/null || break
sleep 0.5
done
wait "$tpid" || rc=$?
# Both the sent line and the reply are on stdout; an error is on stderr.
out="$(cat "$WORK/talk.out")" err="$(cat "$WORK/talk.err")"
n="$(grep -c . <<< "$out")"
if [ "$rc" = 0 ] && grep -q "^sent [^ ]* to chief" <<< "$out" && [ "$n" -ge 2 ]; then
fpass F3 tester-reply "chief replied to $marker"
else
ffail F3 tester-reply "talk exited $rc with $n lines on stdout; last: $(clean "$(tail -n 1 <<< "${err:-$out}")")"
fi
if [ -n "$F5_OK" ]; then F5_LINE=(fpass "$F5_OK"); else F5_LINE=(ffail "$F5_WHY"); fi
}
check_f4() {
local rc=0 out
out="$("${AS_USER[@]}" "$COPY/scripts/case2-precheck.sh" --business "$BUSINESS" --profile factory 2>&1)" || rc=$?
if [ "$rc" = 0 ]; then fpass F4 precheck "case2-precheck.sh --profile factory exits 0"
else ffail F4 precheck "case2-precheck.sh --profile factory exited $rc; last: $(clean "$(tail -n 1 <<< "$out")")"; fi
}
check_f6() {
local want="/opt/mosaic-stack/claude-$CLAUDE_PIN/" v
[[ "$CLAUDE" == "$want"* ]] || { ffail F6 claude-pin "--claude $CLAUDE is not under $want"; return; }
[[ "$CLAUDE/" != */../* ]] || { ffail F6 claude-pin "--claude $CLAUDE has a .. segment"; return; }
v="$("${AS_USER[@]}" "$CLAUDE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$CLAUDE_PIN (Claude Code)" ]; then fpass F6 claude-pin "$CLAUDE prints '$v'"
else ffail F6 claude-pin "$CLAUDE --version prints '$(clean "$v")', not '$CLAUDE_PIN (Claude Code)'"; fi
}
# The PM check confirm runs (stack-release.sh pm_external): exactly one live launches list entry for
# launch.by, for this business. The stack's user runs the whole check and prints one result line.
F7_JS='
const {readFileSync}=require("node:fs");const {spawnSync}=require("node:child_process");
const [file,cli,b]=process.argv.slice(1);
let by;
try{by=JSON.parse(readFileSync(file,"utf8")).launch?.by}catch(e){console.log(`fail cannot read ${file}: ${e.code??"not JSON"}`);process.exit(0)}
if(typeof by!=="string"||!by){console.log("fail the business file has no launch.by");process.exit(0)}
const r=spawnSync(cli,["launches","list","--json"],{encoding:"utf8"});
if(r.status!==0){console.log(`fail launches list exited ${r.status}`);process.exit(0)}
let l;try{l=JSON.parse(r.stdout)}catch{console.log("fail launches list printed no JSON");process.exit(0)}
const live=Array.isArray(l)?l.filter((x)=>x&&x.instance===by&&x.live===true):[];
console.log(live.length!==1?`fail ${live.length} live launches of ${by}`:live[0].business!==b?`fail the live launch of ${by} is for ${live[0].business}`:`ok ${by} run ${live[0].run}`);'
check_f7() {
local out
out="$("${AS_USER[@]}" "$NODE" -e "$F7_JS" -- "$BUSINESS_FILE" "$COPY/scripts/mosaic" "$BUSINESS" 2>/dev/null | sed -n 1p)"
if [[ "$out" =~ ^ok\ [a-z0-9][a-z0-9-]*\ run\ [A-Za-z0-9._-]+$ ]]; then fpass F7 pm-live "one live launch: ${out#ok }"
else ffail F7 pm-live "$(clean "${out#fail }")"; fi
}
check_f8() {
local s
s="$(stat -c '%U %a %F' -- "$ROOT$CRED" 2>/dev/null)" || s="missing"
if [ ! -L "$ROOT$CRED" ] && [ "$s" = "$S_USER 600 regular file" ]; then fpass F8 credential-file "$CRED is '$s'"
else ffail F8 credential-file "$CRED is '$s', not '$S_USER 600 regular file'"; fi
}
# Empty when $1 and every directory above it, up to /, are the running user's (root's on the host) with
# no group or other write: case2-unit.sh's root_only rule for --node, checked again before root runs it.
# Otherwise the first path that breaks the rule. A symlink breaks it, because its mode is 777.
not_root_only() {
local p="$1" st
while :; do
st="$(stat -c '%u %a' -- "$ROOT$p" 2>/dev/null)" || { echo "$p (cannot stat)"; return; }
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) || { echo "$p (uid ${st% *}, mode ${st#* })"; return; }
[ "$p" = / ] && return
p="$(dirname -- "$p")"
done
}
check_f9() {
local s v bad
s="$(stat -c '%U:%G %a %F' -- "$ROOT$SETTINGS" 2>/dev/null)" || s="missing"
[ ! -L "$ROOT$SETTINGS" ] && [ "$s" = "root:root 644 regular file" ] || { ffail F9 autoupdater-off "$SETTINGS is '$s', not 'root:root 644 regular file'"; return; }
# Root runs node here, so it runs only the pinned path, and only when nothing but root can replace it.
[ "$NODE" = "/opt/mosaic-stack/node-$NODE_PIN/bin/node" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: it is not the --node-pin path"; return; }
bad="$(not_root_only "$NODE")"
[ -z "$bad" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: $bad is not root's alone"; return; }
v="$("$ROOT$NODE" -e 'const s=JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(JSON.stringify(s?.env?.DISABLE_AUTOUPDATER??null))' -- "$ROOT$SETTINGS" 2>/dev/null)" || v="unreadable"
if [ "$v" = '"1"' ]; then fpass F9 autoupdater-off "$SETTINGS is root:root 0644 with env.DISABLE_AUTOUPDATER \"1\""
else ffail F9 autoupdater-off "$SETTINGS env.DISABLE_AUTOUPDATER is $v, not \"1\""; fi
}
check_f10() {
local want="/opt/mosaic-stack/node-$NODE_PIN/bin/node" v
[ "$NODE" = "$want" ] || { ffail F10 node-pin "--node $NODE is not $want"; return; }
v="$("${AS_USER[@]}" "$NODE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$NODE_PIN" ]; then fpass F10 node-pin "$NODE prints $v"
else ffail F10 node-pin "$NODE --version prints '$(clean "$v")', not $NODE_PIN"; fi
}
check_f11() {
local d="$ROOT$DATA_ROOT/discord" found
if [ -L "$d" ]; then ffail F11 no-discord "$DATA_ROOT/discord is a symlink"; return; fi
[ -d "$d" ] || { fpass F11 no-discord "$DATA_ROOT/discord is absent"; return; }
found="$(find "$d" -maxdepth 1 -name '*.json' -printf '%f ' 2>/dev/null)"
if [ -z "$found" ]; then fpass F11 no-discord "$DATA_ROOT/discord holds no binding"
else ffail F11 no-discord "$DATA_ROOT/discord holds bindings: ${found% }"; fi
}
# --- P checks ---
check_p1() {
local out
out="$(sudo -l -U "$S_USER" 2>&1)"
if grep -q "is not allowed to run sudo" <<< "$out"; then line P1 "$S_USER-sudo" closed "sudo -l: $(clean "$(sed -n 1p <<< "$out")")"
else line P1 "$S_USER-sudo" unnamed "sudo -l: $(clean "$(tr '\n' ' ' <<< "$out")")"; fi
}
check_p2() {
local gs keys="$ROOT$S_HOME/.ssh/authorized_keys" kstate n
gs="$(groups_of "$S_USER" | tr '\n' ' ')"; gs="${gs% }"
n="$(wc -w <<< "$gs")"
if [ ! -e "$keys" ] && [ ! -L "$keys" ]; then kstate=absent
elif [ -f "$keys" ] && [ ! -L "$keys" ] && [ ! -s "$keys" ]; then kstate=empty
else kstate=present; fi
if [ "$n" = 1 ] && [ "$kstate" != present ]; then line P2 "$S_USER-groups-keys" closed "id -G is '$gs'; authorized_keys is $kstate"
else line P2 "$S_USER-groups-keys" unnamed "id -G is '$gs'; authorized_keys is $kstate"; fi
}
check_p3() {
local n
n="$(sudo -l -U localadmin 2>&1 | grep -c NOPASSWD)"
line P3 localadmin-sudo "accepted Q183" "sudo -l -U localadmin lists $n NOPASSWD lines"
}
check_p4() { line P4 pve-root-pbs "accepted Q182+Q183" "root on pve over VM 1008, and PBS backups that hold $S_HOME with the credential; this script can't see pve"; }
check_p5() {
local gs reach
if [ -z "$(uid_of jarvis)" ]; then line P5 jarvis-reach closed "no jarvis account"; return; fi
gs="$(names_of_groups $(groups_of jarvis) | tr '\n' ' ')"
reach="$(tr ' ' '\n' <<< "$gs" | grep -xE 'sudo|docker|lxd' | tr '\n' ' ')"
if [ -z "$reach" ]; then line P5 jarvis-reach closed "jarvis is in none of sudo, docker and lxd (groups: ${gs% })"
else line P5 jarvis-reach unnamed "jarvis is in ${reach% }"; fi
}
check_p6() {
local g m members bad=0
for g in sudo docker lxd; do
members="$(db group "$g" | cut -d: -f4)"
for m in ${members//,/ }; do
[ "$g" = sudo ] && [ "$m" = localadmin ] && continue
line P6 "group-$g" unnamed "$m is a member of $g"; bad=1
done
done
[ "$bad" = 1 ] || line P6 group-members closed "sudo holds only localadmin, and docker and lxd have no members"
}
check_p7() {
local found
found="$(grep -rlw -- "$S_USER" "$ROOT/etc/sudoers.d" 2>/dev/null | sed "s#^$ROOT##" | tr '\n' ' ')"
if [ -z "$found" ]; then line P7 sudoers-d closed "no file under /etc/sudoers.d names $S_USER"
else line P7 sudoers-d unnamed "files naming $S_USER: ${found% }"; fi
}
check_p8() {
local other
other="$(listeners 7331)$(listeners 7333)"
if loopback_by "$PORT" "$CON_PID" && [ -z "$other" ]; then
line P8 lan-listeners closed "$PORT is on 127.0.0.1 only by MainPID $CON_PID; no TCP listener on 7331 or 7333; tester uses $TESTER_PATH"
else
line P8 lan-listeners unnamed "$PORT: $(listener_text "$PORT"); 7333: $(listener_text 7333); 7331: $(listener_text 7331); Console MainPID ${CON_PID:-none}"
fi
}
check_p9() {
local out n missing=() rc=0 err mg others
out="$("$ROOT$HOSTCHECK" --check 2>/dev/null)"
for n in 1 2 3 4 5 6; do grep -q "^ok $n " <<< "$out" || missing+=("hostcheck item $n has no ok line")
done
err="$(sudo -u jarvis /usr/bin/bwrap --version 2>&1 >/dev/null)" || rc=$?
[ "$rc" != 0 ] && grep -q "Permission denied" <<< "$err" || missing+=("sudo -u jarvis /usr/bin/bwrap --version exited $rc without Permission denied")
mg="$(gid_of "$S_USER")"
[ -n "$mg" ] && [ -z "$(db group "$S_USER" | cut -d: -f4)" ] || missing+=("group $S_USER lists members, or $S_USER has no group")
others="$(db passwd | awk -F: -v g="$mg" -v u="$S_USER" '$4 == g && $1 != u { print $1 }' | tr '\n' ' ')"
[ -z "$others" ] || missing+=("other accounts have $S_USER's gid $mg: ${others% }")
if [ ${#missing[@]} = 0 ]; then
line P9 bwrap-userns "accepted mos-1096" "hostcheck items 1-6 ok, jarvis is denied bwrap, and group $S_USER has no other member"
else
for n in "${missing[@]}"; do line P9 bwrap-userns unnamed "$n"; done
fi
}
# P10 is an observation of mutable same-uid state, never a closure ruling.
TESTER_PEER='import socket,struct,sys
s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
try:
s.settimeout(1)
s.connect(sys.argv[1])
p=struct.unpack("3i",s.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
print(*p)
finally:
s.close()'
tester_snapshot() {
local parent="${TESTER_PATH%/*}" p s pid
[ ! -L "$ROOT$parent" ] && [ ! -L "$ROOT$TESTER_PATH" ] || return 1
p="$(stat -c '%u %a %F %d %i' -- "$ROOT$parent" 2>/dev/null)" || return 1
s="$(stat -c '%u %a %F %d %i' -- "$ROOT$TESTER_PATH" 2>/dev/null)" || return 1
[[ "$p" =~ ^$MOSAIC_UID\ 700\ directory\ [0-9]+\ [0-9]+$ ]] || return 1
[[ "$s" =~ ^$MOSAIC_UID\ 600\ socket\ [0-9]+\ [0-9]+$ ]] || return 1
pid="$(prop "$(unit_show "$CON_UNIT" MainPID)" MainPID)"
[[ "$pid" =~ ^[1-9][0-9]*$ ]] && [ "$pid" = "$CON_PID" ] || return 1
[ "$(proc_uids "$pid")" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] || return 1
printf '%s|%s|%s' "$pid" "$p" "$s"
}
tester_python() {
local real bad magic
# Resolve the system entry, then validate the target and every ancestor.
[ -f "$ROOT/usr/bin/python3" ] && [ -x "$ROOT/usr/bin/python3" ] || return 1
real="$(realpath -e -- "$ROOT/usr/bin/python3" 2>/dev/null)" || return 1
[ -z "$ROOT" ] || real="${real#"$ROOT"}"
case "$real" in /home/*|/root/*|/run/user/*|"$S_HOME"/*) return 1 ;; esac
bad="$(not_root_only "$real")"; [ -z "$bad" ] || return 1
bad="$(not_root_only /usr/bin)"; [ -z "$bad" ] || return 1
magic="$(od -An -tx1 -N4 -- "$ROOT$real" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
printf '%s' "$real"
}
check_p10() {
local floor before after python peer pid uid gid why="" pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})$'
floor="$(cat "$ROOT/proc/sys/net/ipv4/ip_unprivileged_port_start" 2>/dev/null)"
[[ "$floor" =~ ^[0-9]{1,5}$ ]] && [ "$floor" -gt "$PORT" ] || why=floor
before="$(tester_snapshot)" || why=metadata
python="$(tester_python)" || why=interpreter
if [ -z "$why" ]; then
peer="$(tester_capture 2 4 128 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why="probe-$(tester_capture_reason "$?")"
if [[ "$peer" =~ $pattern ]]; then
pid="${BASH_REMATCH[1]}" uid="${BASH_REMATCH[2]}" gid="${BASH_REMATCH[3]}"
[ "$pid" = "$CON_PID" ] && [ "$uid" = "$MOSAIC_UID" ] && [ "$gid" -le 2147483647 ] || why=peer
else [ -n "$why" ] || why=probe; fi
after="$(tester_snapshot)" || why=metadata
[ "$before" = "$after" ] || why=changed
fi
if [ -z "$why" ]; then
line P10 tester-socket-squat unnamed "boundary=observed path=$TESTER_PATH parent=0700 owner=$MOSAIC_UID socket=0600 peer_pid=$pid peer_uid=$uid floor=$floor port=$PORT; hostile same-uid substitution remains"
else
line P10 tester-socket-squat unnamed "boundary=failed reason=$why path=$TESTER_PATH MainPID=${CON_PID:-none}; hostile same-uid substitution remains"
fi
}
check_p11() {
local en before
en="$(systemctl is-enabled mosaic-hostcheck.service 2>/dev/null)"
before="$(prop "$(unit_show mosaic-hostcheck.service Before)" Before)"
if [ "$en" = enabled ] && grep -qxF -- "$BUS_UNIT" <<< "${before// /$'\n'}"; then
line P11 hostcheck-before-bus closed "mosaic-hostcheck.service is enabled, and its Before= names $BUS_UNIT"
else
line P11 hostcheck-before-bus unnamed "mosaic-hostcheck.service is '${en:-unknown}', Before='$before'; it needs enabled and $BUS_UNIT"
fi
}
check_f1
check_f2
F5_LINE=()
check_f3_f5
check_f4
"${F5_LINE[0]}" F5 bwrap-label "${F5_LINE[@]:1}"
check_f6
check_f7
check_f8
check_f9
check_f10
check_f11
check_p1; check_p2; check_p3; check_p4; check_p5; check_p6
check_p7; check_p8; check_p9; check_p10; check_p11
for m in "${MARKS[@]}"; do echo "count $m ${COUNT[$m]:-0}"; done
if [ "$F_FAILED" = 0 ]; then echo "case1-accept: every F check passes; send the whole output to Duckworth, and each unnamed line to Mos"; exit 0; fi
echo "case1-accept: an F check failed, so the row fails"
exit 1
@@ -0,0 +1,717 @@
// Row 195 (#1693): scripts/case1-accept.sh, root's acceptance run on the Case 1 host. Design r7 section 5
// (agents/darkwing/work/queue-195/DESIGN.md); deviations in agents/darkwing/work/queue-195/BUILD.md.
// Every case runs the script with --root on a scratch host tree: /etc, /proc, /srv/mosaic, /opt and the
// host check live under it. Fake systemctl, curl, ss, sudo and stat on PATH record their argv; the fake
// sudo runs the stack's user's commands from the tree. Nothing here reads or writes the real /etc,
// /proc or /srv, and the stand-in credential is not a credential.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { tmp } from "./helpers.mjs";
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const SCRIPT = join(REPO, "scripts", "case1-accept.sh");
const COPY = "0123456789abcdef0123456789abcdef01234567";
const NODE_PIN = "v24.1.0", CLAUDE_PIN = "2.1.0";
const NODE = `/opt/mosaic-stack/node-${NODE_PIN}/bin/node`;
const CLAUDE = `/opt/mosaic-stack/claude-${CLAUDE_PIN}/node_modules/@anthropic-ai/claude-code/bin/claude.exe`;
const HOME = "/srv/mosaic";
const CLI = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/mosaic`;
const PRECHECK = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/case2-precheck.sh`;
const CRED = `${HOME}/.config/mosaic-dev/secrets/claude-session.env`;
const CRED_VALUE = "stand-in-not-a-credential-7f3e2a";
const AS_MOSAIC = `-u mosaic -H env PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin`;
const ARGS = ["--profile", "factory", "--user", "mosaic", "--home", HOME, "--business", "mosaic-stack", "--copy", COPY,
"--node", NODE, "--claude", CLAUDE, "--pm", "--", "--port", "732", "--business", "mosaic-stack", "--tester-socket"];
const OUTER = "bwrap (enforce)", CHILD = "bwrap//&unpriv_bwrap (enforce)";
const MARKS = ["pass", "FAIL", "closed", "accepted Q182+Q183", "accepted Q183", "accepted mos-1096", "unnamed"];
const P_PASS = { P1: "closed", P2: "closed", P3: "accepted Q183", P4: "accepted Q182+Q183", P5: "closed", P6: "closed",
P7: "closed", P8: "closed", P9: "accepted mos-1096", P10: "unnamed", P11: "closed" };
const PASSWD = "root:x:0:0:root:/root:/bin/bash\nmosaic:x:997:997::/srv/mosaic:/usr/sbin/nologin\nlocaladmin:x:1000:1000::/home/localadmin:/bin/bash\njarvis:x:1001:1001::/home/jarvis:/bin/bash\n";
const GROUP = "root:x:0:\nsudo:x:27:localadmin\nmosaic:x:997:\nlxd:x:998:\ndocker:x:999:\nlocaladmin:x:1000:\njarvis:x:1001:\n";
const SS = "LISTEN 0 511 127.0.0.1:732 0.0.0.0:* users:((\"node\",pid=200,fd=21))\n"
+ "LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:((\"sshd\",pid=900,fd=3))\n";
const HOSTCHECK_OK = [1, 2, 3, 4, 5, 6, 7].map((n) => `ok ${n} item ${n} holds`).join("\n") + "\nnote bubblewrap 0.9.0 is the install pin\n";
function put(file, text, mode) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, text);
if (mode) chmodSync(file, mode);
}
const edit = (file, f) => writeFileSync(file, f(readFileSync(file, "utf8")));
const log = (name) => `printf '%s' ${JSON.stringify(name)} >> "$HC/calls.log"; printf ' %s' "$@" >> "$HC/calls.log"; echo >> "$HC/calls.log"`;
// Fakes on PATH. Each records its argv and answers from a file in the state directory.
const FAKES = {
systemctl: `case "$1" in
show) [ -f "$HC/show-$2" ] && cat "$HC/show-$2" ;;
is-enabled) cat "$HC/enabled-$2" 2>/dev/null || { echo disabled; exit 1; } ;;
esac`,
curl: `exit "$(cat "$HC/curl-rc")"`,
ss: `cat "$HC/ss"`,
od: `if [ "$1" = -An ] && [ "$2" = -tx1 ] && [ "$3" = -N4 ]; then
[ -f "$HC/elf" ] && cat "$HC/elf" || echo 7f454c46
else exec /usr/bin/od "$@"; fi`,
// The credential and the settings file answer from the state directory, as root would see them, and
// only those calls are recorded; everything else is the real stat, which reads the args file.
stat: `case "\${@: -1}" in
*claude-session.env) ${log("stat")}; cat "$HC/stat-cred" ;;
*console/tester.sock) cat "$HC/stat-socket" ;;
*console) [ "$2" = '%u %a %F %d %i' ] && cat "$HC/stat-parent" || echo "997 700" ;;
*managed-settings.json) ${log("stat")}; cat "$HC/stat-settings" ;;
*) exec /usr/bin/stat "$@" ;;
esac`,
// -l -U USER prints the state file; -u jarvis is the bwrap probe; -u mosaic -H env PATH=... CMD runs
// CMD from the tree, with the PATH entries and each absolute argument that exists there moved under it.
sudo: `${CASE_SUDO_SESSION}
case "$1" in
-l) [ -f "$HC/sudo-l-$3" ] && cat "$HC/sudo-l-$3"; exit 0 ;;
-u) ;;
*) exit 97 ;;
esac
user="$2"; shift 2
if [ "$user" = jarvis ]; then cat "$HC/jarvis-bwrap" >&2; exit "$(cat "$HC/jarvis-bwrap-rc")"; fi
[ "$user" = mosaic ] && [ "$1" = -H ] && [ "$2" = env ] || exit 98
shift 2
if [ "$1" = -i ]; then
[ "$2" = HOME=/srv/mosaic ] || exit 98
shift 2
fi
[[ "$1" == PATH=* ]] || exit 98
p="\${1#PATH=}"; shift
np=""; IFS=: read -ra parts <<< "$p"; for x in "\${parts[@]}"; do np+="$ROOT_DIR$x:"; done
cmd="$1"; shift
args=(); for a in "$@"; do if [[ "$a" == /* ]] && [ -e "$ROOT_DIR$a" ]; then args+=("$ROOT_DIR$a"); else args+=("$a"); fi; done
export HOME="$ROOT_DIR/srv/mosaic" PATH="$np$PATH"
[ ! -e "$ROOT_DIR$cmd" ] || cmd="$ROOT_DIR$cmd"
case_sudo_run "$HC" "$cmd" "\${args[@]}"`,
};
// Executables inside the tree, run by the fake sudo or by the script itself. Node marks a run outside
// the fake sudo, which sets HOME to the stack's home, so a test can tell whether root ran it.
const NODE_STUB = `[ "$HOME" = "$ROOT_DIR/srv/mosaic" ] || touch "$HC/node-as-root"
[ "\${1:-}" = --version ] && { cat "$HC/node-version"; exit 0; }
exec "$REAL_NODE" "$@"`;
// The real Python payload runs against a mock socket object, never an OS listener.
const PYTHON_STUB = `[ -f "$HC/peer-output" ] && { cat "$HC/peer-output"; exit "$(cat "$HC/peer-status")"; }
[ -f "$HC/peer-hang" ] && {
here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here")
echo "$here" > "$HC/peer-pid"; printf '%s %s\\n' "$here" "$sid" >> "$HC/query-pids"
trap '' TERM HUP; exec sleep 60
}
if [ -f "$HC/peer-change" ]; then cat "$HC/peer-change" > "$HC/stat-socket"; fi
if [ -f "$HC/peer-pid-change" ]; then cat "$HC/peer-pid-change" > "$HC/show-mosaic-webui.service"; fi
exec /usr/bin/python3 -I -S -c '
import os,socket,struct,sys
class Fake:
def __init__(self,*a): assert a==(socket.AF_UNIX,socket.SOCK_STREAM)
def settimeout(self,n): assert n==1
def connect(self,p): assert p.endswith("/console/tester.sock")
def getsockopt(self,*a):
assert a==(socket.SOL_SOCKET,socket.SO_PEERCRED,12)
return struct.pack("3i",*[int(x) for x in open(os.environ["HC"]+"/peer").read().split()])
def close(self): pass
socket.socket=Fake
payload=sys.argv[4]
sys.argv=["probe",sys.argv[5]]
exec(payload)
' "$@"`;
const TREE = {
"/usr/bin/python3": PYTHON_STUB,
[NODE]: NODE_STUB,
[CLAUDE]: `[ "\${1:-}" = --version ] && cat "$HC/claude-version"; exit 0`,
[CLI]: `${log("mosaic")}
case "$1" in
talk) case "$(cat "$HC/talk-mode")" in
reply) echo "sent ab12cd34 to chief"; echo "[chief -> tester] seen" ;;
noreply) echo "sent ab12cd34 to chief"; echo "no reply from chief within 300 s" >&2; exit 1 ;;
sentonly) echo "sent ab12cd34 to chief" ;;
esac ;;
launches) cat "$HC/launches.json" ;;
*) exit 2 ;;
esac`,
[PRECHECK]: `${log("precheck")}
echo "precheck: done"; exit "$(cat "$HC/precheck-rc")"`,
"/usr/local/lib/mosaic-stack/hostcheck.sh": `${log("hostcheck")}
cat "$HC/hostcheck-out"`,
};
function proc(root, pid, { ppid, uid = 997, exe, argv0 = "x", comm = "x", label }) {
const d = join(root, "proc", String(pid));
put(join(d, "status"), `Name:\t${comm}\nState:\tS (sleeping)\nPPid:\t${ppid}\nUid:\t${uid}\t${uid}\t${uid}\t${uid}\n`);
put(join(d, "cmdline"), `${argv0}\0--x\0`);
put(join(d, "comm"), `${comm}\n`);
rmSync(join(d, "exe"), { force: true });
if (exe) symlinkSync(exe, join(d, "exe"));
if (label !== undefined) put(join(d, "attr", "current"), label);
}
// A scratch Case 1 host where every F check passes and every P check is closed or accepted.
function host(t) {
const dir = tmp(t, "c1accept-");
const root = join(dir, "root"), hc = join(dir, "hc"), bin = join(dir, "bin");
const args = join(root, "etc", "mosaic-stack", "dev.args");
put(args, ARGS.join("\n") + "\n", 0o644);
put(join(root, "etc", "passwd"), PASSWD);
put(join(root, "etc", "group"), GROUP);
put(join(root, "etc", "sudoers.d", "90-localadmin"), "localadmin ALL=(ALL) NOPASSWD: ALL\n");
put(join(root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: "1" } }));
put(join(root, CRED), `CLAUDE_CODE_OAUTH_TOKEN=${CRED_VALUE}\n`, 0o600);
put(join(root, HOME, ".config", "mosaic-dev", "businesses", "mosaic-stack.json"), JSON.stringify({ id: "mosaic-stack", launch: { by: "pm" } }));
put(join(root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
proc(root, 1, { ppid: 0, uid: 0, exe: "/usr/lib/systemd/systemd", comm: "systemd" });
proc(root, 100, { ppid: 1, exe: NODE, argv0: NODE, comm: "node" });
proc(root, 200, { ppid: 1, exe: NODE, argv0: NODE, comm: "node" });
proc(root, 300, { ppid: 100, exe: "/usr/bin/bwrap", argv0: "/usr/bin/bwrap", comm: "bwrap", label: `${OUTER}\n` });
proc(root, 301, { ppid: 300, exe: NODE, argv0: NODE, comm: "node", label: `${CHILD}\n` });
proc(root, 302, { ppid: 301, exe: CLAUDE, argv0: CLAUDE, comm: "claude.exe", label: `${CHILD}\n` });
// A root bwrap with a mislabelled claude child, which F5 must pass over.
proc(root, 400, { ppid: 1, uid: 0, exe: "/usr/bin/bwrap", argv0: "/usr/bin/bwrap", comm: "bwrap", label: "unconfined\n" });
proc(root, 401, { ppid: 400, uid: 0, argv0: "claude", comm: "claude", label: "unconfined\n" });
const state = {
"[email protected]": "ActiveState=active\nUser=mosaic\nMainPID=100\n",
"show-mosaic-webui.service": "ActiveState=active\nUser=mosaic\nMainPID=200\n",
"show-mosaic-hostcheck.service": "[email protected] mosaic-webui.service multi-user.target\n",
"enabled-mosaic-hostcheck.service": "enabled\n",
"curl-rc": "0", ss: SS,
"stat-parent": "997 700 directory 9 10\n", "stat-socket": "997 600 socket 9 11\n", peer: "200 997 997\n",
"stat-cred": "mosaic 600 regular file\n", "stat-settings": "root:root 644 regular file\n",
"sudo-l-mosaic": "User mosaic is not allowed to run sudo on dev-53.\n",
"sudo-l-localadmin": "User localadmin may run the following commands on dev-53:\n (ALL : ALL) ALL\n (ALL) NOPASSWD: ALL\n",
"jarvis-bwrap": "sudo: unable to execute /usr/bin/bwrap: Permission denied\n", "jarvis-bwrap-rc": "1",
"node-version": `${NODE_PIN}\n`, "claude-version": `${CLAUDE_PIN} (Claude Code)\n`,
"talk-mode": "reply", "precheck-rc": "0", "hostcheck-out": HOSTCHECK_OK,
"launches.json": JSON.stringify([
{ instance: "pm", live: true, business: "mosaic-stack", run: "r-0002" },
{ instance: "pm", live: false, business: "mosaic-stack", run: "r-0001" },
{ instance: "dev-1", live: true, business: "mosaic-stack", run: "r-0003" },
]),
};
for (const [name, text] of Object.entries(state)) put(join(hc, name), text);
for (const [name, body] of Object.entries(FAKES)) put(join(bin, name), `#!/usr/bin/env bash\n${name === "stat" ? "" : log(name)}\n${body}\n`, 0o755);
for (const [path, body] of Object.entries(TREE)) put(join(root, path), `#!/usr/bin/env bash\n${body}\n`, 0o755);
const copyRoot = join(root, HOME, '.local/share/mosaic-stack/copies', COPY);
put(join(copyRoot, 'packages/cli/src/config.mjs'), `import fs from 'node:fs';export const loadSystem=()=>({dataRoot:fs.readFileSync(${JSON.stringify(join(hc,'data-root'))},'utf8').trim()});`);
put(join(copyRoot, 'packages/webui/src/cli.mjs'), '// MOSAIC_TESTER_SOCKET_V1\n');
put(join(hc, 'data-root'), `${HOME}/.mosaic-dev\n`);
put(join(root, HOME, '.mosaic-dev/console/tester.sock'), 'stand-in socket metadata only');
chmodSync(join(root, HOME, '.mosaic-dev/console'), 0o700);
// F9 runs node only when it and each directory above it have no group or other write, whatever the umask.
for (let d = dirname(NODE); d !== "/"; d = dirname(d)) chmodSync(join(root, d), 0o755);
chmodSync(root, 0o755);
return { dir, root, hc, bin, args };
}
function run(h, extra = {}, argv = ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN, "--root", h.root]) {
const r = spawnSync(SCRIPT, argv, {
encoding: "utf8",
env: { ...process.env, PATH: `${h.bin}:${process.env.PATH}`, HC: h.hc, ROOT_DIR: h.root, REAL_NODE: process.execPath, TMPDIR: h.dir, ...extra },
});
const lines = r.stdout.split("\n").filter(Boolean);
// Each check line as { id, path, mark, evidence }.
const checks = lines.filter((l) => /^[FP][0-9]+ /.test(l)).map((l) => {
const [id, path, ...rest] = l.split(" ");
const tail = rest.join(" ");
const mark = MARKS.find((m) => tail === m || tail.startsWith(`${m} `));
return { id, path, mark, evidence: tail.slice(mark?.length ?? 0).trim(), line: l };
});
return { status: r.status, out: r.stdout, err: r.stderr, lines, checks };
}
const calls = (h, name) => (existsSync(join(h.hc, "calls.log")) ? readFileSync(join(h.hc, "calls.log"), "utf8") : "")
.split("\n").filter((l) => l === name || l.startsWith(`${name} `)).map((l) => l.slice(name.length + 1));
const failed = (r) => r.checks.filter((c) => c.mark === "FAIL").map((c) => c.id);
const marksOf = (r, id) => r.checks.filter((c) => c.id === id).map((c) => c.mark);
const mustRun = (h, extra) => { const r = run(h, extra); assert.ok(r.status === 0 || r.status === 1, r.out + r.err); return r; };
test('row253 socket observation uses configured root, metadata and the Console peer without sending bytes', t=>{
const h=host(t), data='/srv/mosaic/custom-data';
put(join(h.hc,'data-root'),data+'\n');
put(join(h.root,data,'console/tester.sock'),'metadata stand-in');
const r=run(h);
assert.equal(r.status,0,r.out+r.err);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=observed path=\/srv\/mosaic\/custom-data\/console\/tester.sock/);
assert.match(r.out,/peer_pid=200 peer_uid=997/);
assert.match(r.out,/hostile same-uid substitution remains/);
assert.doesNotMatch(r.out,/accepted mos-1101/);
assert.deepEqual(marksOf(r,'P8'),['closed']);
});
test('row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure',t=>{
const faults={
'parent-mode': h=>put(join(h.hc,'stat-parent'),'997 755 directory 9 10\n'),
'parent-owner': h=>put(join(h.hc,'stat-parent'),'998 700 directory 9 10\n'),
'socket-mode': h=>put(join(h.hc,'stat-socket'),'997 660 socket 9 11\n'),
'socket-owner': h=>put(join(h.hc,'stat-socket'),'998 600 socket 9 11\n'),
'socket-type': h=>put(join(h.hc,'stat-socket'),'997 600 regular file 9 11\n'),
symlink:h=>{const p=join(h.root,HOME,'.mosaic-dev/console/tester.sock');rmSync(p);symlinkSync('/absent',p);},
peer:h=>put(join(h.hc,'peer'),'200 998 997\n'),
'peer-pid':h=>put(join(h.hc,'peer'),'555 997 997\n'),
'python-mode':h=>chmodSync(join(h.root,'usr/bin/python3'),0o777),
'python-missing':h=>rmSync(join(h.root,'usr/bin/python3')),
'python-not-elf':h=>put(join(h.hc,'elf'),'23212f62\n'),
'peer-malformed':h=>{put(join(h.hc,'peer-output'),'200 997 997 extra\n');put(join(h.hc,'peer-status'),'0');},
'peer-error':h=>{put(join(h.hc,'peer-output'),'200 997 997\n');put(join(h.hc,'peer-status'),'1');},
'peer-uid-fields':h=>proc(h.root,200,{ppid:1,uid:998,exe:NODE,comm:'node'}),
'changed-inode':h=>put(join(h.hc,'peer-change'),'997 600 socket 9 99\n'),
'changed-pid':h=>put(join(h.hc,'peer-pid-change'),'ActiveState=active\nUser=mosaic\nMainPID=201\n'),
floor:h=>put(join(h.root,'proc/sys/net/ipv4/ip_unprivileged_port_start'),'700\n'),
};
for(const [name,fault] of Object.entries(faults)){
const h=host(t);fault(h);const r=run(h);
assert.equal(r.status,name==='peer-uid-fields'?1:0,`${name}: ${r.out}${r.err}`);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=failed/,name);
assert.doesNotMatch(r.out,/boundary=observed/,name);
}
});
test('row253 acceptance rejects bad config and unsupported copies before talk',t=>{
const faults={
outside:h=>put(join(h.hc,'data-root'),'/srv/mosaic2/data\n'),
dot:h=>put(join(h.hc,'data-root'),'/srv/mosaic/./data\n'),
absent:h=>rmSync(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs')),
unmarked:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/webui/src/cli.mjs'),'// old copy'),
schema:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),'console.log(JSON.stringify({dataRoot:"/srv/mosaic/.mosaic-dev",socket:true,extra:1}));process.exit(0);'),
failed:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),'console.log(JSON.stringify({dataRoot:"/srv/mosaic/.mosaic-dev",socket:true}));process.exit(1);'),
};
for(const [name,fault] of Object.entries(faults)){
const h=host(t);fault(h);const r=run(h);
assert.equal(r.status,2,`${name}: ${r.out}${r.err}`);assert.equal(r.out,'');
assert.match(r.err,/tester (config query|path refuses)|support/);
assert.deepEqual(calls(h,'mosaic'),[]);
}
});
test('row253 stalled peer observation ends within its bound without leaving a reader',async t=>{
const h=host(t);put(join(h.hc,'peer-hang'),'');
const r=run(h);assert.equal(r.status,0,r.out+r.err);
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=failed reason=probe/);
const pid=Number(readFileSync(join(h.hc,'peer-pid'),'utf8'));
let state='gone';
for(let n=0;n<50;n++){
try{state=readFileSync(`/proc/${pid}/stat`,'utf8').split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`peer helper ${pid} still ${state}`);
});
test('row253 P8 refuses both retired TCP and board listeners',t=>{
for(const port of [7333,7331]){
const h=host(t);put(join(h.hc,'ss'),SS+`LISTEN 0 511 127.0.0.1:${port} 0.0.0.0:* users:(("node",pid=200,fd=22))\n`);
assert.deepEqual(marksOf(run(h),'P8'),['unnamed']);
}
});
test('row253 C4 acceptance query inner reason holds under both sudo models',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];
put(join(h.hc,'session-mode'),mode);
put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),blockingQuerySource(files[1]));
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query inner-timeout\n');
assert.ok(seconds<7,`${mode}: ${seconds}s`);assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
assert.deepEqual(await pollFixtureGone(files),[]);await stop();
}
});
test('row253 C4 acceptance query outer reason discards stdout and names residual cleanup',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-config'),'');
put(join(h.hc,'stall-output'),JSON.stringify({dataRoot:HOME+'/.mosaic-dev',socket:true}));
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query outer-timeout\n');
assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s preflight slack`);
assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'R3 session remains before fixture cleanup');
killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit R3 fixture cleanup');await stop();
}
});
test('row253 C4 acceptance query overflow is output-limit in both models',async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);const capture=cleanupFixture(t,files);
put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:'${HOME}/.mosaic-dev'});`);
const r=run(h);capture();assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query output-limit\n');
assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);assert.deepEqual(await pollFixtureGone(files),[]);
}
});
for(const fault of ['inner','outer','overflow'])test(`row253 C4 P10 ${fault} reason holds under both sudo models`,async t=>{
for(const mode of ['exec','hop']){
const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];put(join(h.hc,'session-mode'),mode);
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files,7000);
if(fault==='inner')put(join(h.hc,'peer-hang'),'');
if(fault==='outer'){put(join(h.hc,'stall-probe'),'');put(join(h.hc,'stall-output'),'200 997 997\n');}
if(fault==='overflow'){put(join(h.hc,'peer-output'),'200 997 997\n'+'X'.repeat(256));put(join(h.hc,'peer-status'),'0');}
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,0,r.out+r.err);
const reason=fault==='inner'?'inner-timeout':fault==='outer'?'outer-timeout':'output-limit';
assert.match(r.out,new RegExp('P10 tester-socket-squat unnamed boundary=failed reason=probe-'+reason+' '));
assert.doesNotMatch(r.out,/boundary=observed/);
if(fault==='inner')assert.ok(seconds<4,`${mode}: ${seconds}s includes functional checks`);
if(fault==='outer'){
assert.ok(seconds>=4&&seconds<6,`${mode}: ${seconds}s; 2s functional-check slack`);
if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'stalled supervisor is R3 residual');
for(const file of files)killRecordedPids(file);
}
assert.deepEqual(await pollFixtureGone(files),[],fault==='outer'?'explicit R3 cleanup':mode);await stop();
}
});
// One fault per F check; each makes that check, and only that one, fail.
const F_FAULTS = {
F1: (h) => proc(h.root, 100, { ppid: 1, uid: 0, exe: NODE, comm: "node" }),
F2: (h) => put(join(h.hc, "ss"), SS.replace("127.0.0.1:732 ", "0.0.0.0:732 ")),
F3: (h) => put(join(h.hc, "talk-mode"), "noreply"),
F4: (h) => put(join(h.hc, "precheck-rc"), "1"),
F5: (h) => put(join(h.root, "proc", "302", "attr", "current"), "unconfined\n"),
F6: (h) => put(join(h.hc, "claude-version"), "2.0.9 (Claude Code)\n"),
F7: (h) => put(join(h.hc, "launches.json"), JSON.stringify([{ instance: "pm", live: false, business: "mosaic-stack", run: "r-1" }])),
F8: (h) => put(join(h.hc, "stat-cred"), "mosaic 640 regular file\n"),
F9: (h) => put(join(h.root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: "0" } })),
F10: (h) => put(join(h.hc, "node-version"), "v24.0.0\n"),
F11: (h) => put(join(h.root, HOME, ".mosaic-dev", "discord", "binding.json"), "{}"),
};
test("row 195 accept: every check passing exits 0, with each P mark and counts that match the lines", (t) => {
const h = host(t);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
assert.deepEqual(r.checks.map((c) => c.id), ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11",
"P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9", "P10", "P11"]);
for (const c of r.checks.filter((x) => x.id.startsWith("F"))) assert.equal(c.mark, "pass", c.line);
for (const [id, mark] of Object.entries(P_PASS)) assert.deepEqual(marksOf(r, id), [mark], id);
const counts = r.lines.filter((l) => l.startsWith("count ")).map((l) => l.slice(6));
assert.deepEqual(counts, MARKS.map((m) => `${m} ${r.checks.filter((c) => c.mark === m).length}`));
assert.match(r.lines.at(-1), /^case1-accept: every F check passes/);
assert.match(r.out, /^F5 bwrap-label pass bwrap 300 reads 'bwrap \(enforce\)' and its claude descendant 302 reads/m);
assert.match(r.out, /^F7 pm-live pass one live launch: pm run r-0002$/m);
assert.ok(!(r.out + r.err).includes(CRED_VALUE));
assert.ok(existsSync(join(h.hc, "node-as-root")), "F9 runs the pinned, root-only node as root");
});
test("row 195 accept: exits 1 with each F check failing alone, and its line names it", (t) => {
for (const [id, fault] of Object.entries(F_FAULTS)) {
const h = host(t);
fault(h);
const r = mustRun(h);
assert.equal(r.status, 1, `${id}: ${r.out}`);
assert.deepEqual(failed(r), [id], `${id}: ${r.out}`);
assert.equal(r.lines.at(-1), "case1-accept: an F check failed, so the row fails");
}
});
test("row 195 accept: F3 fails on exit 0 with no reply line, and its evidence takes talk's error", (t) => {
let h = host(t);
put(join(h.hc, "talk-mode"), "sentonly");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F3"]);
assert.match(r.out, /^F3 tester-reply FAIL talk exited 0 with 1 lines on stdout; last: sent ab12cd34 to chief$/m);
h = host(t);
put(join(h.hc, "talk-mode"), "noreply");
r = mustRun(h);
assert.match(r.out, /^F3 tester-reply FAIL talk exited 1 with 1 lines on stdout; last: no reply from chief within 300 s$/m);
});
test("row 195 accept: F1 fails on a unit that isn't active or isn't User=mosaic", (t) => {
for (const show of ["ActiveState=failed\nUser=mosaic\nMainPID=0\n", "ActiveState=active\nUser=root\nMainPID=100\n"]) {
const h = host(t);
put(join(h.hc, "show-mosaic-webui.service"), show);
const r = mustRun(h);
assert.equal(r.status, 1);
assert.ok(failed(r).includes("F1"), r.out);
}
});
test("row 195 accept: F2 fails when healthz doesn't answer and when another pid holds the port", (t) => {
let h = host(t);
put(join(h.hc, "curl-rc"), "7");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F2"]);
assert.match(r.out, /^F2 console-loopback FAIL curl http:\/\/127\.0\.0\.1:732\/healthz exited 7$/m);
h = host(t);
put(join(h.hc, "ss"), SS.replace("pid=200,fd=21", "pid=555,fd=4"));
r = mustRun(h);
assert.deepEqual(failed(r), ["F2"]);
});
test("row 195 accept: F5 fails on unpriv_bwrap (enforce), on complain and on a trailing space", (t) => {
for (const label of ["unpriv_bwrap (enforce)\n", "bwrap//&unpriv_bwrap (complain)\n", `${CHILD} \n`]) {
const h = host(t);
put(join(h.root, "proc", "302", "attr", "current"), label);
const r = mustRun(h);
assert.deepEqual(failed(r), ["F5"], `${JSON.stringify(label)}: ${r.out}`);
}
const h = host(t);
put(join(h.root, "proc", "300", "attr", "current"), `${OUTER} \n`);
assert.deepEqual(failed(mustRun(h)), ["F5"]);
});
test("row 195 accept: F5 fails with no bwrap of mosaic's, and passes on comm alone", (t) => {
let h = host(t);
proc(h.root, 300, { ppid: 100, uid: 1001, exe: "/usr/bin/bwrap", comm: "bwrap", label: `${OUTER}\n` });
let r = mustRun(h);
assert.deepEqual(failed(r), ["F5"]);
assert.match(r.out, /^F5 bwrap-label FAIL no \/usr\/bin\/bwrap process of mosaic/m);
h = host(t);
proc(h.root, 302, { ppid: 301, argv0: "/proc/self/fd/3", comm: "claude", label: `${CHILD}\n` });
assert.deepEqual(failed(mustRun(h)), []);
});
test("row 195 accept: F6 fails on a stub that prints nothing, on a path outside claude-<pin> and on a .. segment", (t) => {
let h = host(t);
put(join(h.hc, "claude-version"), "");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F6"]);
h = host(t);
const other = CLAUDE.replace(`claude-${CLAUDE_PIN}`, "claude-2.0.9");
put(h.args, ARGS.map((a) => (a === CLAUDE ? other : a)).join("\n") + "\n", 0o644);
put(join(h.root, other), `#!/usr/bin/env bash\necho "${CLAUDE_PIN} (Claude Code)"\n`, 0o755);
r = mustRun(h);
assert.deepEqual(failed(r), ["F6"]);
assert.match(r.out, /^F6 claude-pin FAIL --claude \S+claude-2\.0\.9\S+ is not under \/opt\/mosaic-stack\/claude-2\.1\.0\/$/m);
// Under claude-<pin>/ by prefix, but the binary it names is 2.0.9's, which answers with the pin.
h = host(t);
const dotdot = CLAUDE.replace(`claude-${CLAUDE_PIN}/`, `claude-${CLAUDE_PIN}/../claude-2.0.9/`);
put(h.args, ARGS.map((a) => (a === CLAUDE ? dotdot : a)).join("\n") + "\n", 0o644);
put(join(h.root, other), `#!/usr/bin/env bash\necho "${CLAUDE_PIN} (Claude Code)"\n`, 0o755);
r = mustRun(h);
assert.deepEqual(failed(r), ["F6"], r.out);
assert.match(r.out, /^F6 claude-pin FAIL --claude \S+\/claude-2\.1\.0\/\.\.\/claude-2\.0\.9\/\S+ has a \.\. segment$/m);
});
test("row 195 accept: F7 fails on two live PM launches, one for another business, and no launch.by", (t) => {
const two = [{ instance: "pm", live: true, business: "mosaic-stack", run: "a" }, { instance: "pm", live: true, business: "mosaic-stack", run: "b" }];
for (const [file, text] of [["launches.json", JSON.stringify(two)], ["launches.json", JSON.stringify([{ instance: "pm", live: true, business: "other", run: "a" }])]]) {
const h = host(t);
put(join(h.hc, file), text);
assert.deepEqual(failed(mustRun(h)), ["F7"]);
}
const h = host(t);
put(join(h.root, HOME, ".config", "mosaic-dev", "businesses", "mosaic-stack.json"), JSON.stringify({ id: "mosaic-stack" }));
const r = mustRun(h);
assert.deepEqual(failed(r), ["F7"]);
assert.match(r.out, /^F7 pm-live FAIL the business file has no launch\.by$/m);
});
test("row 195 accept: F8 fails on 0640, on another owner and on a symlink, and never prints the credential", (t) => {
for (const fault of [
(h) => put(join(h.hc, "stat-cred"), "mosaic 640 regular file\n"),
(h) => put(join(h.hc, "stat-cred"), "root 600 regular file\n"),
(h) => { const f = join(h.root, CRED); rmSync(f); put(`${f}.real`, `X=${CRED_VALUE}\n`, 0o600); symlinkSync(`${f}.real`, f); },
]) {
const h = host(t);
fault(h);
const r = mustRun(h);
assert.deepEqual(failed(r), ["F8"], r.out);
assert.ok(!(r.out + r.err).includes(CRED_VALUE));
}
});
test("row 195 accept: F9 fails on another owner, a group-writable mode and a number for the flag", (t) => {
for (const fault of [
(h) => put(join(h.hc, "stat-settings"), "root:root 664 regular file\n"),
(h) => put(join(h.hc, "stat-settings"), "mosaic:root 644 regular file\n"),
(h) => put(join(h.root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: 1 } })),
]) {
const h = host(t);
fault(h);
assert.deepEqual(failed(mustRun(h)), ["F9"]);
}
});
test("row 195 accept: F9 won't run --node as root off the pin path or with a writable directory above it", (t) => {
// Another node that works for the stack's user: F9 refuses to run it, and F10 fails on the path.
let h = host(t);
const other = NODE.replace(NODE_PIN, "v24.0.9");
put(h.args, ARGS.map((a) => (a === NODE ? other : a)).join("\n") + "\n", 0o644);
put(join(h.root, other), `#!/usr/bin/env bash\n${NODE_STUB}\n`, 0o755);
let r = mustRun(h);
assert.deepEqual(failed(r), ["F9", "F10"], r.out);
assert.match(r.out, /^F9 autoupdater-off FAIL root won't run --node \/opt\/mosaic-stack\/node-v24\.0\.9\/bin\/node: it is not the --node-pin path$/m);
assert.ok(!existsSync(join(h.hc, "node-as-root")));
// The pinned path, under a group-writable directory and then a symlinked node: F9 alone fails, and
// root doesn't run it.
for (const [fault, bad] of [
[(x) => chmodSync(join(x.root, dirname(NODE)), 0o775), "/opt/mosaic-stack/node-v24\\.1\\.0/bin \\(uid \\d+, mode 775\\)"],
[(x) => { const f = join(x.root, NODE); renameSync(f, `${f}.real`); symlinkSync(`${f}.real`, f); }, "/opt/mosaic-stack/node-v24\\.1\\.0/bin/node \\(uid \\d+, mode 777\\)"],
]) {
h = host(t);
fault(h);
r = mustRun(h);
assert.deepEqual(failed(r), ["F9"], r.out);
assert.match(r.out, new RegExp(`^F9 autoupdater-off FAIL root won't run --node \\S+: ${bad} is not root's alone$`, "m"));
assert.ok(!existsSync(join(h.hc, "node-as-root")));
}
});
test("row 195 accept: F11 fails on one *.json under discord/ and passes on an empty directory", (t) => {
let h = host(t);
put(join(h.root, HOME, ".mosaic-dev", "discord", "chief.json"), "{}");
let r = mustRun(h);
assert.deepEqual(failed(r), ["F11"]);
assert.match(r.out, /^F11 no-discord FAIL \/srv\/mosaic\/\.mosaic-dev\/discord holds bindings: chief\.json$/m);
h = host(t);
mkdirSync(join(h.root, HOME, ".mosaic-dev", "discord"), { recursive: true });
assert.deepEqual(failed(mustRun(h)), []);
});
// Each P check in its unnamed mark, with the line count each fault makes; none changes the exit.
const P_FAULTS = {
P1: [1, (h) => put(join(h.hc, "sudo-l-mosaic"), "User mosaic may run the following commands on dev-53:\n")],
P2: [1, (h) => put(join(h.root, HOME, ".ssh", "authorized_keys"), "ssh-ed25519 AAAA test\n")],
P5: [1, (h) => edit(join(h.root, "etc", "group"), (s) => s.replace("docker:x:999:", "docker:x:999:jarvis"))],
P6: [2, (h) => edit(join(h.root, "etc", "group"), (s) => s.replace("sudo:x:27:localadmin", "sudo:x:27:localadmin,bob").replace("lxd:x:998:", "lxd:x:998:carol"))],
P7: [1, (h) => put(join(h.root, "etc", "sudoers.d", "50-mosaic"), "mosaic ALL=(root) NOPASSWD: /usr/bin/systemctl\n")],
P8: [1, (h) => put(join(h.hc, "ss"), `${SS}LISTEN 0 511 0.0.0.0:7331 0.0.0.0:* users:(("node",pid=200,fd=23))\n`)],
P9: [3, (h) => { put(join(h.hc, "hostcheck-out"), HOSTCHECK_OK.replace("ok 2 ", "FAIL 2 ").replace("ok 5 ", "FAIL 5 ")); put(join(h.hc, "jarvis-bwrap-rc"), "0"); }],
P10: [1, (h) => put(join(h.root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "700\n")],
P11: [1, (h) => put(join(h.hc, "show-mosaic-hostcheck.service"), "[email protected] multi-user.target\n")],
};
test("row 195 accept: each P check reports unnamed on its fault, and the exit stays 0", (t) => {
for (const [id, [n, fault]] of Object.entries(P_FAULTS)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 0, `${id}: ${r.out}`);
assert.deepEqual(marksOf(r, id), Array(n).fill("unnamed"), `${id}: ${r.out}`);
}
});
test("row 195 accept: P2 is unnamed with a supplementary group, and P5 names docker", (t) => {
let h = host(t);
put(join(h.root, "etc", "group"), GROUP.replace("lxd:x:998:", "lxd:x:998:mosaic"));
let r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P2"), ["unnamed"]);
assert.match(r.out, /^P2 mosaic-groups-keys unnamed id -G is '997 998'; authorized_keys is absent$/m);
h = host(t);
P_FAULTS.P5[1](h);
r = run(h);
assert.match(r.out, /^P5 jarvis-reach unnamed jarvis is in docker$/m);
assert.match(r.out, /^P6 group-docker unnamed jarvis is a member of docker$/m);
});
test("row 195 accept: P5 is closed with no jarvis account, and P2 with an empty authorized_keys", (t) => {
const h = host(t);
put(join(h.root, "etc", "passwd"), PASSWD.replace(/^jarvis:.*\n/m, ""));
put(join(h.root, HOME, ".ssh", "authorized_keys"), "");
const r = run(h);
assert.deepEqual(marksOf(r, "P5"), ["closed"]);
assert.deepEqual(marksOf(r, "P2"), ["closed"]);
});
test("row 195 accept: P9 is unnamed once for each item missing", (t) => {
const items = {
hostcheck: (h) => put(join(h.hc, "hostcheck-out"), HOSTCHECK_OK.replace("ok 3 ", "FAIL 3 ")),
jarvis: (h) => { put(join(h.hc, "jarvis-bwrap-rc"), "0"); put(join(h.hc, "jarvis-bwrap"), ""); },
member: (h) => put(join(h.root, "etc", "group"), GROUP.replace("mosaic:x:997:", "mosaic:x:997:jarvis")),
gid: (h) => put(join(h.root, "etc", "passwd"), `${PASSWD}hermes:x:1002:997::/home/hermes:/bin/bash\n`),
};
for (const [name, fault] of Object.entries(items)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P9"), ["unnamed"], `${name}: ${r.out}`);
}
const h = host(t);
put(join(h.hc, "hostcheck-out"), "");
assert.deepEqual(marksOf(run(h), "P9"), Array(6).fill("unnamed"));
});
test("row253 accept: P10 fails its observation with another peer; P11 with the unit disabled", (t) => {
let h = host(t);
put(join(h.hc, "peer"), "555 997 997\n");
let r = run(h);
assert.equal(r.status, 0);
assert.deepEqual(marksOf(r, "P10"), ["unnamed"]);
assert.match(r.out, /P10 tester-socket-squat unnamed boundary=failed reason=peer/);
h = host(t);
put(join(h.hc, "enabled-mosaic-hostcheck.service"), "disabled\n");
r = run(h);
assert.deepEqual(marksOf(r, "P11"), ["unnamed"]);
});
test("row 195 accept: every P check unnamed together still exits 0", (t) => {
const h = host(t);
for (const [, fault] of Object.values(P_FAULTS)) fault(h);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
assert.deepEqual(failed(r), []);
for (const id of ["P1", "P2", "P5", "P6", "P7", "P8", "P9", "P10", "P11"]) assert.ok(marksOf(r, id).every((m) => m === "unnamed"), id);
assert.match(r.lines.at(-1), /^case1-accept: every F check passes/);
});
test("row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason", (t) => {
const bad = {
"group write": [(h) => chmodSync(h.args, 0o664), /dev\.args must be \S+'s with no group or other write \(it is uid \d+, mode 664\)$/],
symlink: [(h) => { put(`${h.args}.real`, ARGS.join("\n") + "\n", 0o644); rmSync(h.args); symlinkSync(`${h.args}.real`, h.args); }, /no regular file \/etc\/mosaic-stack\/dev\.args$/],
"unknown flag": [(h) => put(h.args, ["--send", ...ARGS].join("\n") + "\n", 0o644), /has --send before --, which case2-unit\.sh does not take$/],
"two copies": [(h) => put(h.args, ["--copy", COPY, ...ARGS].join("\n") + "\n", 0o644), /needs exactly one --copy before --, not 2$/],
"library profile": [(h) => put(h.args, ARGS.map((a) => (a === "factory" ? "library" : a)).join("\n") + "\n", 0o644), /has --profile 'library', not factory$/],
"another business": [(h) => put(h.args, ARGS.map((a, i) => (a === "mosaic-stack" && i < 14 ? "acme" : a)).join("\n") + "\n", 0o644), /is for business acme, not mosaic-stack$/],
"no tester socket": [(h) => put(h.args, ARGS.slice(0, -1).join("\n") + "\n", 0o644), /needs one bare --tester-socket after --$/],
"retired TCP": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\n')+'\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],
"socket value": [(h) => put(h.args, [...ARGS,'7333'].join('\n')+'\n',0o644), /unknown serve flag 7333$/],
"duplicate socket": [(h) => put(h.args, [...ARGS,'--tester-socket'].join('\n')+'\n',0o644), /needs one bare --tester-socket after --$/],
"short copy": [(h) => put(h.args, ARGS.map((a) => (a === COPY ? "abc123" : a)).join("\n") + "\n", 0o644), /lacks a plain --user, --home, --business, --node, --claude or --copy$/],
"no file": [(h) => rmSync(h.args), /no regular file \/etc\/mosaic-stack\/dev\.args$/],
};
for (const [name, [fault, why]] of Object.entries(bad)) {
const h = host(t);
fault(h);
const r = run(h);
assert.equal(r.status, 2, `${name}: ${r.out}${r.err}`);
assert.match(r.err.trimEnd(), /^case1-accept: refusing: /, name);
assert.match(r.err.trimEnd(), why, name);
assert.equal(r.out, "", name);
assert.ok(!existsSync(join(h.hc, "calls.log")), name);
}
});
test("row 195 accept: usage errors and a run without --root as a user exit 2", (t) => {
const h = host(t);
let r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--node-pin", NODE_PIN, "--root", h.root]);
assert.equal(r.status, 2);
assert.match(r.err, /^usage: /);
r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", "24.1.0", "--root", h.root]);
assert.equal(r.status, 2);
r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN]);
assert.equal(r.status, 2);
assert.equal(r.err, "case1-accept: refusing: run it as root\n");
assert.ok(!existsSync(join(h.hc, "calls.log")));
});
test("row 195 accept: every in-instance call goes through sudo -u mosaic -H env PATH=<node dir>:...", (t) => {
const h = host(t);
const r = run(h);
assert.equal(r.status, 0, r.out + r.err);
const sudo = calls(h, "sudo");
const asMosaic = sudo.filter((a) => a.startsWith("-u mosaic"));
const isolated = asMosaic.filter(a=>a.startsWith('-u mosaic -H env -i '));
assert.equal(isolated.length, 2);
for(const a of isolated) assert.ok(a.startsWith(`-u mosaic -H env -i HOME=${HOME} PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin `),a);
for (const a of asMosaic.filter(a=>!isolated.includes(a))) assert.ok(a.startsWith(`${AS_MOSAIC} /`), a);
const cmds = asMosaic.filter(a=>!isolated.includes(a)).map((a) => a.slice(AS_MOSAIC.length + 1).split(" ")[0]);
assert.deepEqual([...new Set(cmds)].sort(), [CLI, PRECHECK, CLAUDE, NODE].sort());
assert.deepEqual(sudo.filter((a) => !a.startsWith("-u mosaic")).sort(),
["-l -U localadmin", "-l -U mosaic", "-u jarvis /usr/bin/bwrap --version"]);
const talk = calls(h, "mosaic").filter((a) => a.startsWith("talk "));
assert.equal(talk.length, 1);
assert.match(talk[0], /^talk chief case1-accept-[0-9a-f]{12}: .* --business mosaic-stack --tester --wait 300$/);
assert.deepEqual(calls(h, "precheck"), ["--business mosaic-stack --profile factory"]);
});
test("row 195 accept: the host check runs with --check and never --enforce", (t) => {
const h = host(t);
run(h);
assert.deepEqual(calls(h, "hostcheck"), ["--check"]);
});
@@ -0,0 +1,609 @@
#!/usr/bin/env bash
# Case 2 row C2-13a (design 9.3, 10 step 2c): render and install the two
# system units a hub runs as, [email protected] (enabled as
# mosaic-bus@<business>.service) and mosaic-webui.service, from the existing
# templates, which it doesn't change.
#
# case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH
# [--profile factory|library] [--pm] [--claude PATH]
# -- SERVE FLAGS
#
# Root runs it from a root-owned checkout of the reviewed commit outside
# <home>, never from the release, because <user> can write the release. It
# writes both units under /etc/systemd/system as root:root 0644, runs
# `systemctl daemon-reload`, enables both, then starts the bus and then the
# Console (Q189). Both come back after a boot on their own. It never links
# a unit file into place. Root only stats files under <home> and reads bounded
# stdout from a config/target query run as the stack user through sudo. Root
# never imports copy code. This adds sudo and GNU timeout to the installer
# dependencies. The inner query supervisor runs after the uid drop: 5 s inside,
# 7 s on root's wait. Fixed system Bash, setsid, sleep, timeout and head are
# resolved and root-validated. Only the config query uses this supervisor.
# It renders the default instance only.
#
# The library profile takes exactly these serve flags, each once:
# --port N (1..1023) --business ID (the unit's) --public-host NAME
# --auth-header HEADER --allow-user ID --send
# --proof-header NAME --proof-file PATH
# The factory profile takes --port N (1..1023) and --business ID, and an
# optional bare --tester-socket marker. Retired tester TCP refuses. Only factory takes
# --pm and --claude.
#
# On a re-install the installed units must equal the render in everything
# but the copy id, and both must name one copy, so a re-pin or rollback
# re-renders the pair together. A unit that was already running stays on its
# old copy, since `systemctl start` leaves it alone; the script says so and
# that the bus and Console must restart together, and restarts nothing.
#
# Test seams, for packages/cli/tests/case2-unit.test.mjs only:
# CASE2_UNIT_DIR the unit directory, default /etc/systemd/system;
# the root and root-owned checks apply only there
# CASE2_SYSTEMCTL the systemctl command, default systemctl
# CASE2_GETENT the getent command, default getent
# CASE2_BUS_TEMPLATE default packages/cli/systemd/mosaic-bus.service.in
# CASE2_WEBUI_TEMPLATE default packages/webui/systemd/mosaic-webui.service.in
#
# Exit 0 done, 1 refused, 4 usage.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEFAULT_UNIT_DIR=/etc/systemd/system
UNIT_DIR="${CASE2_UNIT_DIR:-$DEFAULT_UNIT_DIR}"
SYSTEMCTL="${CASE2_SYSTEMCTL:-systemctl}"
GETENT="${CASE2_GETENT:-getent}"
BUS_TEMPLATE="${CASE2_BUS_TEMPLATE:-$REPO/packages/cli/systemd/mosaic-bus.service.in}"
WEBUI_TEMPLATE="${CASE2_WEBUI_TEMPLATE:-$REPO/packages/webui/systemd/mosaic-webui.service.in}"
[email protected]
WEBUI_UNIT=mosaic-webui.service
usage() { echo 'usage: case2-unit.sh --user NAME --home DIR --business ID --copy ID --node PATH [--profile factory|library] [--pm] [--claude PATH] -- SERVE FLAGS' >&2; exit 4; }
die() { echo "case2-unit: $*" >&2; exit 1; }
# A value the unit carries as itself: no space, %, $, quote, backslash, & or |.
plain() { case "$1" in *[!A-Za-z0-9/._+-]*) return 1 ;; esac; }
abs_plain() {
case "$1" in /*) ;; *) die "$2 must be an absolute path: $1" ;; esac
plain "$1" || die "$2 contains a character the unit cannot carry: $1"
case "$1/" in */./*|*/../*|?*//*) die "$2 must be a normalized path: $1" ;; esac
[ "$1" = / ] || [ "${1%/}" = "$1" ] || die "$2 must not end in /: $1"
}
PROFILE=library USER_NAME='' HOME_DIR='' BUSINESS='' COPY_ID='' NODE='' PM=0 CLAUDE='' CLAUDE_SET=0
while [ $# -gt 0 ]; do
case "$1" in
--profile|--user|--home|--business|--copy|--node|--claude)
[ $# -ge 2 ] || usage
case "$1" in
--profile) PROFILE="$2" ;; --user) USER_NAME="$2" ;; --home) HOME_DIR="$2" ;;
--business) BUSINESS="$2" ;; --copy) COPY_ID="$2" ;; --node) NODE="$2" ;;
--claude) CLAUDE="$2"; CLAUDE_SET=1 ;;
esac
shift 2 ;;
--pm) PM=1; shift ;;
--) shift; break ;;
*) usage ;;
esac
done
SERVE=("$@")
[ -n "$USER_NAME" ] && [ -n "$HOME_DIR" ] && [ -n "$BUSINESS" ] && [ -n "$COPY_ID" ] && [ -n "$NODE" ] || usage
case "$PROFILE" in factory|library) ;; *) die "--profile must be factory or library: $PROFILE" ;; esac
if [ "$PROFILE" = library ]; then
[ "$PM" = 0 ] || die "the library profile refuses --pm"
[ "$CLAUDE_SET" = 0 ] || die "the library profile refuses --claude"
fi
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
[ "$(id -u)" = 0 ] || die "writing $DEFAULT_UNIT_DIR needs root"
fi
# The user, its uid, primary group and home, from the passwd database.
[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "--user must be a user name: $USER_NAME"
PW="$("$GETENT" passwd "$USER_NAME")" || die "no user $USER_NAME in the passwd database"
IFS=: read -r _ _ USER_UID USER_GID _ PW_HOME _ <<<"$PW"
[[ "$USER_UID" =~ ^[0-9]+$ ]] && [ "$USER_UID" -ne 0 ] || die "--user must not be root: $USER_NAME"
abs_plain "$HOME_DIR" '--home'
[ "$HOME_DIR" = "$PW_HOME" ] || die "--home $HOME_DIR is not $USER_NAME's home in the passwd database ($PW_HOME)"
GR="$("$GETENT" group "$USER_GID")" || die "no group $USER_GID in the group database"
GROUP="${GR%%:*}"
[[ "$GROUP" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "the primary group of $USER_NAME is not a group name: $GROUP"
case "$BUSINESS" in [a-z0-9]*) ;; *) die "--business must be a business id: $BUSINESS" ;; esac
case "$BUSINESS" in *[!a-z0-9._-]*) die "--business must be a business id: $BUSINESS" ;; esac
[ "${#BUSINESS}" -le 64 ] || die "--business must be a business id: $BUSINESS"
# The copy: copy_root's rule (scripts/stack-copy.sh:8, :22), under <home>.
COPIES="$HOME_DIR/.local/share/mosaic-stack/copies"
[[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] || die "--copy must be a full 40-character lower-case commit id: $COPY_ID"
COPY="$COPIES/$COPY_ID"
[ -d "$COPY" ] && [ ! -L "$COPY" ] || die "no copy directory $COPY"
[ -f "$COPY/scripts/mosaic" ] || die "no copy $COPY_ID in $COPIES: $COPY/scripts/mosaic is missing"
# Owned by root and writable by no one else, the file and every directory above it.
root_only() {
local p="$1" what="$2" owner mode
while :; do
owner="$(stat -c '%u' -- "$p")" || die "cannot stat $p"
mode="$(stat -c '%a' -- "$p")" || die "cannot stat $p"
[ "$owner" = 0 ] || die "$what: $p is not owned by root"
(( (8#$mode & 8#022) == 0 )) || die "$what: $p is writable by its group or others"
[ "$p" = / ] && break
p="$(dirname -- "$p")"
done
}
# --node: an absolute path to a root-owned regular file outside <home>.
abs_plain "$NODE" '--node'
case "$NODE/" in "$HOME_DIR"/*) die "--node must be outside $HOME_DIR: $NODE" ;; esac
[ ! -L "$NODE" ] || die "--node must not be a symlink; pass its target: $NODE"
[ -f "$NODE" ] && [ -x "$NODE" ] || die "--node must be an executable regular file: $NODE"
root_only "$NODE" '--node'
# --claude follows the --node rule, so nothing running as <user> can replace
# the binary the bus runs.
if [ "$CLAUDE_SET" = 1 ]; then
abs_plain "$CLAUDE" '--claude'
case "$CLAUDE/" in "$HOME_DIR"/*) die "--claude must be outside $HOME_DIR: $CLAUDE" ;; esac
[ ! -L "$CLAUDE" ] || die "--claude must not be a symlink; pass its target: $CLAUDE"
[ -f "$CLAUDE" ] && [ -x "$CLAUDE" ] || die "--claude must be an executable regular file: $CLAUDE"
root_only "$CLAUDE" '--claude'
fi
# The checkout the templates come from, when installing for real.
if [ "$UNIT_DIR" = "$DEFAULT_UNIT_DIR" ]; then
case "$REPO/" in "$HOME_DIR"/*) die "run this from a root-owned checkout outside $HOME_DIR, not $REPO" ;; esac
root_only "$BUS_TEMPLATE" 'bus template'
root_only "$WEBUI_TEMPLATE" 'Console template'
fi
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
# row253-supervisor-begin
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
stat_fields() {
local pid="$1" raw="" rest
# EOF is expected: stat has no NUL. Decide on content, not read's status.
IFS= read -r -d '' raw < "/proc/$pid/stat" 2>/dev/null || :
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
[[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
}
clock_cs() {
local boot spare
read -r boot spare < /proc/uptime || return 1
[[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
}
clean_session() {
local entry pid rc live until=""
while :; do
live=0
for entry in /proc/[0-9]*/stat; do
pid="${entry%/stat}"; pid="${pid##*/}"
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" != 3 ] || continue
[ "$rc" = 0 ] || return 1
[ "$ST_SESSION" = "$child" ] || continue
case "$ST_STATE" in Z|X) continue ;; esac
live=1
if ! kill -KILL "$pid" 2>/dev/null; then
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
fi
done
[ "$live" != 0 ] || return 0
clock_cs || return 1
# Inventory pass 1 is outside the bounded confirmation/retry budget.
[ -n "$until" ] || until=$((CLOCK_CS + 25))
[ "$CLOCK_CS" -lt "$until" ] || return 1
"$sleep_bin" 0.01
done
}
finish() {
local rc="$?"
trap - EXIT HUP INT TERM
if [ "$verified" = 1 ]; then
clean_session || rc=86
elif [ -n "$child" ]; then
kill -KILL "$child" 2>/dev/null || :
fi
[ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
"$setsid_bin" "$@" & child=$!
# Allow session creation the whole inner budget, rather than a 200 ms margin.
while :; do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 85
if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
tester_tools() {
local input target p st magic
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
p="$target"
while :; do
st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
[ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
[ "$p" != / ] || break
p="${p%/*}"; [ -n "$p" ] || p=/
done
magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
case "$input" in
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
/usr/bin/head) TS_HEAD="$target" ;;
esac
done
}
tester_capture_reason() {
case "$1" in
81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
*) echo inner-command ;;
esac
}
tester_capture() (
local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
tester_tools || exit 84
file="$(mktemp)" || exit 88
notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
trap 'exit 88' HUP INT TERM
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
set -o pipefail
maximum="$1"; head_bin="$2"; shift 2
"$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
# Only GNU timeout and the trusted head can write this root-side channel.
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
[ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
[ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# The serve flags, checked as scripts/webui-service.sh serve_flags checks
# them (:201-264), and kept in the order given. That script compares the
# proof file's owner to `id -u`, which is root here, so this copy has its own.
serve_flags() {
local -A seen=()
local flags='' name value
local port='' public_host='' auth_header='' allow_user='' proof_header='' proof_file='' tester_socket=0
local i=0 n=${#SERVE[@]}
while [ "$i" -lt "$n" ]; do
name="${SERVE[$i]}"
case "$name" in
--tester-port) die "case2-unit.sh retires tester TCP; use --tester-socket" ;;
--tester-socket) [ "$PROFILE" = factory ] || die "the library profile refuses --tester-socket" ;;
--checkout) die "refusing --checkout: Case 2 has no queue panel" ;;
--board) die "refusing --board: Case 2 runs no control board" ;;
--sso) die "refusing --sso: the Console signs people in through the proxy, with the preview set and the proof pair (Q175 phase 2 is a follow-up)" ;;
--port|--business) ;;
--public-host|--auth-header|--allow-user|--proof-header|--proof-file|--send)
[ "$PROFILE" = library ] || die "the factory profile refuses $name: its host has no proxy" ;;
*) die "unknown serve flag: $name" ;;
esac
[ -z "${seen[$name]:-}" ] || die "serve flag $name given twice"
seen[$name]=1
if [ "$name" = --tester-socket ]; then tester_socket=1; flags="$flags --tester-socket"; i=$((i + 1)); continue; fi
if [ "$name" = --send ]; then flags="$flags --send"; i=$((i + 1)); continue; fi
[ $((i + 1)) -lt "$n" ] || die "serve flag $name needs a value"
value="${SERVE[$((i + 1))]}"
case "$name" in
--port) port="$value"
[[ "$port" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$port" -le 1023 ] || die "--port must be 1..1023, so only the unit's capability can bind it: $port" ;;
--business) [ "$value" = "$BUSINESS" ] || die "serve --business $value is not the unit's business $BUSINESS" ;;
--public-host) public_host="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "public host must be a hostname, no port or scheme: $value" ;; esac
case "$value" in *[!A-Za-z0-9.-]*) die "public host must be a hostname, no port or scheme: $value" ;; esac ;;
--auth-header) auth_header="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "auth header must be a header name: $value" ;; esac
case "$value" in *[!A-Za-z0-9-]*) die "auth header must be a header name: $value" ;; esac ;;
--allow-user) allow_user="$value"
case "$value" in [A-Za-z0-9]*) ;; *) die "allowed user must start with a letter or digit: $value" ;; esac
case "$value" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $value" ;; esac ;;
--proof-header) proof_header="$value"
[[ "$value" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,63}$ ]] || die "invalid proxy proof header" ;;
--proof-file) proof_file="$value" ;;
esac
flags="$flags $name $value"
i=$((i + 2))
done
local required=(--port --business)
[ "$PROFILE" = factory ] || required+=(--public-host --auth-header --allow-user --send --proof-header --proof-file)
for name in "${required[@]}"; do
[ -n "${seen[$name]:-}" ] || die "serve flag $name is required"
done
if [ "$PROFILE" = factory ]; then
if [ "$tester_socket" = 1 ]; then
local query
query="$(tester_capture 5 7 4096 "$USER_NAME" "$HOME_DIR" "$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query $(tester_capture_reason "$?")"
tester_facts "$query" || die "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || die "the target does not support --tester-socket"
tester_location "$TS_DATA" "$HOME_DIR" "$USER_UID" "" || die "tester path refuses $TS_REASON"
fi
echo "${flags# }"; return 0
fi
# Lead decision 101: the uid header carries the Authentik uid.
if [ "${auth_header,,}" = x-authentik-uid ] && ! [[ "$allow_user" =~ ^[0-9a-f]{64}$ ]]; then
die "with --auth-header $auth_header the allowed user must be an Authentik uid, 64 lower-case hex characters (lead decision 101): $allow_user"
fi
case "${proof_header,,}" in host|origin|x-mosaic-tester|"${auth_header,,}") die "proxy proof header must differ from Host, Origin, tester and auth headers" ;; esac
# The proof file: metadata only, never opened (design 9.3).
local secrets="$HOME_DIR/.config/mosaic-dev/secrets" real owner mode
abs_plain "$proof_file" 'proxy proof file'
[ ! -L "$proof_file" ] || die "proxy proof file must not be a symlink: $proof_file"
[ -f "$proof_file" ] || die "proxy proof file must be a regular file: $proof_file"
real="$(realpath -e -- "$proof_file")" || die "cannot resolve proxy proof file: $proof_file"
[ "$real" = "$proof_file" ] || die "proxy proof file path must have no symlink in it: $proof_file"
case "$proof_file" in "$secrets"/*) ;; *) die "proxy proof file must be under $secrets, outside the data root: $proof_file" ;; esac
owner="$(stat -c '%u' -- "$proof_file")" || die "cannot stat proxy proof file"
mode="$(stat -c '%a' -- "$proof_file")" || die "cannot stat proxy proof file"
[ "$owner" = "$USER_UID" ] || die "proxy proof file must be owned by $USER_NAME (uid $USER_UID): $proof_file"
[ "$mode" = 600 ] || [ "$mode" = 400 ] || die "proxy proof file must be mode 0600 or 0400, not $mode: $proof_file"
echo "${flags# }"
}
# PATH: the node's directory first, then the default list without it. Unlike
# scripts/webui-service.sh:303-305 this always puts node's directory first,
# so the bus's `exec node` (scripts/mosaic) finds the same node.
NODE_DIR="$(dirname -- "$NODE")"
UNIT_PATH="$NODE_DIR"
for d in /usr/local/bin /usr/bin /bin; do [ "$d" = "$NODE_DIR" ] || UNIT_PATH="$UNIT_PATH:$d"; done
FLAGS="$(serve_flags)" || exit 1
HARDENING="ProtectSystem=strict
ReadWritePaths=$HOME_DIR
ProtectHome=yes
PrivateTmp=yes"
CAPS="AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE"
# The default instance, as scripts/instance.sh:85-107 renders it: no
# MOSAIC_CONFIG line, and the session env under %h, which becomes <home>.
SESSION_ENV='%h/.config/mosaic-dev/secrets/claude-session.env'
ENV_FILE="-$HOME_DIR/.config/mosaic-dev/secrets/claude-session.env"
PM_FLAGS=''
[ "$PM" = 0 ] || PM_FLAGS=' --pm'
CLAUDE_ENV=''
[ "$CLAUDE_SET" = 0 ] || CLAUDE_ENV="Environment=MOSAIC_CLAUDE_BIN=$CLAUDE"
# One unit from its template. kind is bus or webui.
render() {
local kind="$1" template="$2" text tokens
[ -f "$template" ] && [ ! -L "$template" ] || die "template missing or not a regular file: $template"
text="$(sed -e "s|@REPO@|$COPY|g" -e "s|@PATH@|$UNIT_PATH|g" -e "s|@NODE@|$NODE|g" \
-e "s|@PM_FLAGS@|$PM_FLAGS|g" -e "s|^@CLAUDE_ENV@\$|$CLAUDE_ENV|" \
-e '/^@CONFIG_ENV@$/d' -e "s|@SESSION_ENV@|$SESSION_ENV|g" \
-e "s|%h|$HOME_DIR|g" -- "$template")"
# Checked before @FLAGS@ goes in, since an allowed user may hold @.
tokens="$(printf '%s\n' "$text" | grep -oE '@[A-Z_]+@' || true)"
if [ "$kind" = webui ]; then
[ "$tokens" = @FLAGS@ ] || die "$template: a token other than one @FLAGS@ is left after the render: $(echo $tokens)"
text="${text//@FLAGS@/$FLAGS}"
else
[ -z "$tokens" ] || die "$template: a token is left after the render: $(echo $tokens)"
fi
if printf '%s\n' "$text" | grep -q '%h'; then die "$template: %h is left after the render"; fi
local add="User=$USER_NAME\nGroup=$GROUP\n${HARDENING//$'\n'/\\n}\n"
[ "$kind" = bus ] || add="$add${CAPS//$'\n'/\\n}\n"
# The added lines go at the end of [Service], before its trailing blank
# lines. In [Install], WantedBy=multi-user.target replaces
# WantedBy=default.target in both units, so both start at boot (Q189).
printf '%s\n' "$text" | awk -v add="$add" '
function blanks_out() { for (; blanks > 0; blanks--) print "" }
/^\[.*\]$/ {
if (sec == "[Service]") printf "%s", add
sec = $0
blanks_out(); print; next
}
$0 == "" { blanks++; next }
{ blanks_out() }
sec == "[Install]" && $0 == "WantedBy=default.target" { $0 = "WantedBy=multi-user.target" }
{ print }
END { if (sec == "[Service]") printf "%s", add; blanks_out() }'
}
# The unit's settings as section<TAB>key<TAB>value, one per line, keys
# trimmed as systemd trims them. A continued line refuses, so no setting
# hides in one.
settings() {
awk '
{ sub(/^[ \t]+/, ""); sub(/[ \t]+$/, "") }
$0 == "" || /^[#;]/ { next }
/\\$/ { print "CONTINUED"; exit }
/^\[.*\]$/ { sec = $0; next }
{
i = index($0, "="); if (i == 0) { print "NOKEY\t" $0; next }
k = substr($0, 1, i - 1); v = substr($0, i + 1)
sub(/[ \t]+$/, "", k); sub(/^[ \t]+/, "", v)
print sec "\t" k "\t" v
}'
}
# Refuses unless one unit's fixed lines hold. what names the unit in messages,
# copy is the copy directory it should run from.
check_unit() {
local kind="$1" text="$2" what="$3" copy="$4" s
s="$(printf '%s\n' "$text" | settings)"
! printf '%s\n' "$s" | grep -qx CONTINUED || die "$what: a line continues onto the next"
! printf '%s\n' "$s" | grep -q '^NOKEY' || die "$what: a line is neither a section, a comment nor a setting"
# %h in a system unit is root's home, not <user>'s.
! printf '%s\n' "$text" | grep -q '%h' || die "$what: %h is left; a system unit reads it as root's home"
local key
for key in RestrictNamespaces PrivateUsers SystemCallFilter PermissionsStartOnly; do
! printf '%s\n' "$s" | awk -F'\t' -v k="$key" '$2 == k { found = 1 } END { exit !found }' \
|| die "$what: refusing $key=, which the hub's units must not hold"
done
# An Exec*= value whose leading prefix characters hold + or ! runs with
# full privileges, whatever User= says.
local bad
bad="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^Exec/ { p = $3; sub(/[^-@:+!].*$/, "", p); if (p ~ /[+!]/) print $2 "=" $3 }')"
[ -z "$bad" ] || die "$what: refusing an Exec line that runs with full privileges: $bad"
# The fixed lines, each exactly once, in [Service].
one() {
local k="$1" v="$2" got
got="$(printf '%s\n' "$s" | awk -F'\t' -v k="$k" '$2 == k { print $1 "\t" $3 }')"
[ "$got" = "[Service] $v" ] || die "$what: $k= must be exactly once in [Service], as $k=$v"
}
none() {
! printf '%s\n' "$s" | awk -F'\t' -v k="$1" '$2 == k { found = 1 } END { exit !found }' || die "$what: refusing $1=, which this unit must not hold"
}
one User "$USER_NAME"
one Group "$GROUP"
one WorkingDirectory "$copy"
# Environment= lines: PATH, and MOSAIC_CLAUDE_BIN only with --claude.
local envs want_envs="[Service] PATH=$UNIT_PATH"
[ "$kind" = webui ] || [ -z "$CLAUDE_ENV" ] || want_envs="$want_envs
[Service] MOSAIC_CLAUDE_BIN=$CLAUDE"
envs="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "Environment" { print $1 "\t" $3 }')"
[ "$envs" = "$want_envs" ] || die "$what: the Environment= lines must be exactly: $(printf '%s' "$want_envs" | cut -f2 | tr '\n' ' ')"
one NoNewPrivileges yes
one ProtectSystem strict
one ReadWritePaths "$HOME_DIR"
one ProtectHome yes
one PrivateTmp yes
local order
order="$(printf '%s\n' "$s" | awk -F'\t' '$2 ~ /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)$/ { printf "%s ", $2 }')"
[ "$order" = "ProtectSystem ReadWritePaths ProtectHome PrivateTmp " ] || die "$what: the hardening lines must be in the order ProtectSystem, ReadWritePaths, ProtectHome, PrivateTmp"
local wanted
wanted="$(printf '%s\n' "$s" | awk -F'\t' '$2 == "WantedBy" { print $1 "\t" $3 }')"
[ "$wanted" = "[Install] multi-user.target" ] || die "$what: WantedBy= must be exactly once, as WantedBy=multi-user.target"
if [ "$kind" = bus ]; then
one ExecStart "$copy/scripts/mosaic bus start %i$PM_FLAGS"
one EnvironmentFile "$ENV_FILE"
none AmbientCapabilities
none CapabilityBoundingSet
else
one ExecStart "$NODE $copy/packages/webui/src/cli.mjs serve $FLAGS"
none EnvironmentFile
one AmbientCapabilities CAP_NET_BIND_SERVICE
one CapabilityBoundingSet CAP_NET_BIND_SERVICE
fi
}
BUS_TEXT="$(render bus "$BUS_TEMPLATE")" || exit 1
WEBUI_TEXT="$(render webui "$WEBUI_TEMPLATE")" || exit 1
check_unit bus "$BUS_TEXT" "rendered $BUS_UNIT" "$COPY"
check_unit webui "$WEBUI_TEXT" "rendered $WEBUI_UNIT" "$COPY"
# A re-install: each installed unit passes the same checks, both name one
# copy, and each equals the render once its copy id is the new one.
[ -d "$UNIT_DIR" ] && [ ! -L "$UNIT_DIR" ] || die "no unit directory $UNIT_DIR"
installed_ids=''
for pair in "bus:$BUS_UNIT:BUS_TEXT" "webui:$WEBUI_UNIT:WEBUI_TEXT"; do
IFS=: read -r kind name var <<<"$pair"
path="$UNIT_DIR/$name"
[ ! -L "$path" ] || die "refusing: $path is a symlink"
[ -e "$path" ] || continue
[ -f "$path" ] || die "refusing: $path is not a regular file"
text="$(cat -- "$path")"
old="$(printf '%s\n' "$text" | settings | awk -F'\t' -v c="$COPIES/" '$2 == "WorkingDirectory" && index($3, c) == 1 { print substr($3, length(c) + 1) }')"
[[ "$old" =~ ^[0-9a-f]{40}$ ]] || die "installed $path names no copy under $COPIES"
check_unit "$kind" "$text" "installed $path" "$COPIES/$old"
[ "$text" = "$(printf '%s\n' "${!var}" | sed "s|$COPY|$COPIES/$old|g")" ] \
|| die "installed $path differs from the render in more than the copy id; the node path, serve flags, --pm and --claude must equal the installed ones"
installed_ids="$installed_ids $old"
done
read -r -a ids <<<"$installed_ids"
[ "${#ids[@]}" -lt 2 ] || [ "${ids[0]}" = "${ids[1]}" ] || die "the installed units name different copies, ${ids[0]} and ${ids[1]}"
# `systemctl start` leaves a running unit on the copy it started from, so
# note which units run before the write (row 195 owns the restart).
BUS_NAME="mosaic-bus@$BUSINESS.service"
running=''
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
if "$SYSTEMCTL" is-active --quiet "$name"; then running="$running $name"; fi
done
# Write each unit to a temp file in the unit directory, then rename.
write_unit() {
local name="$1" text="$2" tmp
tmp="$(mktemp "$UNIT_DIR/.case2-unit.XXXXXX")"
printf '%s\n' "$text" >"$tmp"
chmod 0644 "$tmp"
[ "$(id -u)" != 0 ] || chown root:root "$tmp"
mv -fT -- "$tmp" "$UNIT_DIR/$name"
}
write_unit "$BUS_UNIT" "$BUS_TEXT"
write_unit "$WEBUI_UNIT" "$WEBUI_TEXT"
# Enable both, then start the bus, then the Console (design 10 step 2c).
# serve calls the bus per request, so the units carry no After= or Wants=.
"$SYSTEMCTL" daemon-reload || die "systemctl daemon-reload failed; the units are written, nothing is enabled or started"
"$SYSTEMCTL" enable "$BUS_NAME" "$WEBUI_UNIT" || die "systemctl enable $BUS_NAME $WEBUI_UNIT failed; nothing is started"
"$SYSTEMCTL" start "$BUS_NAME" || die "systemctl start $BUS_NAME failed; the Console is not started"
"$SYSTEMCTL" start "$WEBUI_UNIT" || die "systemctl start $WEBUI_UNIT failed; the bus is running"
if [ -z "$running" ]; then
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID; enabled and started $BUS_NAME, then $WEBUI_UNIT"
exit 0
fi
echo "case2-unit: wrote $UNIT_DIR/$BUS_UNIT and $UNIT_DIR/$WEBUI_UNIT from copy $COPY_ID and enabled both"
for name in "$BUS_NAME" "$WEBUI_UNIT"; do
case " $running " in
*" $name "*) echo "case2-unit: $name was already running, so it is still on ${ids[0]:+copy }${ids[0]:-the copy it started from}, not copy $COPY_ID" ;;
*) echo "case2-unit: started $name on copy $COPY_ID" ;;
esac
done
echo "case2-unit: restart $BUS_NAME and $WEBUI_UNIT together to put both on copy $COPY_ID; until then they may run different copies"
@@ -0,0 +1,872 @@
// Case 2 row C2-13a (#1732): scripts/case2-unit.sh renders and installs the hub's two system units,
// and scripts/case2-precheck.sh checks the host before they start. Brief: docs/plans/2026-10-20_case2-rows.md
// "C2-13a". Every case runs against a scratch unit directory, a scratch home and fake systemctl,
// getent, sudo, id, node, bwrap and claude. Nothing here reads or writes /etc/systemd,
// ~/.mosaic-dev or ~/.config/mosaic-dev, and no fixture value is a real credential.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createServer } from "node:net";
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { businessDoc, writeJson } from "../../business/tests/helpers.mjs";
import { binds, prepareRun, sandboxArgv, sandboxProblem } from "../../seat/src/sandbox.mjs";
import { tmp } from "./helpers.mjs";
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const UNIT = join(REPO, "scripts", "case2-unit.sh");
const PRECHECK = join(REPO, "scripts", "case2-precheck.sh");
const BUS_TEMPLATE = join(REPO, "packages", "cli", "systemd", "mosaic-bus.service.in");
const WEBUI_TEMPLATE = join(REPO, "packages", "webui", "systemd", "mosaic-webui.service.in");
const COPY_A = "a".repeat(40);
const COPY_B = "b".repeat(40);
const UID = process.getuid();
const GID = process.getgid();
// A made-up key value; no output may hold it.
const FIXTURE_KEY = "fixture-zai-key-3c9e51";
// A root-owned executable outside /usr/local/bin, /usr/bin and /bin, for the PATH case.
function outsideNode() {
const dir = "/usr/lib/systemd";
for (const name of readdirSync(dir).sort()) {
const p = join(dir, name);
const s = statSync(p, { throwIfNoEntry: false });
if (s?.isFile() && s.uid === 0 && (s.mode & 0o111) && (s.mode & 0o022) === 0 && realpathSync(p) === p) return p;
}
throw new Error(`no root-owned executable in ${dir}`);
}
const USR_NODE = realpathSync("/usr/bin/node");
const TRUE_BIN = realpathSync("/usr/bin/true");
function script(file, text) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, `#!/usr/bin/env bash\n${text}\n`);
chmodSync(file, 0o755);
return file;
}
// A scratch hub: a home with two copies and a proof file, a unit directory, and fakes.
function hub(t, { uid = UID } = {}) {
const root = tmp(t, "case2-unit-");
const home = join(root, "home", "mosaic");
for (const id of [COPY_A, COPY_B]) {
mkdirSync(join(home, ".local", "share", "mosaic-stack", "copies", id, "scripts"), { recursive: true });
writeFileSync(join(home, ".local", "share", "mosaic-stack", "copies", id, "scripts", "mosaic"), "");
}
const secrets = join(home, ".config", "mosaic-dev", "secrets");
mkdirSync(secrets, { recursive: true, mode: 0o700 });
const proof = join(secrets, "proxy-proof");
writeFileSync(proof, "made-up-proof\n");
chmodSync(proof, 0o600);
const units = join(root, "units");
mkdirSync(units);
const bin = join(root, "bin");
const log = join(root, "systemctl.log");
script(join(bin, "getent"), `case "$1" in
passwd) [ "$2" = mosaic ] && echo "mosaic:x:${uid}:${GID}::${home}:/bin/bash" ;;
group) [ "$2" = "${GID}" ] && echo "mosaic:x:${GID}:" ;;
*) exit 2 ;;
esac`);
// is-active answers yes for each unit named in CASE2_TEST_ACTIVE.
script(join(bin, "systemctl"), `echo "$*" >>'${log}'
if [ "$1" = is-active ]; then case " \${CASE2_TEST_ACTIVE:-} " in *" $3 "*) exit 0 ;; *) exit 3 ;; esac; fi
[ "$*" != "\${CASE2_TEST_FAIL:-}" ]`);
const dataRoot = join(home, '.mosaic-dev'), query = join(root, 'query.json'), sudoLog = join(root, 'sudo.log');
mkdirSync(dataRoot, { recursive: true });
writeJson(query, { dataRoot });
for (const copy of [COPY_A, COPY_B]) {
const src = join(home, '.local/share/mosaic-stack/copies', copy, 'packages');
mkdirSync(join(src, 'cli/src'), { recursive: true }); mkdirSync(join(src, 'webui/src'), { recursive: true });
writeFileSync(join(src, 'cli/src/config.mjs'), `import fs from 'node:fs';export const loadSystem=()=>JSON.parse(fs.readFileSync(${JSON.stringify(query)},'utf8'));`);
writeFileSync(join(src, 'webui/src/cli.mjs'), '// MOSAIC_TESTER_SOCKET_V1\n');
}
script(join(bin, 'sudo'), `${CASE_SUDO_SESSION}
echo "$*" >>'${sudoLog}'
[ "$1" = -u ] && [ "$2" = mosaic ] && [ "$3" = -H ] || exit 97
shift 3
[ "$1" = env ] && [ "$2" = -i ] && [ "$3" = 'HOME=${home}' ] || exit 98
if [ -f '${root}/query-hang' ]; then
trap '' TERM
(trap '' TERM; echo "$BASHPID" >>'${root}/query-pids'; exec sleep 60) &
echo "$BASHPID" >>'${root}/query-pids'; wait; exit
fi
if [ -f '${root}/query-orphan' ]; then
(trap '' TERM; here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here"); printf '%s %s\\n' "$here" "$sid" >'${root}/query-owned'; exec sleep 60) </dev/null >/dev/null 2>&1 &
echo "$!" >'${root}/query-pids'
cat '${root}/query-output'; exit 0
fi
if [ -f '${root}/query-output' ]; then cat '${root}/query-output'; exit "$(cat '${root}/query-status')"; fi
case_sudo_run '${root}' "$@"`);
return { root, home, secrets, proof, units, bin, log, dataRoot, query, sudoLog };
}
const libraryFlags = (h) => ["--port", "443", "--business", "acme", "--public-host", "books.example", "--auth-header", "X-Auth-User",
"--allow-user", "melanie", "--send", "--proof-header", "X-Proxy-Proof", "--proof-file", h.proof];
const factoryFlags = () => ["--port", "443", "--business", "acme", "--tester-socket"];
function unit(h, { profile = "library", node = USR_NODE, copy = COPY_A, extra = [], flags, env = {} } = {}) {
const serve = flags ?? (profile === "factory" ? factoryFlags() : libraryFlags(h));
const args = ["--user", "mosaic", "--home", h.home, "--business", "acme", "--copy", copy, "--node", node, "--profile", profile, ...extra, "--", ...serve];
const r = spawnSync(UNIT, args, {
encoding: "utf8",
timeout: 30000,
env: { PATH: `${h.bin}:${process.env.PATH}`, CASE2_UNIT_DIR: h.units, CASE2_SYSTEMCTL: join(h.bin, "systemctl"), CASE2_GETENT: join(h.bin, "getent"), ...env },
});
return { code: r.status, out: r.stdout, err: r.stderr };
}
const busFile = (h) => join(h.units, "[email protected]");
const webuiFile = (h) => join(h.units, "mosaic-webui.service");
const read = (f) => readFileSync(f, "utf8");
const lines = (text, key) => text.split("\n").filter((l) => l.startsWith(`${key}=`)).map((l) => l.slice(key.length + 1));
const copyDir = (h, id = COPY_A) => join(h.home, ".local", "share", "mosaic-stack", "copies", id);
const HARDENING = (h) => ["ProtectSystem=strict", `ReadWritePaths=${h.home}`, "ProtectHome=yes", "PrivateTmp=yes"];
test('row253 factory socket uses actual config and target-uid query without widening hardening', t=>{
const h=hub(t),custom=join(h.home,'custom');mkdirSync(custom);writeJson(h.query,{dataRoot:custom});
const r=unit(h,{profile:'factory'});assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(lines(read(webuiFile(h)),'ExecStart'),[`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${factoryFlags().join(' ')}`]);
assert.deepEqual(read(webuiFile(h)).split('\n').filter(l=>/^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)=/.test(l)),HARDENING(h));
assert.match(read(h.sudoLog),new RegExp(`^-u mosaic -H env -i HOME=${h.home} PATH=`, 'm'));
assert.equal(existsSync(join(custom,'console')),false,'installer never creates the socket parent');
});
test('row253 installer refuses unsafe location and query facts before unit writes',t=>{
const cases=[
['sibling prefix',h=>writeJson(h.query,{dataRoot:h.home+'2/data'}),/outside.*home/],
['dot',h=>writeJson(h.query,{dataRoot:h.home+'/./data'}),/normalized/],
['dotdot',h=>writeJson(h.query,{dataRoot:h.home+'/x/../data'}),/normalized/],
['double slash',h=>writeJson(h.query,{dataRoot:h.home+'//data'}),/normalized/],
['trailing slash',h=>writeJson(h.query,{dataRoot:h.home+'/data/'}),/normalized/],
['newline',h=>writeJson(h.query,{dataRoot:h.home+'/x\nP10 forged closed'}),/query/],
['non ASCII',h=>writeJson(h.query,{dataRoot:h.home+'/é'}),/query/],
['middle symlink',h=>{symlinkSync(h.dataRoot,join(h.home,'link'));writeJson(h.query,{dataRoot:join(h.home,'link','nested')});},/symlink/],
['long path',h=>writeJson(h.query,{dataRoot:join(h.home,'a'.repeat(108))}),/too.long/],
['parent file',h=>writeFileSync(join(h.dataRoot,'console'),'x',{mode:0o600}),/parent-type/],
['parent symlink',h=>symlinkSync(h.home,join(h.dataRoot,'console')),/symlink/],
['parent mode',h=>{mkdirSync(join(h.dataRoot,'console'));chmodSync(join(h.dataRoot,'console'),0o755);},/parent/],
['foreign parent',h=>{mkdirSync(join(h.dataRoot,'console'),{mode:0o700});},/parent/],
['unsupported',h=>writeFileSync(join(copyDir(h),'packages/webui/src/cli.mjs'),'// old copy'),/support/],
['schema',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true,extra:true}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['wrong type',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:'true'}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['noisy query',h=>{writeFileSync(join(h.root,'query-output'),'noise\n'+JSON.stringify({dataRoot:h.dataRoot,socket:true}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
['failed valid query',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));writeFileSync(join(h.root,'query-status'),'1');},/query/],
];
for(const [name,change,why] of cases){
const h=hub(t,{uid:name==='foreign parent'?UID+4242:UID});change(h);
const r=unit(h,{profile:'factory'});refuses(r,why);
assert.equal(existsSync(busFile(h)),false,name);assert.equal(existsSync(webuiFile(h)),false,name);assert.equal(existsSync(h.log),false,name);
assert.equal((r.out+r.err).includes('P10 forged closed'),false,name);
}
});
test('row253 query timeout kills its process group and leaves no running child',async t=>{
const h=hub(t);writeFileSync(join(h.root,'query-hang'),'');
const r=unit(h,{profile:'factory'});refuses(r,/query/);
const pids=read(join(h.root,'query-pids')).trim().split('\n').map(Number);assert.ok(pids.length>=2);
for(const pid of pids){
let state='gone';
for(let n=0;n<50;n++){
try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`query descendant ${pid} still ${state}`);
}
assert.equal(existsSync(h.log),false);assert.equal(existsSync(busFile(h)),false);
});
test('row253 protected homes refuse before any filesystem access',()=>{
const source=read(UNIT), body=source.slice(source.indexOf('tester_location() {'),source.indexOf('# tester-location-end'));
for(const home of ['/home/mosaic','/root/mosaic','/run/user/997','/tmp/mosaic','/var/tmp/mosaic']){
const r=spawnSync('/bin/bash',['-c',body+'\nif tester_location "$1/data" "$1" 997 ""; then exit 99; fi; printf "%s" "$TS_REASON"','--',home],{encoding:'utf8'});
assert.equal(r.status,0);assert.equal(r.stdout,'protected-private-home');
}
});
// C4 cases run through both sudo process models. Exec shares the runner's
// session, so its session-wide check would be vacuous; exact recorded PIDs
// are checked in both modes. Hop additionally isolates the fake session.
for(const kind of ['sleep','signals','member']) test(`row253 C4 ${kind} query has exact inner reason and cleans all members`,async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),sessions=join(h.root,'session-pids'),queries=join(h.root,'query-pids'),files=[sessions,queries];
writeFileSync(join(h.root,'session-mode'),mode);
const member=join(h.root,'q ) '+String.fromCharCode(10)+'mem'),proof=join(h.root,'comm-proof');
if(kind==='member')symlinkSync('/usr/bin/sleep',member);
writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),blockingQuerySource(queries,kind,member,proof));
const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
refuses(r,/^case2-unit: tester config query inner-timeout\n$/);
assert.ok(seconds<7,`${kind}/${mode}: ${seconds}s exceeds outer bound`);
assert.deepEqual(await pollFixtureGone(files),[],mode);
if(kind==='member')assert.ok(read(proof).includes('('+member.split('/').at(-1)+') '),'member comm with newline and closing parenthesis was observed');
assert.equal(existsSync(h.log),false,mode);await stopGuard();
}
});
test('row253 C4 overflow is output-limit in both sudo modes',async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),files=[join(h.root,'session-pids')];writeFileSync(join(h.root,'session-mode'),mode);const capture=cleanupFixture(t,files);
writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:${JSON.stringify(h.dataRoot)}});`);
const r=unit(h,{profile:'factory'});capture();refuses(r,/^case2-unit: tester config query output-limit\n$/);
assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
assert.deepEqual(await pollFixtureGone(files),[]);
}
});
test('row253 C4 stalled supervisor uses the outer reason and explicit residual cleanup',async t=>{
for(const mode of ['exec','hop']){
const h=hub(t),file=join(h.root,'session-pids'),files=[file];writeFileSync(join(h.root,'session-mode'),mode);
writeFileSync(join(h.root,'stall-config'),'');writeFileSync(join(h.root,'stall-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
refuses(r,/^case2-unit: tester config query outer-timeout\n$/);
assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s slack includes root preflight`);
assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
if(mode==='hop')assert.ok(recordedLivePids(file).length>0,'R3 residual exists before fixture cleanup');
killRecordedPids(file);assert.deepEqual(await pollFixtureGone(files),[], 'explicit fixture cleanup of R3 residual');await stopGuard();
}
});
test('row253 C4 inline supervisor is byte-identical in both host scripts',()=>{
const extract=s=>s.split('# row253-supervisor-begin\n')[1]?.split('# row253-supervisor-end')[0];
const a=extract(read(UNIT)),b=extract(read(join(REPO,'scripts/case1-accept.sh')));
assert.ok(a?.length>100);assert.equal(a,b);
});
test('row253 C4 cleanup budget starts after inventory pass one',()=>{
// A deterministic clock models a slow inventory without host-load timing.
// All process reads and signals are stand-ins; no real PID is signalled.
const source=read(UNIT);
const body=source.slice(source.indexOf('stat_fields() {'),source.indexOf('finish() {'));
const standins=`
child=424242; sleep_bin=:; seen=0; clocks=0; kills=0
stat_fields() {
seen=$((seen+1)); ST_STATE=R; ST_SESSION=0
[ "$seen" != 1 ] || ST_SESSION="$child"
return 0
}
clock_cs() { clocks=$((clocks+1)); CLOCK_CS=$((clocks*1000)); }
kill() { kills=$((kills+1)); return 0; }
clean_session; rc=$?
printf '%s' "$kills"
exit "$rc"
`;
const r=spawnSync('/bin/bash',['-c',body+standins],{encoding:'utf8'});
assert.equal(r.status,0,r.stderr);assert.equal(r.stdout,'1');
});
test('row253 query reaps its group even when a child closes stdout before the query returns',async t=>{
const h=hub(t);writeFileSync(join(h.root,'query-orphan'),'');
writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
let pid;
const capture=cleanupFixture(t,[join(h.root,'query-owned')]);
const r=unit(h,{profile:'factory'});capture();
pid=Number(read(join(h.root,'query-pids')).trim());
assert.equal(r.code,0,r.out+r.err);
let state='gone';
for(let n=0;n<50;n++){
try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{state='gone';}
if(state==='gone'||state==='Z')break;
await new Promise(resolve=>setTimeout(resolve,10));
}
assert.ok(state==='gone'||state==='Z',`query descendant ${pid} still ${state}`);
});
test('row253 config parser is strict',()=>{
const source=read(UNIT), body=source.slice(source.indexOf('tester_facts() {'),source.indexOf('tester_location() {'));
const valid=spawnSync('/bin/bash',['-c',body+'\ntester_facts "$1" && printf "%s %s" "$TS_DATA" "$TS_MARKED"','--','{"dataRoot":"/srv/mosaic/data","socket":true}'],{encoding:'utf8'});
assert.equal(valid.status,0);assert.equal(valid.stdout,'/srv/mosaic/data true');
const facts=['{"dataRoot":"/srv/mosaic/data","socket":true,"extra":1}','{"dataRoot":4,"socket":true}','{"dataRoot":"/srv/mosaic/data","socket":"true"}','{"dataRoot":"/srv/mosaic/data","socket":true}\nP10 forged closed'];
for(const fact of facts){
const r=spawnSync('/bin/bash',['-c',body+'\ntester_facts "$1"','--',fact]);
assert.notEqual(r.status,0,fact);
}
});
function refuses(r, pattern) {
assert.equal(r.code, 1, `expected a refusal, got ${r.code}: ${r.out}${r.err}`);
assert.match(r.err, pattern);
}
// A template copy with one edit, for the template cases.
function template(h, from, name, edit) {
const file = join(h.root, name);
writeFileSync(file, edit(read(from)));
return file;
}
test("a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console", (t) => {
const h = hub(t);
const r = unit(h);
assert.equal(r.code, 0, r.err);
for (const f of [busFile(h), webuiFile(h)]) assert.equal(statSync(f).mode & 0o777, 0o644);
const bus = read(busFile(h));
const webui = read(webuiFile(h));
assert.deepEqual(lines(bus, "Environment"), ["PATH=/usr/bin:/usr/local/bin:/bin"]);
assert.deepEqual(lines(bus, "ExecStart"), [`${copyDir(h)}/scripts/mosaic bus start %i`]);
assert.deepEqual(lines(bus, "EnvironmentFile"), [`-${h.home}/.config/mosaic-dev/secrets/claude-session.env`]);
assert.deepEqual(lines(webui, "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${libraryFlags(h).join(" ")}`]);
assert.deepEqual(lines(webui, "EnvironmentFile"), []);
for (const text of [bus, webui]) {
assert.deepEqual(lines(text, "WantedBy"), ["multi-user.target"]);
assert.deepEqual(lines(text, "User"), ["mosaic"]);
assert.deepEqual(lines(text, "Group"), ["mosaic"]);
assert.deepEqual(lines(text, "NoNewPrivileges"), ["yes"]);
const hardening = text.split("\n").filter((l) => /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)=/.test(l));
assert.deepEqual(hardening, HARDENING(h));
assert.doesNotMatch(text, /@[A-Z_]+@|%h|MOSAIC_CONFIG=|MOSAIC_CLAUDE_BIN|After=|Wants=/);
}
assert.deepEqual(lines(bus, "AmbientCapabilities"), []);
assert.deepEqual(lines(bus, "CapabilityBoundingSet"), []);
assert.deepEqual(lines(webui, "AmbientCapabilities"), ["CAP_NET_BIND_SERVICE"]);
assert.deepEqual(lines(webui, "CapabilityBoundingSet"), ["CAP_NET_BIND_SERVICE"]);
assert.match(r.out, /enabled and started mosaic-bus@acme\.service, then mosaic-webui\.service\n$/);
assert.deepEqual(read(h.log).trim().split("\n"), [
"is-active --quiet [email protected]",
"is-active --quiet mosaic-webui.service",
"daemon-reload",
"enable [email protected] mosaic-webui.service",
"start [email protected]",
"start mosaic-webui.service",
]);
});
test("PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node", (t) => {
const h = hub(t);
const node = outsideNode();
assert.equal(unit(h, { node }).code, 0);
for (const f of [busFile(h), webuiFile(h)]) assert.deepEqual(lines(read(f), "Environment"), [`PATH=${dirname(node)}:/usr/local/bin:/usr/bin:/bin`]);
assert.match(lines(read(webuiFile(h)), "ExecStart")[0], new RegExp(`^${node} `));
});
test("a failed bus start refuses with the Console not started", (t) => {
const h = hub(t);
const r = unit(h, { env: { CASE2_TEST_FAIL: "start [email protected]" } });
refuses(r, /start mosaic-bus@acme\.service failed; the Console is not started/);
assert.deepEqual(read(h.log).trim().split("\n").slice(2), ["daemon-reload", "enable [email protected] mosaic-webui.service", "start [email protected]"]);
});
test("the script never links a unit file", () => {
assert.doesNotMatch(read(UNIT), /systemctl link/);
});
test("factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty", (t) => {
const h = hub(t);
assert.equal(unit(h, { profile: "factory", extra: ["--pm", "--claude", TRUE_BIN] }).code, 0);
const bus = read(busFile(h));
const exec = lines(bus, "ExecStart");
assert.equal(exec.length, 1);
assert.match(exec[0], / bus start %i --pm$/);
assert.equal(exec[0].split("--pm").length, 2);
assert.deepEqual(lines(bus, "Environment"), ["PATH=/usr/bin:/usr/local/bin:/bin", `MOSAIC_CLAUDE_BIN=${TRUE_BIN}`]);
assert.deepEqual(lines(read(webuiFile(h)), "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${factoryFlags().join(" ")}`]);
const plain = hub(t);
assert.equal(unit(plain, { profile: "factory" }).code, 0);
const text = read(busFile(plain));
assert.deepEqual(lines(text, "ExecStart"), [`${copyDir(plain)}/scripts/mosaic bus start %i`]);
assert.doesNotMatch(text, /MOSAIC_CLAUDE_BIN|@PM_FLAGS@|@CLAUDE_ENV@/);
});
test("a re-install over running units says they are still on the old copy and must restart together, never that it started them", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
const both = unit(h, { copy: COPY_B, env: { CASE2_TEST_ACTIVE: "[email protected] mosaic-webui.service" } });
assert.equal(both.code, 0, both.err);
assert.doesNotMatch(both.out, /started/);
assert.match(both.out, new RegExp(`mosaic-bus@acme\\.service was already running, so it is still on copy ${COPY_A}, not copy ${COPY_B}`));
assert.match(both.out, new RegExp(`mosaic-webui\\.service was already running, so it is still on copy ${COPY_A}, not copy ${COPY_B}`));
assert.match(both.out, new RegExp(`restart mosaic-bus@acme\\.service and mosaic-webui\\.service together to put both on copy ${COPY_B}`));
assert.deepEqual(read(h.log).trim().split("\n").slice(-2), ["start [email protected]", "start mosaic-webui.service"]);
// Only the Console ran (the bus had stopped): the bus starts on the new copy, the Console stays.
const one = unit(h, { copy: COPY_A, env: { CASE2_TEST_ACTIVE: "mosaic-webui.service" } });
assert.equal(one.code, 0, one.err);
assert.match(one.out, new RegExp(`started mosaic-bus@acme\\.service on copy ${COPY_A}`));
assert.match(one.out, new RegExp(`mosaic-webui\\.service was already running, so it is still on copy ${COPY_B}, not copy ${COPY_A}`));
assert.doesNotMatch(one.out, /started mosaic-webui/);
assert.match(one.out, /restart mosaic-bus@acme\.service and mosaic-webui\.service together/);
});
test("a re-install to another copy re-renders both units, and an edited installed unit refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
assert.equal(unit(h, { copy: COPY_B }).code, 0);
assert.match(read(busFile(h)), new RegExp(`copies/${COPY_B}/scripts/mosaic`));
assert.doesNotMatch(read(busFile(h)) + read(webuiFile(h)), new RegExp(COPY_A));
const edits = [
[busFile, (s) => s.replace("User=mosaic", "User=root"), /User= must be exactly once in \[Service\], as User=mosaic/],
[webuiFile, (s) => s.replace(/^ExecStart=(.*)$/m, "ExecStart=$1 --board http://127.0.0.1:1"), /ExecStart= must be exactly once/],
[busFile, (s) => s.replace("ProtectHome=yes\n", ""), /ProtectHome= must be exactly once/],
[webuiFile, (s) => s.replace("PrivateTmp=yes\n", ""), /PrivateTmp= must be exactly once/],
[busFile, (s) => s.replace("Type=simple", "Type=simple\nX-Note=%h"), /%h is left/],
];
for (const [file, edit, pattern] of edits) {
const before = read(file(h));
writeFileSync(file(h), edit(before));
refuses(unit(h, { copy: COPY_B }), pattern);
writeFileSync(file(h), before);
}
assert.equal(unit(h, { copy: COPY_B }).code, 0);
});
test("a template with a namespace, user or system call restriction refuses", (t) => {
const h = hub(t);
for (const line of ["RestrictNamespaces=yes", "PrivateUsers=yes", "SystemCallFilter=@system-service"]) {
const key = line.split("=")[0];
for (const [from, env] of [[BUS_TEMPLATE, "CASE2_BUS_TEMPLATE"], [WEBUI_TEMPLATE, "CASE2_WEBUI_TEMPLATE"]]) {
const file = template(h, from, `t-${key}.in`, (s) => s.replace("NoNewPrivileges=yes", `NoNewPrivileges=yes\n${line}`));
refuses(unit(h, { env: { [env]: file } }), new RegExp(`refusing ${key}=`));
}
}
assert.equal(existsSync(busFile(h)), false);
});
test("the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none", (t) => {
const h = hub(t);
const gone = template(h, BUS_TEMPLATE, "no-env.in", (s) => s.replace(/^EnvironmentFile=.*\n/m, ""));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: gone } }), /EnvironmentFile= must be exactly once/);
const elsewhere = template(h, BUS_TEMPLATE, "env-elsewhere.in", (s) => s.replace("EnvironmentFile=-@SESSION_ENV@", "EnvironmentFile=-/etc/other.env"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: elsewhere } }), /EnvironmentFile= must be exactly once/);
const added = template(h, WEBUI_TEMPLATE, "webui-env.in", (s) => s.replace("NoNewPrivileges=yes", "NoNewPrivileges=yes\nEnvironmentFile=-/etc/other.env"));
refuses(unit(h, { env: { CASE2_WEBUI_TEMPLATE: added } }), /refusing EnvironmentFile=/);
});
test("a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses", (t) => {
const h = hub(t);
const token = template(h, BUS_TEMPLATE, "token.in", (s) => s.replace("Type=simple", "Type=simple\nX-Note=@SECRET@"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: token } }), /a token is left after the render: @SECRET@/);
const webuiToken = template(h, WEBUI_TEMPLATE, "wtoken.in", (s) => s.replace("Type=simple", "Type=simple\nX-Note=@SECRET@"));
refuses(unit(h, { env: { CASE2_WEBUI_TEMPLATE: webuiToken } }), /a token other than one @FLAGS@/);
const pso = template(h, BUS_TEMPLATE, "pso.in", (s) => s.replace("Type=simple", "Type=simple\nPermissionsStartOnly=yes"));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: pso } }), /refusing PermissionsStartOnly=/);
for (const pre of ["+/srv/mosaic/x", "-+/srv/mosaic/x"]) {
const file = template(h, BUS_TEMPLATE, "pre.in", (s) => s.replace("Type=simple", `Type=simple\nExecStartPre=${pre}`));
refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: file } }), /an Exec line that runs with full privileges: ExecStartPre=/);
}
});
test("a symlink at either unit path refuses", (t) => {
for (const name of ["[email protected]", "mosaic-webui.service"]) {
const h = hub(t);
const target = join(h.root, "elsewhere");
writeFileSync(target, "");
symlinkSync(target, join(h.units, name));
refuses(unit(h), new RegExp(`${name.replace(".", "\\.")} is a symlink`));
}
});
test("a --node that is relative, not root-owned or under <home> refuses", (t) => {
const h = hub(t);
refuses(unit(h, { node: "bin/node" }), /--node must be an absolute path/);
const mine = script(join(h.root, "opt", "node"), "exit 0");
refuses(unit(h, { node: mine }), /--node: .* is not owned by root/);
const inside = script(join(h.home, "bin", "node"), "exit 0");
refuses(unit(h, { node: inside }), /--node must be outside/);
});
test("a re-install with a different --node or different serve flags refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
// A new node moves PATH too, so the installed unit fails its own Environment= check first.
refuses(unit(h, { node: outsideNode() }), /installed .*: the Environment= lines must be exactly/);
const flags = libraryFlags(h).map((f) => (f === "melanie" ? "someone" : f));
refuses(unit(h, { flags }), /installed .*: ExecStart= must be exactly once/);
// A line check_unit doesn't fix still has to equal the render.
writeFileSync(busFile(h), read(busFile(h)).replace(/^Description=.*$/m, "Description=edited"));
refuses(unit(h), /differs from the render in more than the copy id/);
});
test("an installed pair whose units name different copies refuses", (t) => {
const h = hub(t);
assert.equal(unit(h).code, 0);
writeFileSync(webuiFile(h), read(webuiFile(h)).replaceAll(COPY_A, COPY_B));
refuses(unit(h), new RegExp(`the installed units name different copies, ${COPY_A} and ${COPY_B}`));
});
test("a copy id that is short, uppercase, holds / or .., or names no directory refuses", (t) => {
const h = hub(t);
for (const copy of ["a".repeat(39), "A".repeat(40), `${"a".repeat(19)}/${"a".repeat(20)}`, `..${"a".repeat(38)}`]) {
refuses(unit(h, { copy }), /--copy must be a full 40-character lower-case commit id/);
}
refuses(unit(h, { copy: "c".repeat(40) }), /no copy directory/);
});
test("library refuses a privileged-floor miss, a bad proof pair and every flag outside its set", (t) => {
const h = hub(t);
const base = libraryFlags(h);
const without = (...names) => {
const out = [];
for (let i = 0; i < base.length; i++) {
if (names.includes(base[i])) { if (base[i] !== "--send") i++; continue; }
out.push(base[i]);
}
return out;
};
refuses(unit(h, { flags: base.map((f) => (f === "443" ? "7332" : f)) }), /--port must be 1\.\.1023/);
refuses(unit(h, { flags: without("--proof-header", "--proof-file") }), /serve flag --proof-header is required/);
refuses(unit(h, { flags: base.map((f) => (f === "X-Auth-User" ? "X-Authentik-Uid" : f)) }), /must be an Authentik uid/);
assert.equal(unit(h, { flags: base.map((f) => (f === "X-Auth-User" ? "X-Authentik-Uid" : f === "melanie" ? "0".repeat(64) : f)) }).code, 0);
rmSync(busFile(h));
rmSync(webuiFile(h));
for (const [flag, value, pattern] of [
["--tester-port", "7333", /case2-unit.sh retires tester TCP; use --tester-socket/],
["--tester-socket", null, /the library profile refuses --tester-socket/],
["--checkout", "/srv/x", /refusing --checkout/],
["--board", "http://127.0.0.1:7331", /refusing --board/],
["--sso", null, /refusing --sso/],
["--nonsense", "1", /unknown serve flag: --nonsense/],
]) {
refuses(unit(h, { flags: [...base, flag, ...(value === null ? [] : [value])] }), pattern);
}
refuses(unit(h, { flags: base.map((f) => (f === "acme" ? "other" : f)) }), /serve --business other is not the unit's business acme/);
refuses(unit(h, { extra: ["--pm"] }), /the library profile refuses --pm/);
refuses(unit(h, { extra: ["--claude", TRUE_BIN] }), /the library profile refuses --claude/);
chmodSync(h.proof, 0o644);
refuses(unit(h), /mode 0600 or 0400, not 644/);
chmodSync(h.proof, 0o600);
const link = join(h.secrets, "proof-link");
symlinkSync(h.proof, link);
refuses(unit(h, { flags: base.map((f) => (f === h.proof ? link : f)) }), /must not be a symlink/);
const dataRoot = join(h.home, ".mosaic-dev");
mkdirSync(dataRoot, { recursive: true });
const outside = join(dataRoot, "proof");
writeFileSync(outside, "made-up-proof\n");
chmodSync(outside, 0o600);
refuses(unit(h, { flags: base.map((f) => (f === h.proof ? outside : f)) }), /must be under .*secrets, outside the data root/);
assert.equal(existsSync(busFile(h)), false);
});
// A non-root test can't make a root-owned file under the scratch home, so the
// owner case names another uid in the passwd database instead.
test("library refuses a proof file not owned by <user>'s uid", (t) => {
const h = hub(t, { uid: UID + 4242 });
refuses(unit(h), new RegExp(`proxy proof file must be owned by mosaic \\(uid ${UID + 4242}\\)`));
});
test("factory refuses duplicate socket, a missing --port or --business, and each proxy flag", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), '--tester-socket'] }), /given twice/);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), '--port', '443'] }), /given twice/);
refuses(unit(h, { profile: "factory", flags: ["--business", "acme"] }), /serve flag --port is required/);
refuses(unit(h, { profile: "factory", flags: ["--port", "443"] }), /serve flag --business is required/);
for (const [flag, value] of [["--public-host", "x.example"], ["--auth-header", "X-A"], ["--allow-user", "u"], ["--proof-header", "X-P"], ["--proof-file", h.proof], ["--send", null]]) {
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), new RegExp(`the factory profile refuses ${flag}`));
}
for (const [flag, value, pattern] of [["--checkout", "/srv/x", /refusing --checkout/], ["--board", "http://127.0.0.1:7331", /refusing --board/], ["--sso", null, /refusing --sso/], ["--nonsense", "1", /unknown serve flag/]]) {
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), pattern);
}
});
test("row253 factory refuses retired TCP and a value after the socket marker", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);
refuses(unit(h, { profile: 'factory', flags: [...factoryFlags(), '7333'] }), /unknown serve flag: 7333/);
});
test("factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", extra: ["--claude", "bin/claude"] }), /--claude must be an absolute path/);
const mine = script(join(h.root, "opt", "claude"), "exit 0");
refuses(unit(h, { profile: "factory", extra: ["--claude", mine] }), /--claude: .* is not owned by root/);
const inside = script(join(h.home, "bin", "claude"), "exit 0");
refuses(unit(h, { profile: "factory", extra: ["--claude", inside] }), /--claude must be outside/);
assert.equal(unit(h, { profile: "factory", extra: ["--pm", "--claude", TRUE_BIN] }).code, 0);
refuses(unit(h, { profile: "factory", extra: ["--claude", TRUE_BIN] }), /--pm and --claude must equal the installed ones|ExecStart= must be exactly once/);
refuses(unit(h, { profile: "factory", extra: ["--pm"] }), /Environment= lines must be exactly|differs from the render/);
refuses(unit(h, { profile: "factory", extra: ["--pm", "--claude", USR_NODE] }), /Environment= lines must be exactly|differs from the render/);
});
// ---- case2-precheck.sh ----
// A scratch user for the precheck: a config, a business file, an installed Console unit,
// fakes on PATH and a fake /proc/sys.
function host(t, { roles = {}, vars = {}, keyFile = `ZAI_API_KEY=${FIXTURE_KEY}\n`, port = "443", busClaude = null } = {}) {
const root = tmp(t, "case2-pre-");
const home = join(root, "home");
const configDir = join(home, ".config", "mosaic-dev");
const config = writeJson(join(configDir, "config.json"), {
configVersion: 1,
environment: "production",
dataRoot: join(home, ".mosaic-dev"),
execution: { backend: "docker", provider: "zai", model: "glm-5.3-flash", adapter: "pi" },
});
const doc = businessDoc(root);
doc.roles.cto.vars = { harness: "pi" };
for (const [name, v] of Object.entries(roles)) doc.roles[name].vars = v;
Object.assign(doc.vars, vars);
writeJson(join(configDir, "businesses", "acme.json"), doc);
const secrets = join(configDir, "secrets");
mkdirSync(secrets, { recursive: true, mode: 0o700 });
if (keyFile !== null) {
writeFileSync(join(secrets, "claude-session.env"), keyFile);
chmodSync(join(secrets, "claude-session.env"), 0o600);
}
const units = join(root, "units");
mkdirSync(units);
writeFileSync(join(units, "mosaic-webui.service"), `[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --port ${port} --business acme\n`);
writeFileSync(join(units, "[email protected]"), `[Service]\nEnvironment=PATH=/usr/bin\n${busClaude ? `Environment=MOSAIC_CLAUDE_BIN=${busClaude}\n` : ""}`);
const sys = join(root, "sys");
mkdirSync(join(sys, "net", "ipv4"), { recursive: true });
mkdirSync(join(sys, "kernel"), { recursive: true });
writeFileSync(join(sys, "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
writeFileSync(join(sys, "kernel", "apparmor_restrict_unprivileged_userns"), "1\n");
const bin = join(root, "bin");
const me = spawnSync("id", ["-un"], { encoding: "utf8" }).stdout.trim();
script(join(bin, "getent"), `[ "$1" = passwd ] && [ "$2" = '${me}' ] && echo '${me}:x:${UID}:${GID}::${home}:/bin/bash'`);
script(join(bin, "sudo"), "exit 1");
script(join(bin, "id"), `if [ "$1" = -G ]; then echo "\${CASE2_TEST_GROUPS:-${GID}}"; else exec /usr/bin/id "$@"; fi`);
script(join(bin, "node"), `if [ "$1" = --version ]; then echo "\${CASE2_TEST_NODE:-v24.3.0}"; exit 0; fi\nexec '${process.execPath}' "$@"`);
script(join(bin, "bwrap"), `exit "\${CASE2_TEST_BWRAP:-0}"`);
return { root, home, configDir, config, secrets, units, sys, bin, dataRoot: join(home, ".mosaic-dev") };
}
// A fake claude: replace answers with the marker and a small prompt; append answers with a large one;
// nomarker answers with a small prompt and no marker; text answers with plain text, not JSON.
function fakeClaude(dir, kind) {
return script(join(dir, "claude"), `[ "$1" = --version ] && { echo "9.9.9 (Claude Code, ${kind} fake)"; exit 0; }
file=''
while [ $# -gt 0 ]; do [ "$1" = --system-prompt-file ] && file="$2"; shift; done
marker="$(awk '{ print $NF }' "$file")"
${kind === "nomarker" ? "marker=something-else" : ""}
${kind === "text" ? 'echo "$marker"; exit 0' : ""}
tokens=${kind === "append" ? 9000 : 40}
printf '{"result":"%s","usage":{"input_tokens":%s,"cache_read_input_tokens":0}}\\n' "$marker" "$tokens"`);
}
// This host may have a claude in /usr/bin, so this PATH links every /usr/bin entry but that one.
function pathWithoutClaude(x) {
const usr = join(x.root, "usr-bin");
mkdirSync(usr);
for (const name of readdirSync("/usr/bin")) if (name !== "claude") symlinkSync(join("/usr/bin", name), join(usr, name));
return `${x.bin}:${usr}`;
}
function precheck(x, { profile = "library", env = {}, path } = {}) {
const r = spawnSync(PRECHECK, ["--business", "acme", "--profile", profile], {
encoding: "utf8",
timeout: 60000,
env: {
PATH: path ?? `${x.bin}:/usr/bin:/bin`,
HOME: x.home,
MOSAIC_CONFIG: x.config,
CASE2_UNIT_DIR: x.units,
CASE2_PROC_SYS: x.sys,
CASE2_GETENT: join(x.bin, "getent"),
...env,
},
});
const out = r.stdout + r.stderr;
assert.doesNotMatch(out, new RegExp(FIXTURE_KEY), "the precheck printed the fixture key");
return { code: r.status, out };
}
function blocked(r, pattern) {
assert.equal(r.code, 1, r.out);
assert.match(r.out, pattern);
}
test("the precheck passes on a good host and prints free memory, without the key", (t) => {
const x = host(t);
const r = precheck(x);
assert.equal(r.code, 0, r.out);
assert.match(r.out, /ok {6}config validates/);
assert.match(r.out, /ok {6}bus\.tester is off/);
assert.match(r.out, /ok {6}net\.ipv4\.ip_unprivileged_port_start 1024 is above the Console's port 443/);
assert.match(r.out, /claude-session\.env sets ZAI_API_KEY/);
assert.match(r.out, /free memory: \d+ MiB/);
assert.doesNotMatch(r.out, /claude .*fake|marker/);
});
test("the precheck refuses each failed host check", (t) => {
blocked(precheck(host(t), { env: { CASE2_TEST_NODE: "v22.22.2" } }), /REFUSED node v22\.22\.2 is older than 24/);
blocked(precheck(host(t), { env: { CASE2_TEST_GROUPS: `${GID} 998` } }), /REFUSED id -G prints more than one group/);
const sudo = host(t);
script(join(sudo.bin, "sudo"), "exit 0");
blocked(precheck(sudo), /REFUSED sudo -n true succeeds/);
blocked(precheck(host(t), { env: { XDG_DATA_HOME: "/elsewhere" } }), /REFUSED XDG_DATA_HOME is \/elsewhere/);
const home = host(t);
assert.equal(precheck(home, { env: { XDG_DATA_HOME: `${home.home}/.local/share` } }).code, 0);
blocked(precheck(host(t), { env: { HOME: "/tmp" } }), /REFUSED HOME is \/tmp/);
const runs = host(t);
mkdirSync(join(runs.dataRoot, "launches", "acme"), { recursive: true, mode: 0o755 });
chmodSync(join(runs.dataRoot, "launches", "acme"), 0o755);
blocked(precheck(runs), /REFUSED run root .* is not a 0700 directory \(mode 755\)/);
chmodSync(join(runs.dataRoot, "launches", "acme"), 0o700);
assert.match(precheck(runs).out, /ok {6}run root .* is 0700/);
const bad = host(t);
writeFileSync(bad.config, "{ not json");
blocked(precheck(bad), /REFUSED config:/);
});
test("the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root", (t) => {
const nouser = host(t);
script(join(nouser.bin, "getent"), "exit 2");
blocked(precheck(nouser), /REFUSED user: no .* in the passwd database/);
const unparsed = host(t);
writeFileSync(join(unparsed.configDir, "businesses", "acme.json"), "{ not json");
blocked(precheck(unparsed), /REFUSED business file: cannot read .*acme\.json/);
const gone = host(t);
rmSync(join(gone.configDir, "businesses", "acme.json"));
blocked(precheck(gone), /REFUSED business file: cannot read .*acme\.json: ENOENT/);
const invalid = host(t);
const file = join(invalid.configDir, "businesses", "acme.json");
const doc = JSON.parse(read(file));
doc.notAField = true;
writeJson(file, doc);
const r = precheck(invalid);
blocked(r, /REFUSED business file: /);
assert.doesNotMatch(r.out, /REFUSED business file: cannot read/);
// stat follows a link, so a link to a 0700 directory would read as 0700.
const link = host(t);
const target = join(link.root, "elsewhere");
mkdirSync(target, { mode: 0o700 });
chmodSync(target, 0o700);
mkdirSync(join(link.dataRoot, "launches"), { recursive: true });
symlinkSync(target, join(link.dataRoot, "launches", "acme"));
blocked(precheck(link), /REFUSED run root .* is a symlink/);
});
test("a bwrap that fails refuses and prints the sysctl and where bwrap is", (t) => {
const x = host(t);
blocked(precheck(x, { env: { CASE2_TEST_BWRAP: "1" } }), new RegExp(`REFUSED bwrap .* failed; bwrap: ${join(x.bin, "bwrap")}; kernel\\.apparmor_restrict_unprivileged_userns: 1`));
});
test("an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit", (t) => {
for (const floor of ["443", "80"]) {
const x = host(t);
writeFileSync(join(x.sys, "net", "ipv4", "ip_unprivileged_port_start"), `${floor}\n`);
blocked(precheck(x), new RegExp(`REFUSED net\\.ipv4\\.ip_unprivileged_port_start is ${floor}, at or below the Console's port 443`));
}
const x = host(t);
rmSync(join(x.units, "mosaic-webui.service"));
blocked(precheck(x), /REFUSED no installed Console unit/);
const noPort = host(t);
writeFileSync(join(noPort.units, "mosaic-webui.service"), "[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --business acme\n");
blocked(precheck(noPort), /REFUSED .*mosaic-webui\.service has no --port in its ExecStart/);
const noFloor = host(t);
rmSync(join(noFloor.sys, "net", "ipv4", "ip_unprivileged_port_start"));
blocked(precheck(noFloor), /REFUSED cannot read net\.ipv4\.ip_unprivileged_port_start/);
});
test("bus.tester: library refuses it on, factory needs it on, and factory refuses a library business", (t) => {
blocked(precheck(host(t, { vars: { "bus.tester": true } })), /REFUSED bus\.tester is on; the library profile needs it off/);
blocked(precheck(host(t), { profile: "factory" }), /REFUSED bus\.tester is off; the factory profile needs it on/);
const on = precheck(host(t, { vars: { "bus.tester": true } }), { profile: "factory" });
assert.equal(on.code, 0, on.out);
const lib = host(t, { vars: { "bus.tester": true } });
const file = join(lib.configDir, "businesses", "acme.json");
const doc = JSON.parse(read(file));
doc.projects.stack.kind = "library";
writeJson(file, doc);
blocked(precheck(lib, { profile: "factory" }), /REFUSED profile-library-business: .* projects stack are library projects/);
});
test("the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong", (t) => {
// No claude-code instance and no claude anywhere on PATH: the precheck passes.
const none = host(t);
const r = precheck(none, { path: pathWithoutClaude(none) });
assert.equal(r.code, 0, r.out);
assert.doesNotMatch(r.out, /--system-prompt-file|claude is installed/);
const good = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(good.bin, "replace");
const pass = precheck(good);
assert.equal(pass.code, 0, pass.out);
assert.match(pass.out, /claude .*9\.9\.9 \(Claude Code, replace fake\)/);
assert.match(pass.out, /ok {6}claude replaces its prompt with --system-prompt-file \(40 input tokens\)/);
const bad = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(bad.bin, "append");
blocked(precheck(bad), /REFUSED claude adds to its own prompt with --system-prompt-file: 9000 input tokens/);
const nomarker = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(nomarker.bin, "nomarker");
blocked(precheck(nomarker), /REFUSED claude's reply to the marker prompt lacks the marker \(40 input tokens\)/);
const text = host(t, { roles: { cto: { harness: "claude-code" } } });
fakeClaude(text.bin, "text");
blocked(precheck(text), /REFUSED claude gave no JSON reply to the marker prompt/);
const missing = host(t, { roles: { cto: { harness: "claude-code" } } });
blocked(precheck(missing, { path: pathWithoutClaude(missing) }), /REFUSED an instance runs claude-code and no claude is installed/);
});
test("the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH", (t) => {
const pinned = join(tmp(t, "case2-claude-"), "pinned");
const replace = fakeClaude(pinned, "replace");
const x = host(t, { roles: { cto: { harness: "claude-code" } }, busClaude: replace });
fakeClaude(x.bin, "append");
const r = precheck(x);
assert.equal(r.code, 0, r.out);
assert.match(r.out, /replace fake/);
const pinnedBad = join(tmp(t, "case2-claude-"), "pinned");
const append = fakeClaude(pinnedBad, "append");
const y = host(t, { roles: { cto: { harness: "claude-code" } }, busClaude: append });
fakeClaude(y.bin, "replace");
blocked(precheck(y), /REFUSED claude adds to its own prompt/);
});
test("the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line", (t) => {
blocked(precheck(host(t, { keyFile: null })), /REFUSED an instance's provider is zai and .*claude-session\.env is missing/);
const open = host(t);
chmodSync(join(open.secrets, "claude-session.env"), 0o644);
blocked(precheck(open), /REFUSED .*claude-session\.env must be owned by .* at 0600 \(mode 644\)/);
const link = host(t, { keyFile: null });
const real = join(link.root, "real.env");
writeFileSync(real, `ZAI_API_KEY=${FIXTURE_KEY}\n`);
chmodSync(real, 0o600);
symlinkSync(real, join(link.secrets, "claude-session.env"));
blocked(precheck(link), /REFUSED .*claude-session\.env is a symlink/);
blocked(precheck(host(t, { keyFile: "ZAI_API_KEY=\n" })), /REFUSED .*sets no nonempty ZAI_API_KEY/);
blocked(precheck(host(t, { keyFile: `ZAI_API_KEY=${FIXTURE_KEY}\nZAI_API_KEY=\n` })), /REFUSED .*sets no nonempty ZAI_API_KEY/);
const quoted = precheck(host(t, { keyFile: `ZAI_API_KEY=\nZAI_API_KEY="${FIXTURE_KEY}"\n` }));
assert.equal(quoted.code, 0, quoted.out);
});
// ---- the bind test (mos-1100) ----
// A session can't take the Console's port: inside the sandbox a real session gets, a bind below
// 1024 fails with EACCES, and the same bind on a free port of 1024 or above succeeds.
const sandboxSkip = sandboxProblem() ?? false;
const LOW_PORT = 980;
test("inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds", { skip: sandboxSkip, timeout: 30000 }, async (t) => {
const floor = Number(read("/proc/sys/net/ipv4/ip_unprivileged_port_start").trim());
assert.ok(floor > LOW_PORT, `this host's net.ipv4.ip_unprivileged_port_start is ${floor}, at or below the test port ${LOW_PORT}, so the test can't show the sandbox refuses it`);
const root = tmp(t, "c2b-");
const runDir = join(root, "run");
const workspace = join(root, "ws");
mkdirSync(runDir);
mkdirSync(workspace);
mkdirSync(join(runDir, "bundle"));
writeFileSync(join(runDir, "session.json"), "{}");
const adapter = join(root, "adapter.sh");
const brokerSocket = join(root, "broker.sock");
writeFileSync(adapter, "");
writeFileSync(brokerSocket, "");
prepareRun(runDir);
const list = binds({ runDir, workspace, adapter, harnessMounts: [], brokerSocket });
const high = await new Promise((done) => {
const s = createServer().listen(0, "127.0.0.1", () => { const { port } = s.address(); s.close(() => done(port)); });
});
const bindIn = (port) => {
const code = `require("net").createServer().on("error", (e) => { console.log(e.code); process.exit(0); }).listen(${port}, "127.0.0.1", function () { console.log("bound"); this.close(); });`;
const argv = sandboxArgv(list, runDir, [process.execPath, "-e", code], { business: "acme", role: "coder" });
const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf8", timeout: 10000 });
assert.equal(r.status, 0, r.stderr);
return r.stdout.trim();
};
assert.equal(bindIn(LOW_PORT), "EACCES");
assert.equal(bindIn(high), "bound");
});
@@ -0,0 +1,133 @@
# row253-supervisor-begin
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
stat_fields() {
local pid="$1" raw="" rest
# EOF is expected: stat has no NUL. Decide on content, not read's status.
IFS= read -r -d '' raw < "/proc/$pid/stat" 2>/dev/null || :
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
[[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
}
clock_cs() {
local boot spare
read -r boot spare < /proc/uptime || return 1
[[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
}
clean_session() {
local entry pid rc live until=""
while :; do
live=0
for entry in /proc/[0-9]*/stat; do
pid="${entry%/stat}"; pid="${pid##*/}"
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" != 3 ] || continue
[ "$rc" = 0 ] || return 1
[ "$ST_SESSION" = "$child" ] || continue
case "$ST_STATE" in Z|X) continue ;; esac
live=1
if ! kill -KILL "$pid" 2>/dev/null; then
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
fi
done
[ "$live" != 0 ] || return 0
clock_cs || return 1
# Inventory pass 1 is outside the bounded confirmation/retry budget.
[ -n "$until" ] || until=$((CLOCK_CS + 25))
[ "$CLOCK_CS" -lt "$until" ] || return 1
"$sleep_bin" 0.01
done
}
finish() {
local rc="$?"
trap - EXIT HUP INT TERM
if [ "$verified" = 1 ]; then
clean_session || rc=86
elif [ -n "$child" ]; then
kill -KILL "$child" 2>/dev/null || :
fi
[ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
"$setsid_bin" "$@" & child=$!
# Allow session creation the whole inner budget, rather than a 200 ms margin.
while :; do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 85
if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
tester_tools() {
local input target p st magic
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
p="$target"
while :; do
st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
[ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
[ "$p" != / ] || break
p="${p%/*}"; [ -n "$p" ] || p=/
done
magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
case "$input" in
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
/usr/bin/head) TS_HEAD="$target" ;;
esac
done
}
tester_capture_reason() {
case "$1" in
81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
*) echo inner-command ;;
esac
}
tester_capture() (
local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
tester_tools || exit 84
file="$(mktemp)" || exit 88
notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
trap 'exit 88' HUP INT TERM
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
set -o pipefail
maximum="$1"; head_bin="$2"; shift 2
"$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
# Only GNU timeout and the trusted head can write this root-side channel.
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
[ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
[ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
cat -- "$file"
)
+322
View File
@@ -0,0 +1,322 @@
[
{
"id": "M1",
"file": "scripts/case2-unit.sh",
"old": " --tester-port) die \"case2-unit.sh retires tester TCP; use --tester-socket\" ;;",
"new": "",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 factory refuses retired TCP"
},
{
"id": "M2",
"file": "scripts/case2-unit.sh",
"old": "[ \"$PROFILE\" = factory ] || die \"the library profile refuses --tester-socket\"",
"new": ":",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "library refuses a privileged-floor miss"
},
{
"id": "M3",
"file": "scripts/case2-unit.sh",
"old": " [ -z \"${seen[$name]:-}\" ] || die \"serve flag $name given twice\"",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "factory refuses duplicate socket"
},
{
"id": "M4",
"file": "scripts/case2-unit.sh",
"old": " [ \"$rc\" = 0 ] || { case \"$rc\" in 81|85|86|87|88) exit \"$rc\" ;; *) exit 87 ;; esac; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M5",
"file": "scripts/case2-unit.sh",
"old": " [ \"$TS_MARKED\" = true ] || die \"the target does not support --tester-socket\"",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M6",
"file": "scripts/case2-unit.sh",
"old": "case \"$data\" in \"$home\"/*)",
"new": "case \"$data\" in \"$home\"*)",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M7",
"file": "scripts/case2-unit.sh",
"old": "case \"$home/\" in /home/*",
"new": "case /allowed/ in /home/*",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 protected homes"
},
{
"id": "M8",
"file": "scripts/case2-unit.sh",
"old": " [ \"${#TS_SOCKET}\" -le 107 ] || { TS_REASON=too-long; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M9",
"file": "scripts/case2-unit.sh",
"old": " [ -d \"$parent\" ] || { TS_REASON=parent-type; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M10",
"file": "scripts/case2-unit.sh",
"old": " [ \"${st% *}\" = \"$uid\" ] || { TS_REASON=parent-owner; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M11",
"file": "scripts/case2-unit.sh",
"old": " [[ \"${st#* }\" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M12",
"file": "scripts/case1-accept.sh",
"old": " --tester-port) refuse \"case1-accept.sh retires tester TCP; use --tester-socket\" ;;",
"new": "",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "a bad args file refuses"
},
{
"id": "M13",
"file": "scripts/case1-accept.sh",
"old": "[ \"$TESTER_SOCKET\" = 1 ] || refuse \"$ARGS_FILE needs one bare --tester-socket after --\"",
"new": ":",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "a bad args file refuses"
},
{
"id": "M14",
"file": "scripts/case1-accept.sh",
"old": " *) refuse \"unknown serve flag $a\" ;;",
"new": " *) : ;;",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "a bad args file refuses"
},
{
"id": "M15",
"file": "scripts/case1-accept.sh",
"old": " [ \"$rc\" = 0 ] || { case \"$rc\" in 81|85|86|87|88) exit \"$rc\" ;; *) exit 87 ;; esac; }",
"new": " :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 acceptance rejects bad config"
},
{
"id": "M16",
"file": "scripts/case1-accept.sh",
"old": "other=\"$(listeners 7331)$(listeners 7333)\"",
"new": "other=\"$(listeners 7331)\"",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P8 refuses"
},
{
"id": "M17",
"file": "scripts/case1-accept.sh",
"old": " [[ \"$p\" =~ ^$MOSAIC_UID\\ 700\\ directory\\ [0-9]+\\ [0-9]+$ ]] || return 1",
"new": " :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M18",
"file": "scripts/case1-accept.sh",
"old": " [[ \"$s\" =~ ^$MOSAIC_UID\\ 600\\ socket\\ [0-9]+\\ [0-9]+$ ]] || return 1",
"new": " :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M19",
"file": "scripts/case1-accept.sh",
"old": "[ \"$pid\" = \"$CON_PID\" ] && [ \"$uid\" = \"$MOSAIC_UID\" ]",
"new": "true && [ \"$uid\" = \"$MOSAIC_UID\" ]",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M20",
"file": "scripts/case1-accept.sh",
"old": "[ \"$pid\" = \"$CON_PID\" ] && [ \"$uid\" = \"$MOSAIC_UID\" ]",
"new": "[ \"$pid\" = \"$CON_PID\" ] && true",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M21",
"file": "scripts/case1-accept.sh",
"old": "pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})$'",
"new": "pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})'",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M22",
"file": "scripts/case1-accept.sh",
"old": " magic=\"$(od -An -tx1 -N4 -- \"$ROOT$real\" 2>/dev/null | tr -d ' \\n')\"\n [ \"$magic\" = 7f454c46 ] || return 1",
"new": " magic=\"$(od -An -tx1 -N4 -- \"$ROOT$real\" 2>/dev/null | tr -d ' \\n')\"\n :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M23",
"file": "scripts/case1-accept.sh",
"old": " [ \"$before\" = \"$after\" ] || why=changed",
"new": " :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M24",
"file": "scripts/case2-unit.sh",
"old": "sudo -u \"$user\" -H env -i",
"new": "sudo -H env -i",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 factory socket uses actual config"
},
{
"id": "M25",
"file": "scripts/case1-accept.sh",
"old": "tester_capture 2 4 128 \"$S_USER\" \"$S_HOME\"",
"new": "tester_capture 2 4 128 wrong-user \"$S_HOME\"",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 socket observation uses configured root"
},
{
"id": "M26",
"file": "scripts/case1-accept.sh",
"old": " [[ \"$floor\" =~ ^[0-9]{1,5}$ ]] && [ \"$floor\" -gt \"$PORT\" ] || why=floor",
"new": " :",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 P10 refuses unsafe metadata"
},
{
"id": "M27",
"file": "scripts/case1-accept.sh",
"old": "p=struct.unpack(\"3i\",s.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))",
"new": "import os; p=(os.getpid(),os.getuid(),os.getgid())",
"killer": "packages/cli/tests/case1-accept.test.mjs",
"pattern": "row253 socket observation uses configured root"
},
{
"id": "M28",
"file": "scripts/case2-unit.sh",
"old": " [ ! -L \"$parent\" ] || { TS_REASON=symlink-parent; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M29",
"file": "scripts/case2-unit.sh",
"old": " [ ! -L \"$prefix$at\" ] || { TS_REASON=symlink-component; return 1; }",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M30",
"file": "scripts/case2-unit.sh",
"old": " case \"$data/\" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac",
"new": " :",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 installer refuses unsafe location and query facts"
},
{
"id": "M31",
"file": "scripts/case2-unit.sh",
"old": " [[ \"$1\" =~ $pattern ]] || return 1",
"new": " [[ \"$1\" =~ \\\"dataRoot\\\":\\\"([^\\\"]+)\\\",\\\"socket\\\":(true|false) ]] || return 1",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 config parser is strict"
},
{
"id": "M32",
"file": "scripts/case2-unit.sh",
"old": "[ \"$(wc -c < \"$file\")\" -le \"$maximum\" ]",
"new": "true",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 overflow is output-limit"
},
{
"id": "M33",
"file": "scripts/case2-unit.sh",
"old": "\"$TS_TIMEOUT\" --verbose --signal=KILL \"${outer}s\"",
"new": "\"$TS_TIMEOUT\" --foreground --verbose --signal=KILL \"${outer}s\"",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 query reaps its group"
},
{
"id": "M34",
"file": "scripts/case2-unit.sh",
"old": "seconds=\"$1\" setsid_bin=\"$2\" sleep_bin=\"$3\"; shift 3",
"new": "seconds=\"$1\" setsid_bin=\"$2\" sleep_bin=\"$3\"; shift 3\nexec \"$@\"",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 signals query"
},
{
"id": "M35",
"file": "scripts/case2-unit.sh",
"old": "LC_ALL=C \"$TS_TIMEOUT\" --verbose --signal=KILL \"${outer}s\" \"$TS_BASH\" -c",
"new": "\"$TS_BASH\" -c",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 stalled supervisor"
},
{
"id": "M36",
"file": "scripts/case2-unit.sh",
"old": " [ \"$ST_SESSION\" = \"$child\" ] || continue",
"new": " [ \"$ST_SESSION\" = \"$child\" ] || continue\n [ \"$ST_GROUP\" = \"$child\" ] || continue",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 member query"
},
{
"id": "M37",
"file": "scripts/case2-unit.sh",
"old": " LC_ALL=C \"$TS_TIMEOUT\" --verbose --signal=KILL \"${outer}s\" \"$TS_BASH\" -c '\n set -o pipefail\n maximum=\"$1\"; head_bin=\"$2\"; shift 2\n \"$@\" 2>/dev/null | \"$head_bin\" -c \"$((maximum + 1))\"\n ' -- \"$maximum\" \"$TS_HEAD\" sudo -u \"$user\" -H env -i \"HOME=$home\" \"PATH=$path\" \"$TS_BASH\" -c \"$TESTER_SUPERVISOR\" -- \"$inner\" \"$TS_SETSID\" \"$TS_SLEEP\" \"$@\" > \"$file\" 2> \"$notice\" &\n child=$!\n wait \"$child\" 2>/dev/null || rc=$?\n kill -KILL -- \"-$child\" 2>/dev/null || :\n # Only GNU timeout and the trusted head can write this root-side channel.\n if [ \"$rc\" = 137 ] && [[ \"$(<\"$notice\")\" == *': sending signal KILL to command '* ]]; then exit 82; fi\n [ \"$(wc -c < \"$file\")\" -le \"$maximum\" ] || exit 83\n [ \"$rc\" = 0 ] || { case \"$rc\" in 81|85|86|87|88) exit \"$rc\" ;; *) exit 87 ;; esac; }",
"new": " LC_ALL=C \"$TS_TIMEOUT\" --verbose --signal=KILL \"${outer}s\" \"$TS_BASH\" -c '\n set -o pipefail\n maximum=\"$1\"; head_bin=\"$2\"; shift 2\n \"$@\" 2>/dev/null | cat\n ' -- \"$maximum\" \"$TS_HEAD\" sudo -u \"$user\" -H env -i \"HOME=$home\" \"PATH=$path\" \"$TS_BASH\" -c \"$TESTER_SUPERVISOR\" -- \"$inner\" \"$TS_SETSID\" \"$TS_SLEEP\" \"$@\" > \"$file\" 2> \"$notice\" &\n child=$!\n wait \"$child\" 2>/dev/null || rc=$?\n kill -KILL -- \"-$child\" 2>/dev/null || :\n # Only GNU timeout and the trusted head can write this root-side channel.\n if [ \"$rc\" = 137 ] && [[ \"$(<\"$notice\")\" == *': sending signal KILL to command '* ]]; then exit 82; fi\n :\n [ \"$rc\" = 0 ] || { case \"$rc\" in 81|85|86|87|88) exit \"$rc\" ;; *) exit 87 ;; esac; }",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 overflow is output-limit"
},
{
"id": "M38",
"file": "scripts/case2-unit.sh",
"old": "\"$setsid_bin\" \"$@\" & child=$!",
"new": "\"$@\" & child=$!",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 signals query"
},
{
"id": "M39",
"file": "scripts/case2-unit.sh",
"old": "MOSAIC_TESTER_SUPERVISOR' || :\nset +m",
"new": "MOSAIC_TESTER_SUPERVISOR' || :\n# one-copy drift\nset +m",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 inline supervisor is byte-identical"
},
{
"id": "M40",
"file": "scripts/case2-unit.sh",
"old": "clean_session() {\n local entry pid rc live until=\"\"\n while :; do\n live=0\n for entry in /proc/[0-9]*/stat; do\n pid=\"${entry%/stat}\"; pid=\"${pid##*/}\"\n rc=0; stat_fields \"$pid\" || rc=$?\n [ \"$rc\" != 3 ] || continue\n [ \"$rc\" = 0 ] || return 1\n [ \"$ST_SESSION\" = \"$child\" ] || continue\n case \"$ST_STATE\" in Z|X) continue ;; esac\n live=1\n if ! kill -KILL \"$pid\" 2>/dev/null; then\n rc=0; stat_fields \"$pid\" || rc=$?\n [ \"$rc\" = 3 ] || { [ \"$rc\" = 0 ] && { [ \"$ST_STATE\" = Z ] || [ \"$ST_STATE\" = X ]; }; } || return 1\n fi\n done\n [ \"$live\" != 0 ] || return 0\n clock_cs || return 1\n # Inventory pass 1 is outside the bounded confirmation/retry budget.\n [ -n \"$until\" ] || until=$((CLOCK_CS + 25))\n [ \"$CLOCK_CS\" -lt \"$until\" ] || return 1\n \"$sleep_bin\" 0.01\n done\n}\n",
"new": "clean_session() {\n local entry pid rc live until\n clock_cs || return 1\n until=$((CLOCK_CS + 25))\n while :; do\n live=0\n for entry in /proc/[0-9]*/stat; do\n pid=\"${entry%/stat}\"; pid=\"${pid##*/}\"\n rc=0; stat_fields \"$pid\" || rc=$?\n [ \"$rc\" != 3 ] || continue\n [ \"$rc\" = 0 ] || return 1\n [ \"$ST_SESSION\" = \"$child\" ] || continue\n case \"$ST_STATE\" in Z|X) continue ;; esac\n live=1\n if ! kill -KILL \"$pid\" 2>/dev/null; then\n rc=0; stat_fields \"$pid\" || rc=$?\n [ \"$rc\" = 3 ] || { [ \"$rc\" = 0 ] && { [ \"$ST_STATE\" = Z ] || [ \"$ST_STATE\" = X ]; }; } || return 1\n fi\n done\n [ \"$live\" != 0 ] || return 0\n clock_cs || return 1\n # Inventory pass 1 is outside the bounded confirmation/retry budget.\n :\n [ \"$CLOCK_CS\" -lt \"$until\" ] || return 1\n \"$sleep_bin\" 0.01\n done\n}\n",
"killer": "packages/cli/tests/case2-unit.test.mjs",
"pattern": "row253 C4 cleanup budget starts after inventory pass one"
}
]
@@ -0,0 +1,168 @@
{
"count": 17,
"matches": 15,
"mismatches": [
{
"fault": "peer-malformed",
"expected": "probe",
"observed": "probe-inner-session",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=probe-inner-session path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
]
},
{
"fault": "peer-error",
"expected": "probe-inner-command",
"observed": "probe-inner-session",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=probe-inner-session path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
]
}
],
"peer_malformed": {
"fault": "peer-malformed",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=probe-inner-session path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "probe-inner-session",
"status": 0
},
"rows": [
{
"fault": "parent-mode",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "metadata",
"status": 0
},
{
"fault": "parent-owner",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "metadata",
"status": 0
},
{
"fault": "socket-mode",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "metadata",
"status": 0
},
{
"fault": "socket-owner",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "metadata",
"status": 0
},
{
"fault": "socket-type",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "metadata",
"status": 0
},
{
"fault": "symlink",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "metadata",
"status": 0
},
{
"fault": "peer",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=peer path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "peer",
"status": 0
},
{
"fault": "peer-pid",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=peer path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "peer",
"status": 0
},
{
"fault": "python-mode",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=interpreter path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "interpreter",
"status": 0
},
{
"fault": "python-missing",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=interpreter path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "interpreter",
"status": 0
},
{
"fault": "python-not-elf",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=interpreter path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "interpreter",
"status": 0
},
{
"fault": "peer-malformed",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=probe-inner-session path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "probe-inner-session",
"status": 0
},
{
"fault": "peer-error",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=probe-inner-session path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "probe-inner-session",
"status": 0
},
{
"fault": "peer-uid-fields",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "metadata",
"status": 1
},
{
"fault": "changed-inode",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=changed path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "changed",
"status": 0
},
{
"fault": "changed-pid",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=changed path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "changed",
"status": 0
},
{
"fault": "floor",
"p10": [
"P10 tester-socket-squat unnamed boundary=failed reason=floor path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"
],
"reason": "floor",
"status": 0
}
]
}
@@ -0,0 +1 @@
/mnt/storage/scratch/tmp/gBHA
@@ -0,0 +1,121 @@
{
"parent-mode": {
"reason": "metadata",
"meaning": "Private parent mode is exact0700",
"semantic_source": "DESIGN.md:119; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:659",
"exact_reason_token": "reason=metadata "
},
"parent-owner": {
"reason": "metadata",
"meaning": "Parent owner is stack uid",
"semantic_source": "DESIGN.md:119; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:659",
"exact_reason_token": "reason=metadata "
},
"socket-mode": {
"reason": "metadata",
"meaning": "Socket mode is exact0600",
"semantic_source": "DESIGN.md:120; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:659",
"exact_reason_token": "reason=metadata "
},
"socket-owner": {
"reason": "metadata",
"meaning": "Socket owner is stack uid",
"semantic_source": "DESIGN.md:120; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:659",
"exact_reason_token": "reason=metadata "
},
"socket-type": {
"reason": "metadata",
"meaning": "Final entry is socket",
"semantic_source": "DESIGN.md:119; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:659",
"exact_reason_token": "reason=metadata "
},
"symlink": {
"reason": "metadata",
"meaning": "Final entry is not symlink",
"semantic_source": "DESIGN.md:119; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:659",
"exact_reason_token": "reason=metadata "
},
"peer": {
"reason": "peer",
"meaning": "Connected uid equals stack uid",
"semantic_source": "DESIGN.md:136; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:665",
"exact_reason_token": "reason=peer "
},
"peer-pid": {
"reason": "peer",
"meaning": "Connected pid equals MainPID",
"semantic_source": "DESIGN.md:136; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:665",
"exact_reason_token": "reason=peer "
},
"python-mode": {
"reason": "interpreter",
"meaning": "Fixed system interpreter safely owned and protected",
"semantic_source": "DESIGN.md:126; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:660",
"exact_reason_token": "reason=interpreter "
},
"python-missing": {
"reason": "interpreter",
"meaning": "Missing system Python is failed observation",
"semantic_source": "DESIGN.md:129; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:660",
"exact_reason_token": "reason=interpreter "
},
"python-not-elf": {
"reason": "interpreter",
"meaning": "System entry target is ELF",
"semantic_source": "DESIGN.md:126; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:660",
"exact_reason_token": "reason=interpreter "
},
"peer-malformed": {
"reason": "probe",
"meaning": "Probe returns only exactly pid uid gid",
"semantic_source": "DESIGN.md:135; DESIGN.md:163; BUILD.md:17; mos-1293 item2 explicitly reason=probe",
"code_label_location": "scripts/case1-accept.sh:666",
"exact_reason_token": "reason=probe "
},
"peer-error": {
"reason": "probe-inner-command",
"meaning": "Nonzero child output discarded, distinct child failure",
"semantic_source": "DESIGN-C4.md:84-85; BUILD.md:175; BUILD.md:177",
"code_label_location": "scripts/case1-accept.sh:662; tester_capture_reason",
"exact_reason_token": "reason=probe-inner-command "
},
"peer-uid-fields": {
"reason": "metadata",
"meaning": "MainPID four proc uid fields equal stack uid",
"semantic_source": "DESIGN.md:118; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:659",
"exact_reason_token": "reason=metadata "
},
"changed-inode": {
"reason": "changed",
"meaning": "Snapshot replacement fails observation",
"semantic_source": "DESIGN.md:120; DESIGN.md:121; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:668",
"exact_reason_token": "reason=changed "
},
"changed-pid": {
"reason": "changed",
"meaning": "MainPID change fails observation",
"semantic_source": "DESIGN.md:121; BUILD.md:17",
"code_label_location": "scripts/case1-accept.sh:668",
"exact_reason_token": "reason=changed "
},
"floor": {
"reason": "floor",
"meaning": "Numeric floor above privileged Console port",
"semantic_source": "DESIGN.md:122; DESIGN.md:123",
"code_label_location": "scripts/case1-accept.sh:658",
"exact_reason_token": "reason=floor "
}
}
@@ -0,0 +1,37 @@
# Draft(b): expected P10 reasons before diagnostic(a)
Not derived from diagnostic output. No test edit is prepared or committed. The brief delegates P10 checks/outcome wording to DESIGN (docs/plans/2026-10-21_tester-socket-scripts.md:31). DESIGN.md:163-170 requires the failed reason to identify the failing observation, rather than accepting any unnamed line. BUILD.md:17 records the separate parent/socket/process/snapshot and peer checks. C4 refined failure classes in DESIGN-C4.md:84 and:91-95, with BUILD.md:175-177 recording distinct refusals.
The approved prose names semantic classes, not a literal exhaustive code-name table. The expected bounded labels below name those classes using existing source labels, cited separately for transparency. They are not selected from (a). peer-malformed=probe is additionally explicit in mos1293. If the diagnostic reports a different class/code, send it to Mos before any edit, even if it would make an existing broad assertion pass.
| Fault | Expected reason= | Designed observation | Design/build authority | Existing label locator, not output oracle |
|---|---|---|---|---|
| parent-mode | metadata | Private parent mode is exact0700 | DESIGN.md:119; BUILD.md:17 | scripts/case1-accept.sh:659 |
| parent-owner | metadata | Parent owner is stack uid | DESIGN.md:119; BUILD.md:17 | scripts/case1-accept.sh:659 |
| socket-mode | metadata | Socket mode is exact0600 | DESIGN.md:120; BUILD.md:17 | scripts/case1-accept.sh:659 |
| socket-owner | metadata | Socket owner is stack uid | DESIGN.md:120; BUILD.md:17 | scripts/case1-accept.sh:659 |
| socket-type | metadata | Final entry is socket | DESIGN.md:119; BUILD.md:17 | scripts/case1-accept.sh:659 |
| symlink | metadata | Final entry is not symlink | DESIGN.md:119; BUILD.md:17 | scripts/case1-accept.sh:659 |
| peer | peer | Connected uid equals stack uid | DESIGN.md:136; BUILD.md:17 | scripts/case1-accept.sh:665 |
| peer-pid | peer | Connected pid equals MainPID | DESIGN.md:136; BUILD.md:17 | scripts/case1-accept.sh:665 |
| python-mode | interpreter | Fixed system interpreter safely owned and protected | DESIGN.md:126; BUILD.md:17 | scripts/case1-accept.sh:660 |
| python-missing | interpreter | Missing system Python is failed observation | DESIGN.md:129; BUILD.md:17 | scripts/case1-accept.sh:660 |
| python-not-elf | interpreter | System entry target is ELF | DESIGN.md:126; BUILD.md:17 | scripts/case1-accept.sh:660 |
| peer-malformed | probe | Probe returns only exactly pid uid gid | DESIGN.md:135; DESIGN.md:163; BUILD.md:17; mos-1293 item2 explicitly reason=probe | scripts/case1-accept.sh:666 |
| peer-error | probe-inner-command | Nonzero child output discarded, distinct child failure | DESIGN-C4.md:84-85; BUILD.md:175; BUILD.md:177 | scripts/case1-accept.sh:662; tester_capture_reason |
| peer-uid-fields | metadata | MainPID four proc uid fields equal stack uid | DESIGN.md:118; BUILD.md:17 | scripts/case1-accept.sh:659 |
| changed-inode | changed | Snapshot replacement fails observation | DESIGN.md:120; DESIGN.md:121; BUILD.md:17 | scripts/case1-accept.sh:668 |
| changed-pid | changed | MainPID change fails observation | DESIGN.md:121; BUILD.md:17 | scripts/case1-accept.sh:668 |
| floor | floor | Numeric floor above privileged Console port | DESIGN.md:122; DESIGN.md:123 | scripts/case1-accept.sh:658 |
All existing status assertions and the boundary=failed/no-boundary=observed assertions remain. The draft adds exact reasons; it never weakens a status or changes a failure to fit observation. The special peer-uid-fields functional failure remains exit1. Product changes require Mos. A fixture-only repair can be considered only when the stub is shown to mask the intended fault, under mos1293.
One scratch archive of0604d91fc is prepared at the path in PREPARE.json. Its only change logs every P10 line, reason and status immediately before the same existing assertions in the named loop. Both product scripts and shared fixture are exact base blobs. The COPY-PATH is not a commit/worktree. No source or test in the seat checkout is changed.
On Sage CLEAR: create TMPDIR via mktemp-d /mnt/storage/scratch/tmp/gXXX, export it, invoke diagnostic-a/run.py with that grant. It gates with the busy check, uses timeout-k10 418 and outer448, pipefail/tee, records command/header/results and writes P10.jsonl. It refuses a second run if output exists. No diagnostic/syntax/Node execution has run while preparing this.
## Duckworth0160 expected-set review
All17 expected reasons accepted by source-only review. C1: assert the exact reason token, including its following space, for example `reason=probe `, or match the full P10 line. A prefix expression `/reason=probe/` is forbidden because it also accepts probe-inner-command and other probe-* reasons. C2: keep every existing status, boundary=failed and no-boundary=observed assertion. A fixture repair is limited to a mask shown by diagnostic(a), and an unexpected reason goes to Mos before any edit.
N1 citation corrected: peer-error child failure is DESIGN-C4.md:84-85. N2: the existing peer-hang test at case1-accept.test.mjs:291 has a separate loose probe prefix; Duckworth says it is outside mos1292/1293 scope and does not bind this repair. No change to it is prepared.
@@ -0,0 +1,11 @@
--- 0604d91fc/packages/cli/tests/case1-accept.test.mjs
+++ scratch/packages/cli/tests/case1-accept.test.mjs
@@ -262,6 +262,8 @@
};
for(const [name,fault] of Object.entries(faults)){
const h=host(t);fault(h);const r=run(h);
+ const diagnosticP10 = r.lines.filter(line => line.startsWith('P10 '));
+ writeFileSync(process.env.ROW253_P10_DIAG, JSON.stringify({fault:name, p10:diagnosticP10, reason:/\breason=([^ ]+)/.exec(diagnosticP10.join('\n'))?.[1] ?? null, status:r.status})+'\n', {flag:'a'});
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);
@@ -0,0 +1,17 @@
{"fault":"parent-mode","p10":["P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"metadata","status":0}
{"fault":"parent-owner","p10":["P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"metadata","status":0}
{"fault":"socket-mode","p10":["P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"metadata","status":0}
{"fault":"socket-owner","p10":["P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"metadata","status":0}
{"fault":"socket-type","p10":["P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"metadata","status":0}
{"fault":"symlink","p10":["P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"metadata","status":0}
{"fault":"peer","p10":["P10 tester-socket-squat unnamed boundary=failed reason=peer path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"peer","status":0}
{"fault":"peer-pid","p10":["P10 tester-socket-squat unnamed boundary=failed reason=peer path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"peer","status":0}
{"fault":"python-mode","p10":["P10 tester-socket-squat unnamed boundary=failed reason=interpreter path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"interpreter","status":0}
{"fault":"python-missing","p10":["P10 tester-socket-squat unnamed boundary=failed reason=interpreter path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"interpreter","status":0}
{"fault":"python-not-elf","p10":["P10 tester-socket-squat unnamed boundary=failed reason=interpreter path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"interpreter","status":0}
{"fault":"peer-malformed","p10":["P10 tester-socket-squat unnamed boundary=failed reason=probe-inner-session path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"probe-inner-session","status":0}
{"fault":"peer-error","p10":["P10 tester-socket-squat unnamed boundary=failed reason=probe-inner-session path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"probe-inner-session","status":0}
{"fault":"peer-uid-fields","p10":["P10 tester-socket-squat unnamed boundary=failed reason=metadata path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"metadata","status":1}
{"fault":"changed-inode","p10":["P10 tester-socket-squat unnamed boundary=failed reason=changed path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"changed","status":0}
{"fault":"changed-pid","p10":["P10 tester-socket-squat unnamed boundary=failed reason=changed path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"changed","status":0}
{"fault":"floor","p10":["P10 tester-socket-squat unnamed boundary=failed reason=floor path=/srv/mosaic/.mosaic-dev/console/tester.sock MainPID=200; hostile same-uid substitution remains"],"reason":"floor","status":0}
@@ -0,0 +1,18 @@
{
"base": "0604d91fcfc1949810cd5321aea6ec43e9b74b4b",
"scratch_copy": "/mnt/storage/scratch/tmp/gBHA",
"source": "git archive base, all tracked files, no commit and no .git",
"test": "packages/cli/tests/case1-accept.test.mjs",
"original_test_sha256": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901",
"instrumented_test_sha256": "0e029047fa7d8cd94a17e0caabc383432ef31068c4ea781e880664d7c7650d3d",
"unchanged_product_and_fixture": {
"scripts/case1-accept.sh": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"scripts/case2-unit.sh": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"packages/cli/tests/case-sudo-fixture.mjs": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008"
},
"change": "Output-only JSONL before unchanged existing assertions, scoped to named P10 loop. Records all P10 lines and reason for each fault.",
"tests_run": false,
"node_modules": "plain symlink to canonical node_modules",
"planned_timeout_seconds": 418,
"planned_outer_wait_seconds": 448
}
@@ -0,0 +1,19 @@
{
"grant": "sage-2752",
"base": "0604d91fcfc1949810cd5321aea6ec43e9b74b4b",
"scratch_copy": "/mnt/storage/scratch/tmp/gBHA",
"tmpdir": "/mnt/storage/scratch/tmp/grCq",
"command": [
"timeout",
"-k",
"10",
"418",
"node",
"--test",
"--test-reporter=tap",
"--test-name-pattern",
"row253 P10 refuses unsafe metadata",
"packages/cli/tests/case1-accept.test.mjs"
],
"reason_oracle": "EXPECTED-REASONS.md drafted before diagnostic; output does not define expected reasons"
}
@@ -0,0 +1,26 @@
{
"code": 0,
"timeout": false,
"seconds": 8.632,
"header": {
"grant": "sage-2752",
"base": "0604d91fcfc1949810cd5321aea6ec43e9b74b4b",
"scratch_copy": "/mnt/storage/scratch/tmp/gBHA",
"tmpdir": "/mnt/storage/scratch/tmp/grCq",
"command": [
"timeout",
"-k",
"10",
"418",
"node",
"--test",
"--test-reporter=tap",
"--test-name-pattern",
"row253 P10 refuses unsafe metadata",
"packages/cli/tests/case1-accept.test.mjs"
],
"reason_oracle": "EXPECTED-REASONS.md drafted before diagnostic; output does not define expected reasons"
},
"log": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-253/diagnostic-a/diagnostic.log",
"p10_file": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-253/diagnostic-a/P10.jsonl"
}
@@ -0,0 +1,61 @@
{
"count": 0,
"remaining": [],
"excluded_preexisting_infrastructure": [
{
"pid": 2469277,
"comm": "node-MainThread",
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196",
"started_utc": "2026-10-22T14:20:57.360000+00:00"
},
{
"pid": 2469278,
"comm": "npm exec @model",
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196",
"started_utc": "2026-10-22T14:20:57.370000+00:00"
},
{
"pid": 2469279,
"comm": "npm exec chrome",
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196",
"started_utc": "2026-10-22T14:20:57.370000+00:00"
},
{
"pid": 2469292,
"comm": "npm exec @playw",
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196",
"started_utc": "2026-10-22T14:20:57.380000+00:00"
},
{
"pid": 2469478,
"comm": "node-MainThread",
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196",
"started_utc": "2026-10-22T14:20:58.770000+00:00"
},
{
"pid": 2469485,
"comm": "node-MainThread",
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196",
"started_utc": "2026-10-22T14:20:58.800000+00:00"
},
{
"pid": 2469518,
"comm": "chrome-devtools",
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196",
"started_utc": "2026-10-22T14:20:59.100000+00:00"
},
{
"pid": 2469601,
"comm": "node-MainThread",
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196",
"started_utc": "2026-10-22T14:20:59.440000+00:00"
},
{
"pid": 2470486,
"comm": "codex-code-mode",
"cwd": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196",
"started_utc": "2026-10-22T14:21:07.580000+00:00"
}
],
"note": "Read-only cwd sweep over own repo, scratch archive and diagnostic TMPDIR, including deleted cwd. No signalling."
}
@@ -0,0 +1 @@
/mnt/storage/scratch/tmp/grCq
@@ -0,0 +1 @@
["timeout", "-k", "10", "418", "node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 P10 refuses unsafe metadata", "packages/cli/tests/case1-accept.test.mjs"]
@@ -0,0 +1,18 @@
{"grant": "sage-2752", "base": "0604d91fcfc1949810cd5321aea6ec43e9b74b4b", "scratch_copy": "/mnt/storage/scratch/tmp/gBHA", "tmpdir": "/mnt/storage/scratch/tmp/grCq", "command": ["timeout", "-k", "10", "418", "node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 P10 refuses unsafe metadata", "packages/cli/tests/case1-accept.test.mjs"], "reason_oracle": "EXPECTED-REASONS.md drafted before diagnostic; output does not define expected reasons"}
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: 8459.11348
type: 'test'
...
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 8556.217902
{"code": 0, "timeout": false, "seconds": 8.632, "header": {"grant": "sage-2752", "base": "0604d91fcfc1949810cd5321aea6ec43e9b74b4b", "scratch_copy": "/mnt/storage/scratch/tmp/gBHA", "tmpdir": "/mnt/storage/scratch/tmp/grCq", "command": ["timeout", "-k", "10", "418", "node", "--test", "--test-reporter=tap", "--test-name-pattern", "row253 P10 refuses unsafe metadata", "packages/cli/tests/case1-accept.test.mjs"], "reason_oracle": "EXPECTED-REASONS.md drafted before diagnostic; output does not define expected reasons"}, "log": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-253/diagnostic-a/diagnostic.log", "p10_file": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-253/diagnostic-a/P10.jsonl"}
@@ -0,0 +1,16 @@
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: 8459.11348
type: 'test'
...
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 8556.217902
@@ -0,0 +1 @@
2026-10-22T15:25:48.412740+00:00
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""One scratch diagnostic, executable only after Sage grants its gap."""
import argparse,json,os,signal,subprocess,time
from pathlib import Path
HERE=Path(__file__).resolve().parent
def main():
ap=argparse.ArgumentParser();ap.add_argument('--grant',required=True);args=ap.parse_args()
if not os.environ.get('TMPDIR','').startswith('/mnt/storage/scratch/tmp/g'):
raise RuntimeError('create the granted gXXX TMPDIR first')
prep=json.loads((HERE/'PREPARE.json').read_text());copy=Path(prep['scratch_copy'])
busy=subprocess.run(['bash','/mnt/storage/scratch/sage-busy-now.sh'],capture_output=True,text=True)
if busy.returncode or busy.stdout.splitlines()[-1:]!=['CLEAR']:
raise RuntimeError('busy refusal: '+busy.stdout+busy.stderr)
env=os.environ.copy();env['ROW253_P10_DIAG']=str(HERE/'P10.jsonl')
if (HERE/'RUN-RESULT.json').exists() or (HERE/'P10.jsonl').exists():
raise RuntimeError('one diagnostic only; existing output refuses automatic rerun')
command=['timeout','-k','10','418','node','--test','--test-reporter=tap','--test-name-pattern','row253 P10 refuses unsafe metadata','packages/cli/tests/case1-accept.test.mjs']
command_file=HERE/'command.json';command_file.write_text(json.dumps(command)+'\n')
header=dict(grant=args.grant,base=prep['base'],scratch_copy=str(copy),tmpdir=env['TMPDIR'],command=command,reason_oracle='EXPECTED-REASONS.md drafted before diagnostic; output does not define expected reasons')
(HERE/'RUN-HEADER.json').write_text(json.dumps(header,indent=2)+'\n');print(json.dumps(header),flush=True)
started=time.monotonic()
p=subprocess.Popen(['bash','-o','pipefail','-c','python3 "$1" "$2" 2>&1 | tee "$3"','--',str(HERE.parent/'launch.py'),str(command_file),str(HERE/'diagnostic.log')],cwd=copy,env=env,start_new_session=True)
timeout=False
try:
code=p.wait(timeout=448);timeout=code in (124,137)
except subprocess.TimeoutExpired:
timeout=True;os.killpg(p.pid,signal.SIGKILL);code=p.wait()
finally:
try:os.killpg(p.pid,signal.SIGKILL)
except ProcessLookupError:pass
result=dict(code=code,timeout=timeout,seconds=round(time.monotonic()-started,3),header=header,log=str(HERE/'diagnostic.log'),p10_file=str(HERE/'P10.jsonl'))
(HERE/'RUN-RESULT.json').write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result),flush=True)
if timeout or code:raise RuntimeError('diagnostic stopped; no retry or repair before ruling')
if __name__=='__main__':main()
@@ -0,0 +1 @@
2026-10-22T15:25:29Z
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Run explicit row253 changes only under Sage's grant. --static runs no tests."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import signal
import subprocess
import time
HERE = Path(__file__).resolve().parent
REPO = HERE.parents[3]
def digest(data):
return hashlib.sha256(data).hexdigest()
def busy():
p = subprocess.run(['bash', '/mnt/storage/scratch/sage-busy-now.sh'], text=True, capture_output=True)
if p.returncode or p.stdout.splitlines()[-1:] != ['CLEAR']:
raise RuntimeError('busy refusal: ' + p.stdout + p.stderr)
def run(command, path, seconds=60):
if command[:2] == ['node', '--test']:
limits = json.loads((HERE/'WHOLE-GATE-LIMITS.json').read_text())['files']
limit = limits.get(command[-1], 900)
command = ['timeout','-k','10',str(limit),*command]
seconds = limit + 30
busy()
started = time.monotonic()
command_file = path.with_suffix('.command.json')
command_file.write_text(json.dumps(command))
# Keep command text out of wrapper argv: the busy guard scans every argv.
# launch.py execs the structured command. bash owns a pipefail/tee pipeline.
p = subprocess.Popen(['bash','-o','pipefail','-c',
'python3 "$1" "$2" 2>&1 | tee "$3"', '--',str(HERE/'launch.py'),str(command_file),str(path)],
cwd=REPO,start_new_session=True)
expired = False
try:
code = p.wait(timeout=seconds)
expired = code in (124, 137)
except subprocess.TimeoutExpired:
expired = True
os.killpg(p.pid, signal.SIGKILL)
code = p.wait()
finally:
try:
os.killpg(p.pid, signal.SIGKILL)
except ProcessLookupError:
pass
return dict(code=code, timeout=expired, seconds=round(time.monotonic()-started, 3), log=str(path))
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--static', action='store_true')
ap.add_argument('--grant')
ap.add_argument('--out', type=Path)
args = ap.parse_args()
definitions = json.loads((HERE/'changes.json').read_text())
checked = []
for d in definitions:
original = (REPO/d['file']).read_bytes()
count = original.count(d['old'].encode())
if count != 1:
raise RuntimeError(f"{d['id']}: target occurs {count} times")
changed = original.replace(d['old'].encode(), d['new'].encode(), 1)
checked.append(dict(id=d['id'], file=d['file'], occurrences=count,
before=digest(original), after=digest(changed)))
if args.static:
print(json.dumps(checked, indent=2))
return
if not args.grant or not args.out:
ap.error('execution needs --grant and --out')
if not os.environ.get('TMPDIR', '').startswith('/mnt/storage/scratch/tmp/g'):
raise RuntimeError('TMPDIR must be the granted scratch directory')
status = subprocess.check_output(['git','status','--porcelain','--untracked-files=no'],cwd=REPO)
args.out.mkdir(parents=True, exist_ok=True)
results = dict(grant=args.grant, static=checked, bases={}, changes=[])
failures = 0
for d in definitions:
key = d['killer']+'|'+d['pattern']
command = ['node','--test','--test-reporter=tap','--test-name-pattern',d['pattern'],d['killer']]
if key not in results['bases']:
base = run(command, args.out/(d['id']+'-base.log'))
results['bases'][key] = base
(args.out/'results.json').write_text(json.dumps(results,indent=2)+'\n')
if base['code'] != 0 or base['seconds'] > 30:
raise RuntimeError('base failed or exceeded 30 seconds: '+d['id'])
target = REPO/d['file']
original = target.read_bytes()
try:
target.write_bytes(original.replace(d['old'].encode(),d['new'].encode(),1))
pre = subprocess.run(['bash','-n',str(target)],capture_output=True)
if pre.returncode:
raise RuntimeError('invalid syntax: '+d['id'])
result = run(command,args.out/(d['id']+'.log'))
result.update(id=d['id'], base_seconds=results['bases'][key]['seconds'],
outcome='timeout' if result['timeout'] else 'killed' if result['code'] != 0 else 'survived')
results['changes'].append(result)
if result['timeout']:
raise RuntimeError('timeout requires Sage/Mos classification: '+d['id'])
if result['outcome'] != 'killed':
failures += 1
finally:
target.write_bytes(original)
assert target.read_bytes() == original
assert subprocess.check_output(['git','status','--porcelain','--untracked-files=no'],cwd=REPO) == status
(args.out/'results.json').write_text(json.dumps(results,indent=2)+'\n')
if failures >= 1:
raise RuntimeError('first unexpected failure: end the sage-2297 window')
print(json.dumps(results,indent=2))
if __name__ == '__main__':
main()
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Summarize completed focused evidence without executing tests."""
import datetime
import hashlib
import json
import math
from pathlib import Path
HERE=Path(__file__).resolve().parent
RUN=HERE/'run3-focused'
def main():
results=json.loads((RUN/'files/results.json').read_text())
assert len(results['steps'])==27
assert all(s['code']==0 and not s['timeout'] and s['counts'].get('fail')==0 for s in results['steps'])
header=(RUN/'controller.log').read_text().splitlines()
assert json.loads(header[0])=={k:results[k] for k in ['grant','head','manifest']}
assert json.loads(header[1])['authority']=='sage-2709'
named=[]
basis={}
lines=['# Focused27 whole-file results','','| Whole file | Pass/fail/skip | Wall seconds | Command limit seconds | Next3x plus120 basis |','|---|---:|---:|---:|---:|']
for s in results['steps']:
command=json.loads(Path(s['log']).with_suffix('.command.json').read_text())
expected=900 if s['file'].startswith('packages/cli/') else 300
assert command==['timeout','-k','10',str(expected),'node','--test','--test-reporter=tap',s['file']]
c=s['counts'];limit=math.ceil(3*s['seconds'])+120
basis[s['file']]=dict(recorded_seconds=s['seconds'],receipt=s['log'],next_limit_seconds=limit,derivation='ceil(3*measured whole-file seconds)+120')
lines.append(f"| {s['file']} | {c['pass']}/{c['fail']}/{c['skipped']} | {s['seconds']} | {expected} | {limit} |")
named += [dict(file=s['file'],line=line) for line in s['named_skips']]
case2=next(s for s in results['steps'] if s['file'].endswith('/case2-unit.test.mjs'))
n9=[line for line in Path(case2['log']).read_text().splitlines() if 'inside a session' in line and 'ok ' in line]
assert len(n9)==1
result=dict(status='GREEN',grant=results['grant'],candidate=results['head'],manifest=results['manifest'],counts={k:sum(s['counts'].get(k,0) for s in results['steps']) for k in ['pass','fail','skipped']},steps=results['steps'],named_skips=named,n9_exact_line=n9[0],file_seconds_sum=round(sum(s['seconds'] for s in results['steps']),3),next_file_limits=basis,timeout_count=0,source_manifest_verified=True)
(RUN/'SUMMARY.json').write_text(json.dumps(result,indent=2)+'\n')
(RUN/'RESULTS.md').write_text('\n'.join(lines)+'\n\nN9 verbatim: '+n9[0]+'\n\nEvery skip:\n\n'+'\n'.join('- '+s['line'] for s in named)+'\n')
(RUN/'MEASURED-FILE-LIMITS.json').write_text(json.dumps(basis,indent=2)+'\n')
print(json.dumps({k:result[k] for k in ['counts','named_skips','n9_exact_line','file_seconds_sum']},indent=2))
if __name__=='__main__':main()
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Read-only sweep of this worktree and focused TMPDIR, including deleted cwd."""
import datetime
import json
import os
from pathlib import Path
HERE=Path(__file__).resolve().parent
REPO=HERE.parents[3]
RUN=HERE/'run3-focused'
def main():
tmp=Path((RUN/'TMPDIR').read_text().strip())
started=datetime.datetime.fromisoformat((RUN/'start.txt').read_text().strip().replace('Z','+00:00')).timestamp()
boot=int(next(s.split()[1] for s in Path('/proc/stat').read_text().splitlines() if s.startswith('btime ')))
ancestors=set();pid=os.getpid()
while pid and pid not in ancestors:
ancestors.add(pid)
try:pid=int(next(s.split(':')[1] for s in Path(f'/proc/{pid}/status').read_text().splitlines() if s.startswith('PPid:')))
except (OSError,StopIteration):break
known={2880281,2880282,2880283,2880284,2880445,2880487,2880537,2883251,2880439,2469277,2469278,2469279,2469292,2469478,2469485,2469518,2469601,2470486}
remaining=[];excluded=[]
for proc in Path('/proc').iterdir():
if not proc.name.isdigit() or int(proc.name) in ancestors:continue
try:
rawcwd=os.readlink(proc/'cwd');cwd=Path(rawcwd.removesuffix(' (deleted)'))
if not(cwd==REPO or REPO in cwd.parents or cwd==tmp or tmp in cwd.parents):continue
stat=(proc/'stat').read_text().rsplit(') ',1)[1].split()
stamp=boot+int(stat[19])/os.sysconf('SC_CLK_TCK')
item=dict(pid=int(proc.name),comm=(proc/'comm').read_text().strip(),cwd=rawcwd,started_utc=datetime.datetime.fromtimestamp(stamp,datetime.timezone.utc).isoformat())
if int(proc.name) in known and stamp<started:excluded.append(item)
else:remaining.append(item)
except (OSError,ValueError,IndexError):continue
result=dict(timestamp=datetime.datetime.now(datetime.timezone.utc).isoformat(),count=len(remaining),remaining=remaining,excluded_preexisting_infrastructure=excluded,note='Read-only /proc cwd sweep, including deleted cwd under this run TMPDIR. Own ancestry excluded. Only known MCP/npm/code-mode infrastructure PIDs with pre-run start ticks are excluded. No signalling.')
(RUN/'sweep.json').write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result,indent=2))
if __name__=='__main__':main()
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Assemble complete whole-gate evidence from retained logs only."""
import hashlib,json,re
from pathlib import Path
HERE=Path(__file__).resolve().parent
RUN=HERE/'run3-gate'
def main():
sections={n:json.loads((RUN/n/'results.json').read_text()) for n in ['focused','changes','whole']}
header=json.loads((RUN/'HEADER.json').read_text());lists=json.loads((RUN/'FILE-LISTS.json').read_text())
whole=sections['whole']['steps'];expected=sum(map(len,lists.values()))+10
assert len(whole)==expected
assert all(s['code']==0 and not s['timeout'] for s in whole)
focused=sections['focused']['steps']
assert len(focused)==4 and all(not s['timeout'] and (s['code']!=0 if s['expected_red'] else s['code']==0) for s in focused)
changes=sections['changes']['changes'];assert len(changes)==40 and all(s['outcome']=='killed' and not s['timeout'] for s in changes)
limits=json.loads((HERE/'WHOLE-GATE-LIMITS.json').read_text())['files'];shell_limits=json.loads((HERE/'SHELL-LIMITS.json').read_text())['scripts']
counts={};skips=[];rows=[];shells=[];n9=[]
for package,files in lists.items():
steps=[s for s in whole if s['name'].startswith(package+'-')]
commands=[json.loads(Path(s['log']).with_suffix('.command.json').read_text()) for s in steps]
assert [c[-1] for c in commands]==files
counts[package]={k:sum(s['counts'].get(k,0) for s in steps) for k in ['pass','fail','skipped']}
for s,c,file in zip(steps,commands,files):
assert c==['timeout','-k','10',str(limits[file]),'node','--test','--test-reporter=tap',file]
text=Path(s['log']).read_text(errors='replace')
skips += [dict(file=file,line=line) for line in text.splitlines() if '# SKIP' in line]
if file.endswith('/case2-unit.test.mjs'):n9 += [line for line in text.splitlines() if line.startswith('ok ') and "inside a session's sandbox" in line]
rows.append(dict(file=file,counts=s['counts'],seconds=s['seconds'],limit_seconds=limits[file],log=s['log']))
for s in whole[-10:]:
command=json.loads(Path(s['log']).with_suffix('.command.json').read_text());file=command[-1]
assert command==['timeout','-k','10',str(shell_limits[file]['limit_seconds']),'bash',file]
text=Path(s['log']).read_text(errors='replace');found=re.findall(r'([^\n]*?: (\d+) passed, (\d+) failed)',text);assert found,file
line,passed,failed=found[-1];assert int(failed)==0
skips += [dict(file=file,line=line) for line in text.splitlines() if re.match(r'^\s*skip\b',line,re.I) or '# SKIP' in line]
shells.append(dict(file=file,pass_count=int(passed),fail_count=int(failed),seconds=s['seconds'],limit_seconds=shell_limits[file]['limit_seconds'],summary_line=line,log=s['log']))
assert len(n9)==1
counts['shell_self_assertions']=dict(pass_count=sum(s['pass_count'] for s in shells),fail_count=0)
result=dict(status='GREEN',header=header,sections=sections,counts=counts,files=rows,shells=shells,named_skips=skips,n9_exact_line=n9[0],killed=40,survived=0,timeouts=0,head_diff_outside_gander=(RUN/'HEAD-DIFF-OUTSIDE-GANDER.txt').read_text().splitlines())
repo=HERE.parents[3]
expected_hashes=json.loads((HERE/'CANDIDATE-SHAS.json').read_text())
assert all(hashlib.sha256((repo/n).read_bytes()).hexdigest()==v for n,v in expected_hashes.items())
assert all(hashlib.sha256((repo/n).read_bytes()).hexdigest()==v['script_sha256'] for n,v in shell_limits.items())
(RUN/'SUMMARY.json').write_text(json.dumps(result,indent=2)+'\n')
lines=['# Row253 whole gate RESULTS','','HEAD '+header['head']+', product '+header['product']+', manifest '+header['manifest']+'.','','0604..344 name-only outside agents/gander/:','','```text',*result['head_diff_outside_gander'],'```','','All listed whole files ran, with no unlisted-file coverage claim. Intended REDs and focused repeats are separate from package totals. Bus coverage is the20-file rebase union, not a bus-whole claim.','','| Whole file | Pass/fail/skip | Wall seconds | Limit seconds |','|---|---:|---:|---:|']
lines += [f"| {r['file']} | {r['counts']['pass']}/{r['counts']['fail']}/{r['counts']['skipped']} | {r['seconds']} | {r['limit_seconds']} |" for r in rows]
lines += [f"| {r['file']} | {r['pass_count']}/{r['fail_count']}/0 | {r['seconds']} | {r['limit_seconds']} |" for r in shells]
lines += ['','N9 verbatim: '+n9[0],'','Every named skip:','']+[f"- {s['file']}: {s['line']}" for s in skips]
(RUN/'RESULTS.md').write_text('\n'.join(lines)+'\n')
defs={d['id']:d for d in json.loads((HERE/'changes.json').read_text())}
table=['# M1-M40 kill table','','Every base passes; each change restored exact bytes and tracked status before the next. No timeout counts as kill.','','| Check | Named killer | Base seconds | Changed seconds | Outcome |','|---|---|---:|---:|---|']
table += [f"| {s['id']} | {defs[s['id']]['pattern']} | {s['base_seconds']} | {s['seconds']} | {s['outcome']} |" for s in changes]
(RUN/'KILL-TABLE.md').write_text('\n'.join(table)+'\n')
print(json.dumps(dict(counts=counts,named_skips=skips,n9=n9,killed=40),indent=2))
if __name__=='__main__':main()
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Read-only sweep of this worktree and focused TMPDIR, including deleted cwd."""
import datetime
import json
import os
from pathlib import Path
HERE=Path(__file__).resolve().parent
REPO=HERE.parents[3]
RUN=HERE/'run3-gate'
def main():
tmp=Path((RUN/'TMPDIR').read_text().strip())
started=datetime.datetime.fromisoformat((RUN/'start.txt').read_text().strip().replace('Z','+00:00')).timestamp()
boot=int(next(s.split()[1] for s in Path('/proc/stat').read_text().splitlines() if s.startswith('btime ')))
ancestors=set();pid=os.getpid()
while pid and pid not in ancestors:
ancestors.add(pid)
try:pid=int(next(s.split(':')[1] for s in Path(f'/proc/{pid}/status').read_text().splitlines() if s.startswith('PPid:')))
except (OSError,StopIteration):break
known={2880281,2880282,2880283,2880284,2880445,2880487,2880537,2883251,2880439,2469277,2469278,2469279,2469292,2469478,2469485,2469518,2469601,2470486}
remaining=[];excluded=[]
for proc in Path('/proc').iterdir():
if not proc.name.isdigit() or int(proc.name) in ancestors:continue
try:
rawcwd=os.readlink(proc/'cwd');cwd=Path(rawcwd.removesuffix(' (deleted)'))
if not(cwd==REPO or REPO in cwd.parents or cwd==tmp or tmp in cwd.parents):continue
stat=(proc/'stat').read_text().rsplit(') ',1)[1].split()
stamp=boot+int(stat[19])/os.sysconf('SC_CLK_TCK')
item=dict(pid=int(proc.name),comm=(proc/'comm').read_text().strip(),cwd=rawcwd,started_utc=datetime.datetime.fromtimestamp(stamp,datetime.timezone.utc).isoformat())
if int(proc.name) in known and stamp<started:excluded.append(item)
else:remaining.append(item)
except (OSError,ValueError,IndexError):continue
result=dict(timestamp=datetime.datetime.now(datetime.timezone.utc).isoformat(),count=len(remaining),remaining=remaining,excluded_preexisting_infrastructure=excluded,note='Read-only /proc cwd sweep, including deleted cwd under this run TMPDIR. Own ancestry excluded. Only known MCP/npm/code-mode infrastructure PIDs with pre-run start ticks are excluded. No signalling.')
(RUN/'sweep.json').write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result,indent=2))
if __name__=='__main__':main()
@@ -0,0 +1,15 @@
# Row253 integration onto 0fa7bf421
Authority: sage-2463. New branch gander/row253-0fa7, rebase tip before these records 67224933425e9687bf366cd7ba7c31c1daa4e9f3. Both earlier branches remain unchanged. Nine commits replayed onto 0fa7bf421fc153a226b5f06ed484e089e48e23ba.
One conflict: the first design commit's BUILD-LOG.md append. Kept upstream in full, then appended the row253 phase. The integrated log has the entire current base blob as its exact prefix. It stays outside the manifest and lands as a separate doc-only append after code. README merged automatically, retaining row253's tester-socket wording and row191's complete Instance processing controls section. No runtime or test conflict.
Only packages/webui/README.md changes among the seven manifest entries. CANDIDATE-SHAS.json and MANIFEST.sha256 are refreshed. The old C4 packets remain historical exact snapshots; the README addition is the sole new manifest blob. Runtime scripts, focused test files and shared fixture remain byte-identical to the approved candidate.
## Static test dependency inspection
The row253 focused case1/case2 files read their two host scripts, generated fixtures, kernel proc data, the two service templates, and case2-precheck.sh. They import the shared case-sudo fixture, cli test helpers, business test helpers and seat sandbox helpers. Neither directly reads or imports the row191 changes in webui cli.mjs, serve.mjs, index.html, processing.js/css or processing-http.mjs. The cli.mjs/config.mjs in their scratch copies are stand-ins written by the fixtures, not the repository Console code.
The required WebUI whole suite does consume row191's changed modules and UI, including the processing/browser/shell tests now present on the integrated base. The full CLI suite also runs against the new base and scripts/mosaic. No focused test, suite, syntax execution, browser, probe or host experiment ran. Dependency inspection used source reads only.
range-diff.json covers the nine replayed commits. patch-ids.json lists each old/new pair; the final record/manifest refresh commit has no old counterpart. The requested exact range-diff through the final committed tip is sent with the handoff. Publishing uses only the repo helper and waits for a clear busy check.
@@ -0,0 +1,47 @@
{
"digest": "24b6a01b2ede6f025ec898fa4ec830bb9585ce6151d6298e713e31c7430a502b",
"entries": [
{
"path": "scripts/case2-unit.sh",
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"after": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"changed": false
},
{
"path": "scripts/case1-accept.sh",
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"after": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"changed": false
},
{
"path": "packages/cli/tests/case-sudo-fixture.mjs",
"before": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
"after": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
"changed": false
},
{
"path": "packages/cli/tests/case2-unit.test.mjs",
"before": "1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44",
"after": "1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44",
"changed": false
},
{
"path": "packages/cli/tests/case1-accept.test.mjs",
"before": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901",
"after": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901",
"changed": false
},
{
"path": "packages/webui/README.md",
"before": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9",
"after": "9de6c8356e1f1e0ecb8d0be8ec523cb817b04bb633f0cd8aed7db32565ffd009",
"changed": true
},
{
"path": "docs/plans/DEFERRED.md",
"before": "3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f",
"after": "3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f",
"changed": false
}
]
}
@@ -0,0 +1,74 @@
[
{
"old": "971681f29a347a3771802b19d1b9a65c88943a79",
"new": "39c30f1c200a1f5f27767dae4b730cade7703777",
"subject": "gander: design tester socket migration for host scripts",
"old_patch_id": "47bdc56d349c8fcce2e5f56f3f5ce4ce7b1d6f14",
"new_patch_id": "895656745186c2dc4cb1ee1261c323af90bf9fb9",
"equal": false
},
{
"old": "43f17b9afe60bfe256e924b1317332d6068a8b4f",
"new": "d004b44982db5196777710e20163f877b07bf54d",
"subject": "test: prepare row253 socket fixtures and fix gate collisions",
"old_patch_id": "d597cc7402b568d845a87c1f142bde74ce6ae9da",
"new_patch_id": "d597cc7402b568d845a87c1f142bde74ce6ae9da",
"equal": true
},
{
"old": "f9b9112b970b634ee7c990447ded04dfe622d8cc",
"new": "9e4bbee25d3a97e9dfec743d92c20f50154222da",
"subject": "docs: propose row253 C4 session supervisor and draft hop fake",
"old_patch_id": "899327761a9bde7e7b4fefd790c1a523488e8029",
"new_patch_id": "899327761a9bde7e7b4fefd790c1a523488e8029",
"equal": true
},
{
"old": "70e6bb16159dd28391dbbfe0b4808f0c3dde5225",
"new": "69474277277fe87e7c398c508f6450aa0d691de7",
"subject": "docs(row253): freeze C4 repair for static review",
"old_patch_id": "38dcf7f0ec2a39bad2696d971a684a729b154747",
"new_patch_id": "38dcf7f0ec2a39bad2696d971a684a729b154747",
"equal": true
},
{
"old": "bef1bd886b6e90f2a404825cce728e3901328ba3",
"new": "d85cf93d64a4ebf37865cbb74719d842105a1864",
"subject": "docs(row253): store repair patch without context whitespace",
"old_patch_id": "1cf1916dab2a8552fadd07ce81246fce5f105b3d",
"new_patch_id": "1cf1916dab2a8552fadd07ce81246fce5f105b3d",
"equal": true
},
{
"old": "ecb15d1debdad4fbd2e9bdfedb639143467709c6",
"new": "463a97f49f6bcaa65a10a54c2ad40e428e7fd135",
"subject": "fix(row253): supervise socket queries across sudo sessions",
"old_patch_id": "d104bd53960b75c1e9d9402ec2ea193a55bf8164",
"new_patch_id": "08a16c862148ddae13044130cac740064d0e86fe",
"equal": false
},
{
"old": "39b98a26e9ac00edf3b8d77690cf06bc689a6272",
"new": "de93d8e038f591ea7be2f850c5fb0dea272c3d63",
"subject": "fix(row253): bound cleanup retries after initial inventory",
"old_patch_id": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3",
"new_patch_id": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3",
"equal": true
},
{
"old": "2f466464cccfe7b4f2522936a7f8409b74af7b5c",
"new": "36bd3c3a8bda8c31da8219cc726ce005ec2dbd2b",
"subject": "docs(row253): record static approval and pre-confirmation residual",
"old_patch_id": "d2aecebf222474b0e06c5109b5f22caf768176ce",
"new_patch_id": "d2aecebf222474b0e06c5109b5f22caf768176ce",
"equal": true
},
{
"old": "8b8f8d7810d076974aedcae3c800123bf791fa47",
"new": "67224933425e9687bf366cd7ba7c31c1daa4e9f3",
"subject": "docs(row253): record integration onto refactor 378a76b85",
"old_patch_id": "efb06f08af899d8f03390ab24ab65f6558cbae58",
"new_patch_id": "efb06f08af899d8f03390ab24ab65f6558cbae58",
"equal": true
}
]
@@ -0,0 +1,9 @@
{
"command": [
"git",
"range-diff",
"378a76b85..8b8f8d781",
"0fa7bf421..672249334"
],
"stdout": " 1: 971681f29 ! 1: 39c30f1c2 gander: design tester socket migration for host scripts\n @@ Commit message\n \n ## BUILD-LOG.md ##\n @@ BUILD-LOG.md: Before: managed project/thread verbs had no Console conversation area. After: fi\n - 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.\n + 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)\n \n - 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.\n + 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).\n +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.\n \n ## agents/gander/work/queue-253/BASE-PORT-INVENTORY.json (new) ##\n 2: 43f17b9af = 2: d004b4498 test: prepare row253 socket fixtures and fix gate collisions\n 3: f9b9112b9 = 3: 9e4bbee25 docs: propose row253 C4 session supervisor and draft hop fake\n 4: 70e6bb161 = 4: 694742772 docs(row253): freeze C4 repair for static review\n 5: bef1bd886 = 5: d85cf93d6 docs(row253): store repair patch without context whitespace\n 6: ecb15d1de ! 6: 463a97f49 fix(row253): supervise socket queries across sudo sessions\n @@ Commit message\n ## BUILD-LOG.md ##\n @@ BUILD-LOG.md: Before: managed project/thread verbs had no Console conversation area. After: fi\n \n - 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-unkLine truncated
}
@@ -0,0 +1,98 @@
[
"BUILD-LOG.md",
"Containerfile",
"agents/darkwing/work/queue-273/BUILD.md",
"agents/rocko/work/queue-191/stoprun-build/BUILD.md",
"compose.yaml",
"docs/TOOLS.md",
"docs/plans/QUEUE.md",
"docs/plans/queue.json",
"packages/bus/README.md",
"packages/bus/src/broker.mjs",
"packages/bus/src/client.mjs",
"packages/bus/src/runtime.mjs",
"packages/bus/src/server.mjs",
"packages/bus/src/web.mjs",
"packages/bus/tests/processing-runtime.test.mjs",
"packages/bus/tests/processing-timeout.test.mjs",
"packages/bus/tests/processing-web.test.mjs",
"packages/bus/tests/processing.test.mjs",
"packages/bus/tests/project-runtime.test.mjs",
"packages/bus/tests/tasks.test.mjs",
"packages/cli/README.md",
"packages/cli/src/cli.mjs",
"packages/cli/src/git-socket.mjs",
"packages/cli/src/launcher.mjs",
"packages/cli/src/notifier-process.mjs",
"packages/cli/src/notifier.mjs",
"packages/cli/tests/gate-lane-image.test.mjs",
"packages/cli/tests/host.test.mjs",
"packages/cli/tests/launch-account.test.mjs",
"packages/cli/tests/launch-limit-checks.test.mjs",
"packages/cli/tests/launch-limits.test.mjs",
"packages/cli/tests/launch-routes.test.mjs",
"packages/cli/tests/launcher.test.mjs",
"packages/cli/tests/notifier.test.mjs",
"packages/cli/tests/processing-cli.test.mjs",
"packages/cli/tests/stack-copy.test.mjs",
"packages/cli/tests/stack-release-external.test.mjs",
"packages/cli/tests/stack-release.test.mjs",
"packages/discord/src/cli.mjs",
"packages/discord/src/connector.mjs",
"packages/discord/src/engine-pi.mjs",
"packages/discord/src/journal.mjs",
"packages/discord/src/processing.mjs",
"packages/discord/tests/binding.test.mjs",
"packages/discord/tests/processing-stop.test.mjs",
"packages/discord/tests/processing.test.mjs",
"packages/discord/tests/recover.test.mjs",
"packages/processing-control/README.md",
"packages/processing-control/package.json",
"packages/processing-control/src/container-cli.mjs",
"packages/processing-control/src/containers.mjs",
"packages/processing-control/src/coordinator.mjs",
"packages/processing-control/src/effects.mjs",
"packages/processing-control/src/entrypoint.mjs",
"packages/processing-control/src/runtimes.mjs",
"packages/processing-control/src/state.mjs",
"packages/processing-control/tests/containers.test.mjs",
"packages/processing-control/tests/coordinator.test.mjs",
"packages/processing-control/tests/effects.test.mjs",
"packages/processing-control/tests/mount.test.mjs",
"packages/processing-control/tests/runtimes.test.mjs",
"packages/processing-control/tests/state.test.mjs",
"packages/processing-control/tests/stop-run.test.mjs",
"packages/processing-control/tests/task.test.mjs",
"packages/project/src/git.mjs",
"packages/seat/src/checkout.mjs",
"packages/seat/src/landing.mjs",
"packages/seat/src/sandbox.mjs",
"packages/seat/src/working.mjs",
"packages/seat/tests/working.test.mjs",
"packages/tasks/src/adapter.mjs",
"packages/tasks/src/vikunja.mjs",
"packages/tasks/tests/processing.test.mjs",
"packages/webui/README.md",
"packages/webui/src/cli.mjs",
"packages/webui/src/processing-http.mjs",
"packages/webui/src/public/index.html",
"packages/webui/src/public/processing.css",
"packages/webui/src/public/processing.js",
"packages/webui/src/serve.mjs",
"packages/webui/tests/browser.test.mjs",
"packages/webui/tests/processing-browser.test.mjs",
"packages/webui/tests/processing-ui.test.mjs",
"packages/webui/tests/processing.test.mjs",
"packages/webui/tests/shell.test.mjs",
"scripts/agent.sh",
"scripts/common.sh",
"scripts/hello.sh",
"scripts/mosaic",
"scripts/mosaic-task.mjs",
"scripts/run-task.sh",
"scripts/stack-copy.sh",
"scripts/stack-release.sh",
"scripts/testing/lane-image.mjs",
"scripts/verify.sh",
"src/run-agent.sh"
]
@@ -0,0 +1,43 @@
# Row253 final integration after264a
Authority sage-2698. New gander/row253-264a on84f53540d; previous branch retained. Eleven commits replayed with no conflict. All eleven range-diff pairs and full patch-ids equal. Rebased tip before this record b9d3d4c75c78aa6b06aefd734da781bef9b7f1da.
Only DEFERRED changes among the seven manifest blobs, preserving the new base as an exact byte prefix. Six other blobs remain unchanged, including both host scripts, both focused tests and shared sudo fixture. New manifest 735fa2d7405d01ace8eb7aab366753fa5b1877cf8044b0151cb2113a0502bc1f. BUILD-LOG remains an append outside the landing manifest.
264a reaches the focused tests through Store loaded by CLI helpers. The new base includes schema3l, migrations/3k-3l.sql and matching store hash. The focused tests pin neither3k/3l nor schema bytes. No product/test/assertion repair. Duckworth0132 already checked this dependency statically against264a r9. This packet also lists actual landing changes. T46 stack-release.test.mjs reads actual schema and will run whole.
The required whole-file union conservatively includes every .test.mjs changed upstream from385a to84f plus both own focused test files. Shared case-sudo fixture is exercised by its two importers; schema-3k.sql is a migration fixture, not a standalone test. Whole-test-union.json has 27 runnable files. No result carries. These runs await a focused CLEAR before publication and Duckworth rebase check; the whole gate needs a later CLEAR.
N9 remains the exact case2 test: inside a session's sandbox a bind below1024 fails with EACCES, and a bind on a free high port succeeds. Its skip reason comes from sandboxProblem() at gate time and will be copied verbatim. Other environment/provider/canonical-root skips will be named too.
SHELL-LIMITS.json stays as prepared under sage-2659. Every scripts/test-*.sh blob matches the previous base; the only changed script upstream is stack-release.sh. Current limits retain their receipts and3x rule. No tests, Node, syntax commands, browser, host probe, queue command or push ran.
Required whole files:
- packages/bus/tests/account-migration.test.mjs
- packages/bus/tests/chief-of-staff.test.mjs
- packages/bus/tests/comment-migration.test.mjs
- packages/bus/tests/end-launch.test.mjs
- packages/bus/tests/fire-revokes-grants.test.mjs
- packages/bus/tests/gitea-grant.test.mjs
- packages/bus/tests/gitea-migration.test.mjs
- packages/bus/tests/grant-executor.test.mjs
- packages/bus/tests/grant-migration.test.mjs
- packages/bus/tests/lifecycle-revoke.test.mjs
- packages/bus/tests/message-filter.test.mjs
- packages/bus/tests/name-migration.test.mjs
- packages/bus/tests/schema.test.mjs
- packages/bus/tests/sessions.test.mjs
- packages/bus/tests/spec-migration.test.mjs
- packages/bus/tests/tester-writes.test.mjs
- packages/bus/tests/thread-claims.test.mjs
- packages/bus/tests/thread-migration.test.mjs
- packages/bus/tests/threads.test.mjs
- packages/bus/tests/usage-migration.test.mjs
- packages/cli/tests/case1-accept.test.mjs
- packages/cli/tests/case2-unit.test.mjs
- packages/cli/tests/lifecycle.test.mjs
- packages/cli/tests/stack-release.test.mjs
- packages/cli/tests/thread-cli.test.mjs
- packages/cli/tests/thread-launch.test.mjs
- packages/webui/tests/chat.test.mjs
@@ -0,0 +1,90 @@
{
"old_base": "385a7194c",
"old_tip": "d213ef651",
"new_base": "84f53540d",
"rebased_tip": "b9d3d4c75c78aa6b06aefd734da781bef9b7f1da",
"manifest": "735fa2d7405d01ace8eb7aab366753fa5b1877cf8044b0151cb2113a0502bc1f",
"manifest_blobs_changed": [
"docs/plans/DEFERRED.md"
],
"upstream_paths": [
"agents/gizmoduck/work/queue-264/BUILD.md",
"agents/megavolt/work/queue-281/BUILD.md",
"docs/plans/DEFERRED.md",
"packages/bus/README.md",
"packages/bus/schema.sql",
"packages/bus/src/broker.mjs",
"packages/bus/src/migrations/3k-3l.sql",
"packages/bus/src/store.mjs",
"packages/bus/tests/account-migration.test.mjs",
"packages/bus/tests/chief-of-staff.test.mjs",
"packages/bus/tests/comment-migration.test.mjs",
"packages/bus/tests/end-launch.test.mjs",
"packages/bus/tests/fire-revokes-grants.test.mjs",
"packages/bus/tests/fixtures/schema-3k.sql",
"packages/bus/tests/gitea-grant.test.mjs",
"packages/bus/tests/gitea-migration.test.mjs",
"packages/bus/tests/grant-executor.test.mjs",
"packages/bus/tests/grant-migration.test.mjs",
"packages/bus/tests/lifecycle-revoke.test.mjs",
"packages/bus/tests/message-filter.test.mjs",
"packages/bus/tests/name-migration.test.mjs",
"packages/bus/tests/schema.test.mjs",
"packages/bus/tests/sessions.test.mjs",
"packages/bus/tests/spec-migration.test.mjs",
"packages/bus/tests/tester-writes.test.mjs",
"packages/bus/tests/thread-claims.test.mjs",
"packages/bus/tests/thread-migration.test.mjs",
"packages/bus/tests/threads.test.mjs",
"packages/bus/tests/usage-migration.test.mjs",
"packages/cli/tests/lifecycle.test.mjs",
"packages/cli/tests/stack-release.test.mjs",
"packages/cli/tests/thread-cli.test.mjs",
"packages/cli/tests/thread-launch.test.mjs",
"packages/webui/tests/chat.test.mjs",
"scripts/stack-release.sh"
],
"whole_test_union": [
"packages/bus/tests/account-migration.test.mjs",
"packages/bus/tests/chief-of-staff.test.mjs",
"packages/bus/tests/comment-migration.test.mjs",
"packages/bus/tests/end-launch.test.mjs",
"packages/bus/tests/fire-revokes-grants.test.mjs",
"packages/bus/tests/gitea-grant.test.mjs",
"packages/bus/tests/gitea-migration.test.mjs",
"packages/bus/tests/grant-executor.test.mjs",
"packages/bus/tests/grant-migration.test.mjs",
"packages/bus/tests/lifecycle-revoke.test.mjs",
"packages/bus/tests/message-filter.test.mjs",
"packages/bus/tests/name-migration.test.mjs",
"packages/bus/tests/schema.test.mjs",
"packages/bus/tests/sessions.test.mjs",
"packages/bus/tests/spec-migration.test.mjs",
"packages/bus/tests/tester-writes.test.mjs",
"packages/bus/tests/thread-claims.test.mjs",
"packages/bus/tests/thread-migration.test.mjs",
"packages/bus/tests/threads.test.mjs",
"packages/bus/tests/usage-migration.test.mjs",
"packages/cli/tests/case1-accept.test.mjs",
"packages/cli/tests/case2-unit.test.mjs",
"packages/cli/tests/lifecycle.test.mjs",
"packages/cli/tests/stack-release.test.mjs",
"packages/cli/tests/thread-cli.test.mjs",
"packages/cli/tests/thread-launch.test.mjs",
"packages/webui/tests/chat.test.mjs"
],
"shared_test_helpers": [
"packages/cli/tests/case-sudo-fixture.mjs"
],
"shared_fixture_artifacts": [
"packages/bus/tests/fixtures/schema-3k.sql"
],
"unchanged_runnable_blobs": [
"packages/cli/tests/case-sudo-fixture.mjs",
"packages/cli/tests/case1-accept.test.mjs",
"packages/cli/tests/case2-unit.test.mjs",
"packages/webui/README.md",
"scripts/case1-accept.sh",
"scripts/case2-unit.sh"
]
}
@@ -0,0 +1,20 @@
diff --git a/docs/plans/DEFERRED.md b/docs/plans/DEFERRED.md
index 4c0c736f1..020fd5b57 100644
--- a/docs/plans/DEFERRED.md
+++ b/docs/plans/DEFERRED.md
@@ -460,6 +460,15 @@ at every gate. Started 2026-09-12 during the control board MVP.
changes `packages/cli/tests/tester.test.mjs`: add the resolve pin so M8
dies on an assertion. (2026-10-22)
+- **Row 274's user reply check is scoped to the thread, not the session.**
+ (Row 264a r6, sage-2591.) In `packages/bus/src/broker.mjs`, the check that
+ a run answers a user only in reply to that user's message in its thread
+ joins `message_threads` by thread. That holds while a user's thread has
+ only its default session: a user can't call `session.create`, and
+ `#thread` refuses anyone but the owner. Binds row 272 (264e, #1777), or
+ any row that lets a user add sessions: make the check session-scoped and
+ test it with two sessions. (2026-10-22)
+
## Queue
Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
@@ -0,0 +1,68 @@
[
{
"old": "a9c094d0657a52c22ba51b35e690868358cf2bd0",
"new": "0c44ba7c46103d4cb74de2a5393ce677a5010378",
"old_patch_id": "2b17f1d6773aa17909fc959081eec3f3821c3dee",
"new_patch_id": "2b17f1d6773aa17909fc959081eec3f3821c3dee"
},
{
"old": "2e81f78722ccd43d7524bb6a7d27d7ac26d0cea8",
"new": "a1f3b4f31b861e19b9a05189fb16692515819f5f",
"old_patch_id": "d597cc7402b568d845a87c1f142bde74ce6ae9da",
"new_patch_id": "d597cc7402b568d845a87c1f142bde74ce6ae9da"
},
{
"old": "3c8edc358c1f30ea7b5cc9a79d30282e59092715",
"new": "1ecc23eb1f0b68e8a940169a71b6d37f754a034c",
"old_patch_id": "899327761a9bde7e7b4fefd790c1a523488e8029",
"new_patch_id": "899327761a9bde7e7b4fefd790c1a523488e8029"
},
{
"old": "3114087efe655f1d38c5e30f8327f65980854043",
"new": "18523ce92db9b02fdc72d00999e04d4de8550d6a",
"old_patch_id": "38dcf7f0ec2a39bad2696d971a684a729b154747",
"new_patch_id": "38dcf7f0ec2a39bad2696d971a684a729b154747"
},
{
"old": "911e470b76d78e6a1ddd73c8e3e88a3ce821e43c",
"new": "7450fb71218e3736878d52bd7ce6a3a60ed531ba",
"old_patch_id": "1cf1916dab2a8552fadd07ce81246fce5f105b3d",
"new_patch_id": "1cf1916dab2a8552fadd07ce81246fce5f105b3d"
},
{
"old": "4a8b5c3ff98bfd9c31d6bfeb5356570df240e98c",
"new": "d202f6d631ccfe413621b6ef99d1c20f82578a0a",
"old_patch_id": "c93ea22b2651163be712ebb7ce96c8230ee41da8",
"new_patch_id": "c93ea22b2651163be712ebb7ce96c8230ee41da8"
},
{
"old": "e18b90e95dd928743c53d63449a82ffaffe360e8",
"new": "37c97b41d58b487f994591310383b48eec3383a9",
"old_patch_id": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3",
"new_patch_id": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3"
},
{
"old": "67776714515b562dd29a15d6a0a3bea9d642e6f2",
"new": "7bca9b042f245ff519ce58f57fbd57ce60359fbd",
"old_patch_id": "d2aecebf222474b0e06c5109b5f22caf768176ce",
"new_patch_id": "d2aecebf222474b0e06c5109b5f22caf768176ce"
},
{
"old": "c733371c5c297291b9d06c2dd2772e22670fe653",
"new": "3a2022038fa9fce043f7cf0e34b564ba3625e409",
"old_patch_id": "efb06f08af899d8f03390ab24ab65f6558cbae58",
"new_patch_id": "efb06f08af899d8f03390ab24ab65f6558cbae58"
},
{
"old": "141b79c293d78e78b9ce05bcf0fcaa18ba4d6963",
"new": "614bdc8e9b2692856972a252e4452c91990bc18b",
"old_patch_id": "e29f40a61cc88532701f0b063154a0cbe8d7ba7b",
"new_patch_id": "e29f40a61cc88532701f0b063154a0cbe8d7ba7b"
},
{
"old": "d213ef6514eb696df5c3e44eaa36d2385e2900ab",
"new": "b9d3d4c75c78aa6b06aefd734da781bef9b7f1da",
"old_patch_id": "afdf42b5115e72fb0aaaded10aed6617a1d68cd2",
"new_patch_id": "afdf42b5115e72fb0aaaded10aed6617a1d68cd2"
}
]
@@ -0,0 +1,11 @@
1: a9c094d06 = 1: 0c44ba7c4 gander: design tester socket migration for host scripts
2: 2e81f7872 = 2: a1f3b4f31 test: prepare row253 socket fixtures and fix gate collisions
3: 3c8edc358 = 3: 1ecc23eb1 docs: propose row253 C4 session supervisor and draft hop fake
4: 3114087ef = 4: 18523ce92 docs(row253): freeze C4 repair for static review
5: 911e470b7 = 5: 7450fb712 docs(row253): store repair patch without context whitespace
6: 4a8b5c3ff = 6: d202f6d63 fix(row253): supervise socket queries across sudo sessions
7: e18b90e95 = 7: 37c97b41d fix(row253): bound cleanup retries after initial inventory
8: 677767145 = 8: 7bca9b042 docs(row253): record static approval and pre-confirmation residual
9: c733371c5 = 9: 3a2022038 docs(row253): record integration onto refactor 378a76b85
10: 141b79c29 = 10: 614bdc8e9 docs(row253): integrate row191 base and refresh manifest
11: d213ef651 = 11: b9d3d4c75 docs(row253): integrate 385a base and record dependency drift and bounds
@@ -0,0 +1,29 @@
[
"packages/bus/tests/account-migration.test.mjs",
"packages/bus/tests/chief-of-staff.test.mjs",
"packages/bus/tests/comment-migration.test.mjs",
"packages/bus/tests/end-launch.test.mjs",
"packages/bus/tests/fire-revokes-grants.test.mjs",
"packages/bus/tests/gitea-grant.test.mjs",
"packages/bus/tests/gitea-migration.test.mjs",
"packages/bus/tests/grant-executor.test.mjs",
"packages/bus/tests/grant-migration.test.mjs",
"packages/bus/tests/lifecycle-revoke.test.mjs",
"packages/bus/tests/message-filter.test.mjs",
"packages/bus/tests/name-migration.test.mjs",
"packages/bus/tests/schema.test.mjs",
"packages/bus/tests/sessions.test.mjs",
"packages/bus/tests/spec-migration.test.mjs",
"packages/bus/tests/tester-writes.test.mjs",
"packages/bus/tests/thread-claims.test.mjs",
"packages/bus/tests/thread-migration.test.mjs",
"packages/bus/tests/threads.test.mjs",
"packages/bus/tests/usage-migration.test.mjs",
"packages/cli/tests/case1-accept.test.mjs",
"packages/cli/tests/case2-unit.test.mjs",
"packages/cli/tests/lifecycle.test.mjs",
"packages/cli/tests/stack-release.test.mjs",
"packages/cli/tests/thread-cli.test.mjs",
"packages/cli/tests/thread-launch.test.mjs",
"packages/webui/tests/chat.test.mjs"
]
@@ -0,0 +1,12 @@
# Row253 integration onto 378a76b85
Authority: sage-2348. New branch gander/row253-378a; gander/row253 remains at 9e916c454. Rebased range c3d88840c..9e916c454 onto 378a76b85; rebase tip before these records is 2f466464cccfe7b4f2522936a7f8409b74af7b5c.
Two conflict resolutions, both documentation:
1. First design commit, BUILD-LOG.md: kept every upstream appended line, then appended the row253 design phase. The integrated log has the complete 378a76b85 blob as its byte-identical prefix. Later row253 phase entries replayed normally. It remains outside the manifest and lands as a doc-only append after code.
2. Runnable repair commit, DEFERRED.md: kept current upstream content and appended the row276 / #1781 F5 entry. That commit had incorporated upstream changes on the old branch; the rebase correctly omits changes already on the new base. The later N1/N2 entry replayed normally.
No runtime or test conflict. All seven final manifest paths, including DEFERRED.md, are byte-identical to 9e916c454. Manifest digest remains 34d52cc500b2de855142daca4e293bbf05ff56d1c98bb94a061edabfc117e3e5. No entry changed. CANDIDATE-SHAS.json also remains valid.
range-diff.json stores the exact command and stdout. patch-ids.json gives every paired commit. Six commits compare equal; design and runnable repair differ only through BUILD-LOG context and DEFERRED upstream incorporation described above. No test or syntax execution ran. No push helper ran during Darkwing's slot. Sage publishes both own branches in the next gap. The whole gate remains held for the manifest review and CLEAR on this integrated candidate.
@@ -0,0 +1,47 @@
{
"digest": "34d52cc500b2de855142daca4e293bbf05ff56d1c98bb94a061edabfc117e3e5",
"entries": [
{
"path": "docs/plans/DEFERRED.md",
"old_sha256": "3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f",
"integrated_sha256": "3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f",
"changed": false
},
{
"path": "packages/cli/tests/case-sudo-fixture.mjs",
"old_sha256": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
"integrated_sha256": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
"changed": false
},
{
"path": "packages/cli/tests/case1-accept.test.mjs",
"old_sha256": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901",
"integrated_sha256": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901",
"changed": false
},
{
"path": "packages/cli/tests/case2-unit.test.mjs",
"old_sha256": "1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44",
"integrated_sha256": "1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44",
"changed": false
},
{
"path": "packages/webui/README.md",
"old_sha256": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9",
"integrated_sha256": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9",
"changed": false
},
{
"path": "scripts/case1-accept.sh",
"old_sha256": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"integrated_sha256": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"changed": false
},
{
"path": "scripts/case2-unit.sh",
"old_sha256": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"integrated_sha256": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
"changed": false
}
]
}
@@ -0,0 +1,66 @@
[
{
"old": "d8d7c34a7d10dffe93f3b931b8f4e3e1c4b6d983",
"new": "971681f29a347a3771802b19d1b9a65c88943a79",
"subject": "gander: design tester socket migration for host scripts",
"old_patch_id": "ee1088a4ca06de60f5a844d17d680bf33a37cd6c",
"new_patch_id": "47bdc56d349c8fcce2e5f56f3f5ce4ce7b1d6f14",
"equal": false
},
{
"old": "8a8df41715c016ea5aaf6de7307771ee2dde98f6",
"new": "43f17b9afe60bfe256e924b1317332d6068a8b4f",
"subject": "test: prepare row253 socket fixtures and fix gate collisions",
"old_patch_id": "d597cc7402b568d845a87c1f142bde74ce6ae9da",
"new_patch_id": "d597cc7402b568d845a87c1f142bde74ce6ae9da",
"equal": true
},
{
"old": "968db0bcdf96301f896873a81fa43e13d82f4a64",
"new": "f9b9112b970b634ee7c990447ded04dfe622d8cc",
"subject": "docs: propose row253 C4 session supervisor and draft hop fake",
"old_patch_id": "899327761a9bde7e7b4fefd790c1a523488e8029",
"new_patch_id": "899327761a9bde7e7b4fefd790c1a523488e8029",
"equal": true
},
{
"old": "144a7bf7c7947627b0fd5ed966b8f1a2e5431088",
"new": "70e6bb16159dd28391dbbfe0b4808f0c3dde5225",
"subject": "docs(row253): freeze C4 repair for static review",
"old_patch_id": "38dcf7f0ec2a39bad2696d971a684a729b154747",
"new_patch_id": "38dcf7f0ec2a39bad2696d971a684a729b154747",
"equal": true
},
{
"old": "b64aa316ab4fa911a4b4422dfdf39ad75ec96620",
"new": "bef1bd886b6e90f2a404825cce728e3901328ba3",
"subject": "docs(row253): store repair patch without context whitespace",
"old_patch_id": "1cf1916dab2a8552fadd07ce81246fce5f105b3d",
"new_patch_id": "1cf1916dab2a8552fadd07ce81246fce5f105b3d",
"equal": true
},
{
"old": "aa81ca390b059e36112093758484a564ce197505",
"new": "ecb15d1debdad4fbd2e9bdfedb639143467709c6",
"subject": "fix(row253): supervise socket queries across sudo sessions",
"old_patch_id": "24f6b70a08e4534381b7b4a0d59c6bf0699a3b80",
"new_patch_id": "d104bd53960b75c1e9d9402ec2ea193a55bf8164",
"equal": false
},
{
"old": "761893dac7133671b34aada788cfab91de111e5b",
"new": "39b98a26e9ac00edf3b8d77690cf06bc689a6272",
"subject": "fix(row253): bound cleanup retries after initial inventory",
"old_patch_id": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3",
"new_patch_id": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3",
"equal": true
},
{
"old": "9e916c4543912d2215d81a0a51b7f4751d36ce6d",
"new": "2f466464cccfe7b4f2522936a7f8409b74af7b5c",
"subject": "docs(row253): record static approval and pre-confirmation residual",
"old_patch_id": "d2aecebf222474b0e06c5109b5f22caf768176ce",
"new_patch_id": "d2aecebf222474b0e06c5109b5f22caf768176ce",
"equal": true
}
]
@@ -0,0 +1,9 @@
{
"command": [
"git",
"range-diff",
"c3d88840c..9e916c454",
"378a76b85..2f466464c"
],
"stdout": "1: d8d7c34a7 ! 1: 971681f29 gander: design tester socket migration for host scripts\n @@ Commit message\n \n ## BUILD-LOG.md ##\n @@ BUILD-LOG.md: Before: managed project/thread verbs had no Console conversation area. After: fi\n - 2026-10-20 19:34:28 UTC | Gander row 196 build paused at design mismatch | Before: approved design promised Unix tester cleanup only for the owned inode. After: scratch test shows Node/libuv close unlinks a replacement inode before JS close handlers. Evidence agents/gander/work/queue-196/evidence/tester-cleanup-blocker.txt. Sent Sage/Duckworth gander-0016/0017, recommending explicit same-uid residual with private 0700 parent retained. Interpreter, concurrent peer, principal, human, peer-transport, transport, tester and process single files passed. Partial code uncommitted, tester transport not wired. No suites, mutant batches or live actions.\n + 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.\n \n - 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.\n -+\n + 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.\n +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.\n \n ## agents/gander/work/queue-253/BASE-PORT-INVENTORY.json (new) ##\n2: 8a8df4171 = 2: 43f17b9af test: prepare row253 socket fixtures and fix gate collisions\n3: 968db0bcd = 3: f9b9112b9 docs: propose row253 C4 session supervisor and draft hop fake\n4: 144a7bf7c = 4: 70e6bb161 docs(row253): freeze C4 repair for static review\n5: b64aa316a = 5: bef1bd886 docs(row253): store repair patch without context whitespace\n6: aa81ca390 ! 6: ecb15d1de fix(row253): supervise socket queries across sudo sessions\n @@ Commit message\n \n ## BUILD-LOG.md ##\n @@ BUILD-LOG.md: Before: managed project/thread verbs had no Console conversation area. After: fi\n - 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 Line truncated
}
@@ -0,0 +1,29 @@
# Row253 re-integration onto 385a7194c
Authority: sage-2659. Branch gander/row253-385a preserves all previous seat branches. Source tip before this packet: 141b79c293d78e78b9ce05bcf0fcaa18ba4d6963. Ten commits replayed from 0fa7bf421..8de88458d onto 385a7194c.
The first design commit conflicted only on BUILD-LOG.md. Kept all upstream bytes and appended the row253 phase; later appends replayed. BUILD-LOG and DEFERRED keep 385a7194c as exact byte prefixes. BUILD-LOG stays outside the manifest and lands by append after code. README merged automatically; its four row253 replacement pairs are exactly the same as before. The row278 thread-claims documentation stays present. No runtime/test conflict or repair.
range-diff.txt identifies two context-only changes (commits1 and6), both BUILD-LOG context. All other pairs equal. patch-ids.json records all ten old/new pairs. The full patch-ids for commits1 and6 change because their BUILD-LOG context changed. Excluding BUILD-LOG, all ten patch-ids match. No runtime/test change.
The new seven-path manifest digest is 72a70e99ba0416ceb1f63ed737132ebbeb212332d1441c362b3dfa6c8b00209c. Only DEFERRED and README differ from8de88458d. The two host scripts, two test files and shared sudo fixture are byte-identical to Duckworth's approved candidate. manifest-drift.diff is the required 0fa7bf421..385a7194c diff restricted to the seven landing paths; upstream changed only DEFERRED(+79) and README(+9). drift.json lists every upstream changed path separately.
## Test reads and pins, source-only inspection
The focused case1/case2 tests read the host scripts and temporary fixture files. case2 also reads case2-precheck.sh and the two service templates; those three assets did not change in 0fa7bf421..385a7194c. Both tests write their own config.mjs and webui cli.mjs stand-ins into scratch copies. They do not import the real Console cli/serve/public modules, and row278's real Console changes require no focused fixture change.
The earlier dependency claim is widened here: both tests import cli/tests/helpers.mjs, which imports Store/Broker. Store reads schema.sql and the migration chain and checks schema bytes at module load, even when a particular test only calls tmp(). Store, broker, schema.sql and 3j-3k.sql changed upstream. drift.json lists the conservative literal-import closure and its nine moved modules (including business access/credentials/role/vocabulary and library mapping). The closure includes generated-source literals and is not an execution trace. case2-precheck.sh also dynamically imports the actual cli config and business index; business imports changed and the whole focused file must cover them. cli config itself did not change.
case2 imports seat/src/sandbox.mjs, as Duckworth0116 N9 identified. It did not change since0fa7, and its sandbox test still may skip on sandboxProblem(). Report that exact skip in the gate; do not assume zero. The actual sandbox binder reads the repository file inventory, so changed dependencies are covered by the whole-file run if available. No host experiment was run here.
Whole CLI covers changed format/grants/instances/lifecycle/tester/stack-release tests and real cli modules. Whole WebUI covers row278 and its existing shell fixture repair on this base. stack-release.test.mjs T46 at916 reads actual schema.sql and classifies every table; Store's schema hash and migrations are another live consumer. Whole-shell calls can load queue/business/bus consumers too. No row253 test has a literal 3k/3l version pin to update by assumption. The relevant whole-CLI schema fixture strings in stack-release-fixture.mjs model old target copies intentionally; they are not pins on the current bus schema. No assertion is changed to anticipate264a.
264a's 3k-to-3l schema change has not landed on this base. Before CLEAR, compare its store/schema/migration and any test or asset it moves, and integrate on a new branch if Sage names a new base. A seven-manifest-path-only check is insufficient because the focused import closure reads schema at load time. No result carries from the historical row253 gates after the C4 product repair.
## Gate preparation, no runs
SHELL-LIMITS.json uses the same successful whole-file receipt set as row278, plus every row278 run3 whole-shell result from385a7194c. Each limit is ceil(3*longest)+120 seconds. Foundation's newer39.901s receipt replaces39.708726s; its limit remains240. Other maxima remain the row196 receipts. All ten scripts have receipts;900 is the no-record fallback only. Each current script hash is pinned. No scripts/test-*.sh changed.
window.py now uses these whole-shell wrappers: timeout -k10 LIMIT bash scripts/test-X.sh, with the outer harness wait30s later. driver.py marks exits124/137 as timeout and never reports them as kills. These are harness-only changes; no product/test/fixture changed. No command was executed by either harness. All40 proposed string targets still occur exactly once, checked as bytes in uniqueness.json without running syntax or tests. Full C4 timing and kills remain unverified.
The next action is Duckworth's integration read, followed by Sage's CLEAR after281 and264a. No Node, test, syntax command, browser, probe, queue write, helper push or live action ran during this re-integration. Publishing the new branch needs a gap because the repo helper runs Node.
@@ -0,0 +1,385 @@
{
"old_base": "0fa7bf421",
"old_tip": "8de88458d",
"new_base": "385a7194c",
"rebased_tip": "141b79c293d78e78b9ce05bcf0fcaa18ba4d6963",
"manifest_digest": "72a70e99ba0416ceb1f63ed737132ebbeb212332d1441c362b3dfa6c8b00209c",
"manifest_paths_changed_upstream": [
"docs/plans/DEFERRED.md",
"packages/webui/README.md"
],
"candidate_blobs_changed": [
{
"path": "docs/plans/DEFERRED.md",
"before": "3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f",
"after": "8a050b494f6678030aabb7a7c92116c58ad6306ab0a27f31b368cff53c9b5d7c"
},
{
"path": "packages/webui/README.md",
"before": "9de6c8356e1f1e0ecb8d0be8ec523cb817b04bb633f0cd8aed7db32565ffd009",
"after": "ce9054060867120798c6b26737ac7d910d8cfbc7aa4ad614f4295619c7605e46"
}
],
"unchanged_runnable_manifest_paths": [
"packages/cli/tests/case-sudo-fixture.mjs",
"packages/cli/tests/case1-accept.test.mjs",
"packages/cli/tests/case2-unit.test.mjs",
"scripts/case1-accept.sh",
"scripts/case2-unit.sh"
],
"literal_import_walk": [
"packages/bus/src/broker.mjs",
"packages/bus/src/landing.mjs",
"packages/bus/src/store.mjs",
"packages/business/src/access.mjs",
"packages/business/src/business.mjs",
"packages/business/src/credentials.mjs",
"packages/business/src/errors.mjs",
"packages/business/src/index.mjs",
"packages/business/src/launch.mjs",
"packages/business/src/project.mjs",
"packages/business/src/resolve.mjs",
"packages/business/src/role.mjs",
"packages/business/src/util.mjs",
"packages/business/src/vars.mjs",
"packages/business/src/vocabulary.mjs",
"packages/business/tests/helpers.mjs",
"packages/cli/src/errors.mjs",
"packages/cli/src/transport.mjs",
"packages/cli/tests/case-sudo-fixture.mjs",
"packages/cli/tests/case1-accept.test.mjs",
"packages/cli/tests/case2-unit.test.mjs",
"packages/cli/tests/helpers.mjs",
"packages/harness/tests/helpers.mjs",
"packages/library/src/mapping.mjs",
"packages/library/src/problems.mjs",
"packages/plugins/src/kinds.mjs",
"packages/plugins/src/loader.mjs",
"packages/plugins/tests/helpers.mjs",
"packages/processing-control/src/state.mjs",
"packages/seat/src/sandbox.mjs",
"packages/tasks/src/requirements.mjs"
],
"import_edges": [
{
"source": "packages/cli/tests/case2-unit.test.mjs",
"target": "packages/business/tests/helpers.mjs"
},
{
"source": "packages/cli/tests/case2-unit.test.mjs",
"target": "packages/seat/src/sandbox.mjs"
},
{
"source": "packages/cli/tests/case2-unit.test.mjs",
"target": "packages/cli/tests/helpers.mjs"
},
{
"source": "packages/cli/tests/case2-unit.test.mjs",
"target": "packages/cli/tests/case-sudo-fixture.mjs"
},
{
"source": "packages/cli/tests/helpers.mjs",
"target": "packages/bus/src/broker.mjs"
},
{
"source": "packages/cli/tests/helpers.mjs",
"target": "packages/bus/src/store.mjs"
},
{
"source": "packages/cli/tests/helpers.mjs",
"target": "packages/business/tests/helpers.mjs"
},
{
"source": "packages/cli/tests/helpers.mjs",
"target": "packages/harness/tests/helpers.mjs"
},
{
"source": "packages/cli/tests/helpers.mjs",
"target": "packages/plugins/tests/helpers.mjs"
},
{
"source": "packages/cli/tests/helpers.mjs",
"target": "packages/cli/src/transport.mjs"
},
{
"source": "packages/cli/src/transport.mjs",
"target": "packages/cli/src/errors.mjs"
},
{
"source": "packages/harness/tests/helpers.mjs",
"target": "packages/business/src/index.mjs"
},
{
"source": "packages/harness/tests/helpers.mjs",
"target": "packages/business/tests/helpers.mjs"
},
{
"source": "packages/harness/tests/helpers.mjs",
"target": "packages/plugins/src/loader.mjs"
},
{
"source": "packages/harness/tests/helpers.mjs",
"target": "packages/business/tests/helpers.mjs"
},
{
"source": "packages/harness/tests/helpers.mjs",
"target": "packages/business/tests/helpers.mjs"
},
{
"source": "packages/plugins/src/loader.mjs",
"target": "packages/plugins/src/kinds.mjs"
},
{
"source": "packages/business/src/index.mjs",
"target": "packages/business/src/errors.mjs"
},
{
"source": "packages/business/src/index.mjs",
"target": "packages/business/src/vocabulary.mjs"
},
{
"source": "packages/business/src/index.mjs",
"target": "packages/business/src/role.mjs"
},
{
"source": "packages/business/src/index.mjs",
"target": "packages/business/src/vars.mjs"
},
{
"source": "packages/business/src/index.mjs",
"target": "packages/business/src/credentials.mjs"
},
{
"source": "packages/business/src/index.mjs",
"target": "packages/business/src/business.mjs"
},
{
"source": "packages/business/src/index.mjs",
"target": "packages/business/src/access.mjs"
},
{
"source": "packages/business/src/index.mjs",
"target": "packages/business/src/project.mjs"
},
{
"source": "packages/business/src/index.mjs",
"target": "packages/business/src/resolve.mjs"
},
{
"source": "packages/business/src/resolve.mjs",
"target": "packages/business/src/errors.mjs"
},
{
"source": "packages/business/src/resolve.mjs",
"target": "packages/business/src/vocabulary.mjs"
},
{
"source": "packages/business/src/resolve.mjs",
"target": "packages/business/src/vars.mjs"
},
{
"source": "packages/business/src/resolve.mjs",
"target": "packages/business/src/util.mjs"
},
{
"source": "packages/business/src/resolve.mjs",
"target": "packages/business/src/project.mjs"
},
{
"source": "packages/business/src/project.mjs",
"target": "packages/business/src/errors.mjs"
},
{
"source": "packages/business/src/project.mjs",
"target": "packages/business/src/vars.mjs"
},
{
"source": "packages/business/src/project.mjs",
"target": "packages/business/src/util.mjs"
},
{
"source": "packages/business/src/util.mjs",
"target": "packages/business/src/errors.mjs"
},
{
"source": "packages/business/src/util.mjs",
"target": "packages/business/src/vocabulary.mjs"
},
{
"source": "packages/business/src/vars.mjs",
"target": "packages/business/src/errors.mjs"
},
{
"source": "packages/business/src/vars.mjs",
"target": "packages/business/src/vocabulary.mjs"
},
{
"source": "packages/business/src/vars.mjs",
"target": "packages/business/src/util.mjs"
},
{
"source": "packages/business/src/vars.mjs",
"target": "packages/plugins/src/kinds.mjs"
},
{
"source": "packages/business/src/access.mjs",
"target": "packages/business/src/errors.mjs"
},
{
"source": "packages/business/src/access.mjs",
"target": "packages/business/src/business.mjs"
},
{
"source": "packages/business/src/access.mjs",
"target": "packages/business/src/util.mjs"
},
{
"source": "packages/business/src/access.mjs",
"target": "packages/business/src/vocabulary.mjs"
},
{
"source": "packages/business/src/business.mjs",
"target": "packages/business/src/errors.mjs"
},
{
"source": "packages/business/src/business.mjs",
"target": "packages/business/src/launch.mjs"
},
{
"source": "packages/business/src/business.mjs",
"target": "packages/business/src/vars.mjs"
},
{
"source": "packages/business/src/business.mjs",
"target": "packages/business/src/credentials.mjs"
},
{
"source": "packages/business/src/business.mjs",
"target": "packages/business/src/role.mjs"
},
{
"source": "packages/business/src/business.mjs",
"target": "packages/business/src/vocabulary.mjs"
},
{
"source": "packages/business/src/business.mjs",
"target": "packages/plugins/src/kinds.mjs"
},
{
"source": "packages/business/src/business.mjs",
"target": "packages/library/src/mapping.mjs"
},
{
"source": "packages/business/src/business.mjs",
"target": "packages/business/src/vocabulary.mjs"
},
{
"source": "packages/business/src/business.mjs",
"target": "packages/business/src/util.mjs"
},
{
"source": "packages/library/src/mapping.mjs",
"target": "packages/library/src/problems.mjs"
},
{
"source": "packages/business/src/role.mjs",
"target": "packages/business/src/errors.mjs"
},
{
"source": "packages/business/src/role.mjs",
"target": "packages/business/src/vocabulary.mjs"
},
{
"source": "packages/business/src/role.mjs",
"target": "packages/business/src/util.mjs"
},
{
"source": "packages/business/src/credentials.mjs",
"target": "packages/business/src/errors.mjs"
},
{
"source": "packages/business/src/credentials.mjs",
"target": "packages/business/src/vocabulary.mjs"
},
{
"source": "packages/business/src/credentials.mjs",
"target": "packages/business/src/util.mjs"
},
{
"source": "packages/business/src/launch.mjs",
"target": "packages/business/src/util.mjs"
},
{
"source": "packages/business/src/launch.mjs",
"target": "packages/business/src/errors.mjs"
},
{
"source": "packages/bus/src/broker.mjs",
"target": "packages/processing-control/src/state.mjs"
},
{
"source": "packages/bus/src/broker.mjs",
"target": "packages/plugins/src/kinds.mjs"
},
{
"source": "packages/bus/src/broker.mjs",
"target": "packages/tasks/src/requirements.mjs"
},
{
"source": "packages/bus/src/broker.mjs",
"target": "packages/bus/src/landing.mjs"
},
{
"source": "packages/bus/src/broker.mjs",
"target": "packages/business/src/vocabulary.mjs"
},
{
"source": "packages/seat/src/sandbox.mjs",
"target": "packages/business/src/vocabulary.mjs"
},
{
"source": "packages/cli/tests/case1-accept.test.mjs",
"target": "packages/cli/tests/helpers.mjs"
},
{
"source": "packages/cli/tests/case1-accept.test.mjs",
"target": "packages/cli/tests/case-sudo-fixture.mjs"
}
],
"changed_import_dependencies": [
"packages/bus/src/broker.mjs",
"packages/bus/src/store.mjs",
"packages/business/src/access.mjs",
"packages/business/src/business.mjs",
"packages/business/src/credentials.mjs",
"packages/business/src/index.mjs",
"packages/business/src/role.mjs",
"packages/business/src/vocabulary.mjs",
"packages/library/src/mapping.mjs"
],
"read_assets": [
"scripts/case1-accept.sh",
"scripts/case2-unit.sh",
"scripts/case2-precheck.sh",
"packages/cli/systemd/mosaic-bus.service.in",
"packages/webui/systemd/mosaic-webui.service.in",
"packages/bus/schema.sql",
"packages/bus/src/migrations/3b-3c.sql",
"packages/bus/src/migrations/3b.sql",
"packages/bus/src/migrations/3c-3d.sql",
"packages/bus/src/migrations/3d-3e.sql",
"packages/bus/src/migrations/3e-3f.sql",
"packages/bus/src/migrations/3f-3g.sql",
"packages/bus/src/migrations/3g-3h.sql",
"packages/bus/src/migrations/3h-3i.sql",
"packages/bus/src/migrations/3i-3j.sql",
"packages/bus/src/migrations/3j-3k.sql"
],
"changed_read_assets": [
"packages/bus/schema.sql",
"packages/bus/src/migrations/3j-3k.sql"
],
"readme_change_lines_equal": true,
"upstream_log_and_deferred_exact_prefix": true,
"dependency_limit": "Conservative static literal-import walk, including generated source strings; not runtime trace. Runtime file reads and stand-ins assessed in NOTES.md. Schema3l from264a not yet on base and must be checked before CLEAR."
}
@@ -0,0 +1,117 @@
diff --git a/docs/plans/DEFERRED.md b/docs/plans/DEFERRED.md
index 197a29ecb..4c0c736f1 100644
--- a/docs/plans/DEFERRED.md
+++ b/docs/plans/DEFERRED.md
@@ -319,6 +319,10 @@ at every gate. Started 2026-09-12 during the control board MVP.
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)
+ Closing note, row 280 (2026-10-22): both scratch sshd fixtures install a
+ bounded 30/60-second sleep stub that exits when its test directory is gone;
+ other sleep arguments exec /usr/bin/sleep unchanged. Evidence is recorded
+ in agents/rocko/work/queue-280/BUILD.md.
- **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
@@ -380,6 +384,81 @@ at every gate. Started 2026-09-12 during the control board MVP.
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)
+- **Creative instances: the human can't delete one.** (Sage's ruling
+ sage-2164 on the row 194 b4 rebase, #1692.) Row 213 put the creative
+ definitions in END_PROTECTED, and row 194's `deleteRefusal`
+ (`instance-settings.mjs`) refuses END_PROTECTED for every actor, so
+ the human can't delete a companion instance and fire refuses one too.
+ The Agents page hides Delete on the listing's `endProtected`. The fix
+ threads the actor through `deleteRefusal` and the launcher's delete
+ pass, so the human may delete one and the tester may not
+ (`creativeHumanOnly`). Until then, the human can delist
+ (instances.set launch: false) but neither fire nor delete removes a
+ creative entry (sage-2166). The row covers fire and delete together;
+ Sage files it. (2026-10-21)
+ Closed for delete by row 256 (#1761, REQ-COS-3): `deleteRefusal` takes
+ the actor, so the human deletes a creative instance and any other actor
+ is refused `organization-creative-human-only`, and the listing's
+ endProtected is the same check for its reader. Fire still refuses every
+ END_PROTECTED instance, by the row 256 brief's ruling, not by a code
+ change. (2026-10-22)
+- **A fire stuck between step A and step B has no human escape.** (Webby
+ N4 on row 194 b5, webby-0094; Sage's ruling sage-2241, #1692.) The
+ broker's chief fire records `fire.stopped` at step A, and `#firing`
+ refuses every launch and take of the instance until a `fire.done`. The
+ events are in the store, so a bus restart doesn't clear them. Row 194
+ b6 runs the writer's checks against the file before step A
+ (`instance.fireCheck`), so a keep the bus hasn't loaded no longer
+ strands an instance. A step B that keeps refusing still does: a
+ credential revoke that fails every time, or a file a human changed
+ between the check and the write. Each repeat fire runs step A again
+ and refuses at B. The fix is a human verb, never an agent one, that
+ records the end of the firing (a `fire.done`, or a new
+ `fire.abandoned` that `#firing` also counts) with a reason. Its test:
+ a fire whose step B refuses leaves the instance `instance-firing`, the
+ verb as the chief or the tester refuses, and the verb as the human
+ clears it so the instance launches again. (2026-10-21)
+- **A fire's done with more than 64 stops refuses, and nothing flags a
+ run left alive.** (Webby N2 on row 194 b8 r2, webby-0100; Sage's ruling
+ sage-2290, #1692.) `lifecycleDone` caps `stops` at 64 and refuses
+ `invalid-request` above it (`broker.mjs`). The launcher's fireStop has
+ already signalled every run through stopRun by then, so the killed runs
+ are dead. But the refused done writes no lifecycle.end.killed or
+ lifecycle.end.alive event and no lifecycle.done, and the end request
+ stays taken. A run the stop left alive, or couldn't confirm (unknown),
+ gets no Console flag. The fire itself is unaffected, because
+ `fire.stopped` and `fire.done` are written at the verb. No MVP instance
+ comes near 64 runs. The follow-up row either raises the cap or splits
+ the done. Its test: a fire's done over the cap refuses, and the runs it
+ left alive still get the alive flag. (2026-10-22)
+- **A fired or deleted instance keeps its grants.** (Dewey on the row 236
+ rebase look, dewey-0815; Mos mos-1271; Sage sage-2558.) Row 194's
+ `instance.fire` and row 256's delete revoke only the roles entry's
+ credential services through `revoke()` in `runtime.mjs`. The agent's
+ host grants (row 235) and its Gitea user and token (row 236) stay live
+ until a human runs `mosaic hosts revoke` or `mosaic grants revoke`. The
+ Console has no revoke route and doesn't say a grant is left. Row 281
+ (#1786) closes it: a fire or a delete revokes every host and Gitea grant
+ the instance holds, fail closed, with a test for each kind. It lands
+ before the .53 Case 1 install hires an instance with a grant.
+ (2026-10-22)
+
+- **The runs suite doesn't gate a change to `scripts/mosaic-task.mjs`.**
+ (Dewey N7 on row 191, repeated on the Vikunja 502 review.)
+ `packages/runs/tests/task-cli.test.mjs` spawns `scripts/mosaic-task.mjs`,
+ but row 191's gate didn't run the runs suite. test-task.sh covers the same
+ list and show paths, so nothing is known broken. Binds the next row that
+ changes `scripts/mosaic-task.mjs`: its gate runs the runs suite whole.
+ (2026-10-22)
+
+- **Row 277's M8 is killed by a rethrown error, not an assertion.**
+ (Duckworth N1 on the row 277 manifest round, duckworth-0127.) M8 makes
+ the thread.claims catch in `packages/cli/src/cli.mjs` rethrow. The stubbed
+ `processing-halted` error then escapes `main` and fails
+ `packages/cli/tests/tester.test.mjs:149`. That counts as a kill, but no
+ test pins that `main` resolves on that path. Binds the next row that
+ changes `packages/cli/tests/tester.test.mjs`: add the resolve pin so M8
+ dies on an assertion. (2026-10-22)
## Queue
diff --git a/packages/webui/README.md b/packages/webui/README.md
index 74d438c71..59cb4ee3a 100644
--- a/packages/webui/README.md
+++ b/packages/webui/README.md
@@ -382,6 +382,15 @@ there are others. The page says what each state means. It writes nothing;
the server passes the rows through unchanged. How the bus derives the state
and its limits: `packages/bus/README.md`, "Agent state".
+The same view has a separate **Thread claims** section (row 278, #1783).
+It reads `thread.claims` and shows thread id, holder run, harness and claim time,
+in the bus's order. Thread names, messages, instance and address are not shown.
+An empty read has an empty-state line. A refused or failed read clears only
+this section's kept claims and shows its reason; the held roles and their
+count stay intact. Sign-out clears the claims with the other bus reads.
+The read needs no `--send`. The bus permits human and reader sessions;
+the tester listener refuses it with HTTP 403 `tester-refused` before any bus call.
+
## Board: held roles and signing out (row 95, #1589)
The Board (`#/`) opens with **Held roles**: each role a run holds on the
Loaded 100 of 1790 files, more files were not shown because too many files have changed in this diff. Show more