Compare commits
196
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2735bb6a1 | ||
|
|
0cbe2e9b35 | ||
|
|
9538e0fc86 | ||
|
|
1a228f395f | ||
|
|
8295622600 | ||
|
|
ac0e4dc9bc | ||
|
|
c49fdb2692 | ||
|
|
cfb86f1bd7 | ||
|
|
178eefd5c4 | ||
|
|
8a29f94771 | ||
|
|
d78a9cc83c | ||
|
|
17f4157920 | ||
|
|
647424442f | ||
|
|
71fed6281c | ||
|
|
86a665bdf0 | ||
|
|
efcc9ea8dd | ||
|
|
f39508c24f | ||
|
|
d20ef317fc | ||
|
|
6e50bd6304 | ||
|
|
6c80b12904 | ||
|
|
42fd3edd45 | ||
|
|
dc5836c7df | ||
|
|
2133c5a0c6 | ||
|
|
194dec83d6 | ||
|
|
84b32e3903 | ||
|
|
9d241fdcc2 | ||
|
|
76a6524cfe | ||
|
|
693f7e745e | ||
|
|
94c33a43c0 | ||
|
|
a870fd85bb | ||
|
|
5b1f1dc885 | ||
|
|
8c638b6f66 | ||
|
|
a700992cd4 | ||
|
|
d30f35e292 | ||
|
|
e5b7d1d083 | ||
|
|
836b2f25ca | ||
|
|
73516da853 | ||
|
|
95e3f71661 | ||
|
|
cde0cd0832 | ||
|
|
100f018af3 | ||
|
|
4a8215e3a1 | ||
|
|
669e680368 | ||
|
|
c06ccc9ba4 | ||
|
|
136e5c1ca3 | ||
|
|
cf17ab387b | ||
|
|
09948da187 | ||
|
|
35615b01b7 | ||
|
|
3f1f4e44fa | ||
|
|
65b8c8a9bd | ||
|
|
271cef205c | ||
|
|
3e5db651b7 | ||
|
|
e1c9c09935 | ||
|
|
aaaf45576a | ||
|
|
cf6abca9c0 | ||
|
|
3410df7c3b | ||
|
|
1e3db53436 | ||
|
|
4dd85c6716 | ||
|
|
48e450ce8f | ||
|
|
14e4600617 | ||
|
|
a6c71e6d99 | ||
|
|
e1bc766d87 | ||
|
|
362ec674c9 | ||
|
|
f5ef1c33a3 | ||
|
|
2bef9124c4 | ||
|
|
34401e7c90 | ||
|
|
0604d91fcf | ||
|
|
ed3bb376cd | ||
|
|
cd269e7337 | ||
|
|
e75ef1b19f | ||
|
|
72b17562d9 | ||
|
|
ad67d95955 | ||
|
|
3b142c9c69 | ||
|
|
950cf47f3e | ||
|
|
cca9367168 | ||
|
|
c8cca3a688 | ||
|
|
62de848820 | ||
|
|
6f320b4b0c | ||
|
|
643d16528b | ||
|
|
730a78aa91 | ||
|
|
aca16404cf | ||
|
|
f7e4ec3df5 | ||
|
|
b789188504 | ||
|
|
84f53540dd | ||
|
|
2cc34aef87 | ||
|
|
93f4af12bc | ||
|
|
75f049ed23 | ||
|
|
54f65f9abd | ||
|
|
7451878b66 | ||
|
|
16889a46be | ||
|
|
c3f43960eb | ||
|
|
0431778465 | ||
|
|
f3e9b6512c | ||
|
|
b7a05482bb | ||
|
|
4beb6433a4 | ||
|
|
fbfaab3655 | ||
|
|
9ff668550f | ||
|
|
5baaaa447b | ||
|
|
c4a12de3d8 | ||
|
|
72fc5a330b | ||
|
|
d6301dffce | ||
|
|
ae525c4095 | ||
|
|
b5630ca198 | ||
|
|
26380e3f0b | ||
|
|
e479521d3a | ||
|
|
7979fb41eb | ||
|
|
54ebc5d71c | ||
|
|
5e05e21794 | ||
|
|
6b6eb533e8 | ||
|
|
f9fcf5d7b3 | ||
|
|
c055dddf44 | ||
|
|
6d62ef04e0 | ||
|
|
4982fa0adf | ||
|
|
7c2c876319 | ||
|
|
7d5f16ef95 | ||
|
|
a7b4f6785c | ||
|
|
938378455d | ||
|
|
5f4cd2faa6 | ||
|
|
6faa0f1479 | ||
|
|
9867a6f322 | ||
|
|
15ab01c0c3 | ||
|
|
385a7194c7 | ||
|
|
3e4c77ac60 | ||
|
|
9696d1fcb7 | ||
|
|
f99c39a3f0 | ||
|
|
42abda031f | ||
|
|
964b6b1848 | ||
|
|
0011fe357e | ||
|
|
e7d21b91b8 | ||
|
|
2c350a4669 | ||
|
|
6af85df4be | ||
|
|
04a86cf338 | ||
|
|
407c8d7be2 | ||
|
|
980cec88c2 | ||
|
|
c33cba810c | ||
|
|
01ea1fb11b | ||
|
|
b831733053 | ||
|
|
62f53b1e07 | ||
|
|
bda4ee6138 | ||
|
|
3a5275a9ad | ||
|
|
eccd6e276e | ||
|
|
571ca3d912 | ||
|
|
2f7fc6832f | ||
|
|
1d5d00939a | ||
|
|
46dedf9259 | ||
|
|
cac129e23a | ||
|
|
f12441d4fa | ||
|
|
e617543b8d | ||
|
|
d4dfc79b18 | ||
|
|
b5f1e09d6c | ||
|
|
0165be2661 | ||
|
|
6d6cc0f608 | ||
|
|
4c485c63b9 | ||
|
|
f58cfc3617 | ||
|
|
cee3f25a37 | ||
|
|
1313bfb718 | ||
|
|
618fe0f0b3 | ||
|
|
d20c92bf65 | ||
|
|
47a35bf436 | ||
|
|
6b32424adb | ||
|
|
bd5f11dedd | ||
|
|
e40b74bebc | ||
|
|
ce74477e14 | ||
|
|
4d9d62ae89 | ||
|
|
5e5ba94b19 | ||
|
|
97bbe7fd30 | ||
|
|
91f7029923 | ||
|
|
be11a1f7e6 | ||
|
|
d87483d683 | ||
|
|
be2b008cc0 | ||
|
|
ab7377bcf1 | ||
|
|
fd6dd327c3 | ||
|
|
5fd178e44d | ||
|
|
345f3ace67 | ||
|
|
54ec93d06f | ||
|
|
4f68485272 | ||
|
|
c48239ec3d | ||
|
|
5d2c274420 | ||
|
|
9e5ad7c599 | ||
|
|
939356a1c2 | ||
|
|
3521c23c3e | ||
|
|
2311a50e1c | ||
|
|
29daebf548 | ||
|
|
2f08fd8f23 | ||
|
|
b8c3d00362 | ||
|
|
14124d96d4 | ||
|
|
e7fb59f225 | ||
|
|
e122aad8e2 | ||
|
|
4c4b8559cf | ||
|
|
b0fd28b6f3 | ||
|
|
b765f5cb81 | ||
|
|
da5d3651d7 | ||
|
|
82c8281166 | ||
|
|
ceadad993c | ||
|
|
f5611c937e | ||
|
|
343b6dd766 | ||
|
|
f40f2e770e |
No files matched your search
@@ -5279,3 +5279,66 @@ Before: managed project/thread verbs had no Console conversation area. After: fi
|
||||
2026-10-22 05:07:34 UTC | Rocko (d99326cc) | Vikunja502 committed/review handoff sage-2482 | newbranchrocko/vikunja502-ce6a-20261022,source8e3c6a79b+packet9bf4683c6 on ce6a96ad6;5paths no base drift,range-diff=,patch-id37135ebe unchanged;manifest968d6ee3,packet93889519 verifies39/39;focused runs carry;Sage publish/Dewey review handoff sent,no push/liveaction.
|
||||
|
||||
2026-10-22 05:10:00 UTC | Sage (1ef1e4f8) | serial landing 205 then 268 | 205: 3 commits, 4 paths, digest 858463b1, every scripts/test-*.sh green on ce6a96ad6, Duckworth N1-N4 carried (N1/N3 to 205b or R3, N2 to the 193b2 brief, N4 #1782 before any user thread goes live). 268 r4: c27207f6e on 205, 9 paths, digest caa0fa4e, patch-id 2942acc8 equal to r3. Overlap checks clean before both pushes. No live action; LIVE stays 86f95bb27.
|
||||
|
||||
2026-10-22 05:58:59 UTC | Rocko | row 279 #1784: before, deadline overshoot skipped final observation and returned unknown; after, one-line for (;;) retains final mapping and unchanged unreachable fallback. Regression red as specified; whole stop-run 13/13 and processing-control 62/62; M1 assertion kill, no skips, restored and swept. Source 1016b8584, packet cad927137; manifest 312ed511d8f1cff826ee2fd547de8368923130c7bfe06236d8e29d8655b80aaa. Pending Sage publication and Dewey review; no live action.
|
||||
|
||||
2026-10-22 06:15:54 UTC | Sage (1ef1e4f8) | row 279 #1784 landed (mos-1269) | Before: stopRun returned unknown for a live run when a sleep overshot the deadline. After: LANDED ceadad993 as a carry of cad927137 onto 343b6dd76 (range-diff 1:= 2:=, patch-ids 6d8ae7f4/e484c297 equal, Dewey carry c30410). Sage gate on cad927137: business 209/0/0, cli 794/0/0, discord processing + processing-stop 9/0/0. Correction: the first discord run wrote to a wrong out path and ran nothing; the rerun is the result. Overlap sweep clean (13, 257). Note rev 3144, done rev 3145. No live action; LIVE stays 86f95bb27.
|
||||
|
||||
2026-10-22 06:37:43 UTC | Sage (1ef1e4f8) | row 194 b16 third red (mos-1269/mos-1270) | Gate on ceadad993 + 9cdc47d04: business 228/0, cli 826/0 (T9a passes), control-board 124/0, bus 705/1 at grant-executor.test.mjs G13 (timed out 10044 ms); conversation, webui and scripts/test-*.sh never ran. runb8 green 436/0/0, 17/17 mutants. Correction to sage-2524 (mos-1270): the stuck wait is the first until at :733, the holding file after decision.resolve, so the executor never started the install and hosts.revoke at :734 never ran; it is not a revoke-path hang. Ruling: a diagnosis run, 30 base + 30 candidate alternating under cli load, no gate retry.
|
||||
|
||||
2026-10-22 07:51:04 UTC | Rocko | row 280 #1785: before, any TCP connect could accept a port our sshd failed to bind; after, attempt-local exact log proof plus liveness and connect, non-ephemeral picker, bounded scratch remote sleep cleanup. Both occupied regressions red on old acceptance; CRLF build failure captured and fixed under sage-2535. Committed source b765f5cb8, packet d6c924764, manifest27806c0579c76d0999d2e6be4180c3867570df0268af9d7e0d7b971af799e6f5. Repeats07:29:03Z–07:48:38Z grant1230/1230,host780/780,CLI1588/1588; final M1/M2 four assertion kills,0skips/timeouts,sweepclear. Sage publishes/gates; Duckworth reviews.
|
||||
2026-10-22 08:00:02 UTC | Sage (1ef1e4f8) | row 280 #1785 landed | before: the bus tests' scratch sshd helper returned a port on a connect alone, and 194 b16's gate hung at G13 (base hung too, honker-0127). After: b0fd28b6f on refactor, a ff from da5d3651d (b765f5cb8 + Rocko slim packet; full evidence d6c924764 on rocko/row280-da5d-20261022). Duckworth approve rev 3153 (manifest 27806c05). Sage gate bus 656/0/0, ge 41/0/0, hr 26/0/0 (c30431). Rocko repeat 2010/2010 under cli 1588/1588, no timeout; M1/M2 killed 4/4. Two build-time slips recorded in Rocko's packet: a runner root path (no test ran) and the CRLF sshd log line. Overlap 13/257 clean. Note rev 3154, done 3155, #1785 closed c30432, P19 511 comment 623. 194 b17 rebases on b0fd28b6f (sage-2542).
|
||||
|
||||
2026-10-22 08:38:11 UTC | Sage (1ef1e4f8) | row 194 #1692 landed | before: 194 b16 9cdc47d04 went red three times (stopRun deadline, then the bus test sshd port race), fixed by rows 279 and 280. After: fd6dd327c on refactor, a ff on e122aad8e. b18 is b17 3f451772d rebased after my doc and queue push blocked a ff (16 =, series patch-id 64303253f8e6, packages equal b17). Webby approve rev 3160 on manifest 29f131ab. Honker gate on fd6dd327c: runb8 503/0/0, mutants 17/17; 18 packages 3668/0, ten scripts rc=0, Docker up, env skips named. The secret scan hit three fake_*_token fixtures in bus tests (false positive). Note 3161, done 3162; #1692 closed c30435; P19 414 comment 625. LIVE still 86f95bb27.
|
||||
2026-10-22 08:42:04 UTC | Honker (Claude Opus 5.5, ecd17786) | row 194 #1692 agent lifecycle build | before: no Console or CLI lifecycle for agents (create, launch, terminate, regenerate, delete) and no per-agent hire/fire veto. After: 16 commits landed as fd6dd327c (b18, a rebase of b17 3f451772d on e122aad8e: 16 =, series patch-id 64303253f8e6, packages equal b17). Manifest 53 paths, digest 29f131ab. Final run on fd6dd327c: runb8 39 files whole 503/0/0, mutants 17/17 killed, restores matched; gate.sh merge onto e122aad8e, 18 packages 3668/0, ten scripts rc=0, Docker up, four env skips named (webui esbuild pin, queue render outside canonical root, test-task live provider x2); sweep clean. Corrections: b16 went red at G13; my diagnosis (60 runs, base hang on port 38927 sshd bind EADDRINUSE) placed it outside 194, fixed by row 280; the g13diag load kill missed its process group and overran about 8 minutes; one runb8 launch failed rc=126 on a lost exec bit before any test ran; the b17 runb8 was stopped unscored when refactor moved. In-review revs 3156/3157, Webby approve rev 3160.
|
||||
2026-10-22 09:43:34 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 236 #1742 Gitea grant landing, and row 281 filing | 236 landed bd5f11ded at 09:37:31Z, a fast-forward on be2b008cc: 08840f3c1 rebased past 194 (commit 1 unions only, 2-7 =), e40b74beb for two stale out-of-manifest pins, bd5f11ded for a gitea-not-configured raise test. Dewey approve rev 3168 on manifest 0b00269b (34 paths). Megavolt gate under my slot: 2c bus 131, cli 224, route 28, docker 1; whole bus 765, business 228, cli 832, ten scripts rc=0; mutants 40/40; skips named, env only. Overlap with 13 and 257 clean. Note 3169, done 3170; #1742 closed c30451; P19 436 comment 639. Correction: the first whole run on ce74477e1 went red 762/764 on the two stale pins; that run is void. Follow-up per mos-1271: row 281 (#1786), a fire or delete revokes host and Gitea grants, Megavolt owns, Dewey reviews; brief docs/plans/2026-10-22_fire-revokes-grants.md; DEFERRED entry points at it.
|
||||
2026-10-22 10:30:51 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 274 #1779 FED R3 broker user principal landing | 274 landed 4c485c63b at 10:28:26Z, a fast-forward on 47a35bf43: six Morgana commits d20c92bf6..4c485c63b. r5 f58cfc361 moved covers, LEVELS and the all marker into business/src/vocabulary.mjs so the broker keeps its sandbox import closure (M16/M16b/M16c). r6 4c485c63b pins USER_VERBS as the ruled seven and widens the R3 off-list set. Dewey approve rev 3178 on manifest 00047b52 (13 paths), comment 30487. Morgana gate under my slot, 10:09-10:22Z: 23 touched files 0 fail, bus 787, harness 151, business 228, cli 832, seat 167, ten scripts rc=0, mutants 22/22; skips named, env only (test-queue 1, test-task 2). Overlap with 13 and 257 clean. Note 3179, done 3180; #1779 closed c30491; P19 414 comment 646. Corrections: the r5 gate on f58cfc361 went red (M3x/M6x survived the R3 off-list set), void; my gate comment 30490 said 24 touched files, the count is 23; the kit comment guessed human-required for the M3x wall, the run code is not-holder.
|
||||
2026-10-22 10:57:34 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 256 #1761 the human deletes a creative instance, landing | before: row 194's deleteRefusal refused every END_PROTECTED definition for every actor, so nobody could delete a creative instance; the human could only delist one with launch: false. After: 256 landed f12441d4f, a fast-forward on 0165be266 (the 274 landing plus its docs and queue commits): four Honker commits b5f1e09d6..f12441d4f, patch-id b28e63746f8c. One protectedRefusal predicate serves delete and the listing's per-viewer endProtected; any non-human actor gets organization-creative-human-only, fire unchanged. Rebased twice (past 236, then past 274), each range-diff 4/4 =, carries checked by Webby (webby-0126, webby-0128). Webby APPROVE on manifest-r3 01eb75aa (10 paths), comment 30519, posted by Sage as jarvis because Webby has no token; review request c30518 (Sage, revs 3183/3184) after Honker's request failed (no token, revs 3181/3182). Gate by Honker under Sage's slot 10:33Z-10:52Z on merge tree ced6bf5f8: touched files 31/0/0, 18 packages 3757/0 (bus 788, business 231, cli 832, webui 335), ten scripts/test-*.sh rc=0, mutants M1, M2, M3, M4a, M4b killed with blob-checked restores. Skips, named and environment-only: webui editor rebuild byte check (esbuild 0.28.1 installed, pin 0.28.2), test-queue non-canonical root, test-task live provider x2. Overlap with in-review rows 13 and 257 clean. Order change sage-2594: 256 landed before 277 because 277's gate had not run. LIVE is still 86f95bb27. Queue note rev 3186, done rev 3187; #1761 closed c30520; P19 416 comment 649.
|
||||
2026-10-22 11:19:28 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 277 #1782 the owner sees a thread agent's claims, landing | before: a thread run's role.claim wrote thread_claims only, and the agents verb read role_claims only, so the owner had no way to see a thread agent's claim (PRD :1083-1084). After: 277 landed 62f53b1e0, a fast-forward on 46dedf925 (the 256 landing plus its docs and queue commits): seven Morgana commits 1d5d00939..62f53b1e0, range patch-id 7f0533520bd2. A bus verb thread.claims serves human and reader sessions; mosaic agents adds thread lines without --tester or --json, --json keeps the agents array, the tester path makes one call, a refused thread.claims prints the role lines and the refusal code, and an error with no bus code still throws. Rebased onto 0165be266, then onto 46dedf925 after 256 landed first (sage-2594); both range-diffs all =, manifest e281aa2c unchanged (duckworth-0126). Duckworth APPROVE comment 30539, recorded rev 3192, posted by Sage as jarvis; review request c30538 (revs 3190/3191) after Morgana's request failed (no token, revs 3188/3189). Gate by Morgana under Sage's slot 11:01:23Z-11:13:56Z: 7 touched files 0 fail (thread-claims 5, message-filter 6, user-principal 22, format 4, tester 11, other-tree 4, host 43), bus 793, cli 836, harness 151, business 231, seat 167, ten scripts/test-*.sh rc=0, M1-M9 killed on committed bytes. Skips named, environment only: test-queue non-canonical root, test-task live provider x2. Overlap with in-review 13 and 257 clean. Duckworth N1 (M8 dies on a rethrown error) to DEFERRED; 0123 N3 per-session claims binds 264a r7. Note rev 3193, done rev 3194; #1782 closed c30540; P19 506 comment 652. Also landed as doc-only picks: Honker's 256 BUILD.md gate results (80620a3a0 as b83173305) and Megavolt's 236 BUILD.md runs (9c6e1a165 as 01ea1fb11). LIVE is still 86f95bb27.
|
||||
|
||||
2026-10-22T11:37:16Z | Gander row278 #1783 implementation prepared, verification held | Before: owner thread claims were visible through row277 but absent from the Console Agents view. After: prepared a separate four-field table, a Console-owned read adapter, section-local refusal handling and an explicit tester refusal before busRead. Ten tests and six unique check definitions are prepared but unrun. Base moved from980cec88c to6af85df4b under sage-2625 on new gander/row278-b; no candidate path overlapped. No Node process, syntax check, test, browser, queue command, push helper or live action ran. Next: Sage CLEAR for the queue claim and gate; Webby reviews the verified manifest.
|
||||
|
||||
2026-10-22T12:09:45.285844+00:00 | Gander row278 run1 void, loader fake repaired | First new-file run stopped3/1/0 because the fake omitted BusReadError imported by base chat.mjs. Sage-2638 places the first-own-test red and permits the assertion-preserving export; test-only commit964b6b184. Runtime bytes unchanged. All run1 results void, no timeout or run leftovers. Run2 must restart every step after busy CLEAR; a second red goes to Sage then Mos before any change. No product acceptance or push claimed.
|
||||
|
||||
### 2026-10-22 | Gander | row278 gate run3
|
||||
|
||||
Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version skip), all ten shell files425/0, touched files15/0, M1-M6 killed6/6. No timeout; sweep0 run-created leftovers. Runs1/2 void. Seven-path manifest8fa859a0 goes to Webby for final review. No landing or live change.
|
||||
|
||||
2026-10-22 11:29:09 UTC | Rocko | V502 carry/rerun on980cec88c: source407c8d7be and packet04a86cf33 match approved8e3c6a79b/9bf4683c6 patch-ids, no conflicts. Dewey0834 carry conditioned on rerun, satisfied under sage2616: stop-run13,processingHTTP6,bustasks9 allgreen (Node28/28),Python4/4,0skips/timeouts. Manifest5758d699,source5/5restored,cwdsweep[]. Doc rerun packet6af85df4b explicitly pushed to rocko/vikunja502-980c-20261022; no live action.
|
||||
|
||||
2026-10-22 11:30:17 UTC | Sage | Vikunja 502 (row 191 follow-up, no queue row) LANDED 6af85df4b, a fast-forward from 980cec88c. Before: the processing failure diagnostics and cleanup in stop-run, processing HTTP and bus tasks tests were Dewey-approved on the pre-194 base only (8e3c6a79b/9bf4683c6). After: Rocko's picks 407c8d7be/04a86cf33 carry the same patch-ids (37135ebe/0dbcda54), plus a doc-only rerun record 6af85df4b. The rerun on the new base was green: stop-run 13/13, processing HTTP 6/6, bus tasks 9/9, Python 4/4, 0 skips. Dewey carried the approve (dewey-0836). mosaic-task.mjs is unchanged, so N7 (runs suite) didn't trigger. Overlap with in-review rows 13 and 257 was clean. LIVE is still 86f95bb27.
|
||||
|
||||
2026-10-22 13:10 UTC | Sage | Rows 278 and 281 LANDED. Before: refactor at 6af85df4b (V502). Row 278 (#1783, Gander, Webby): the Console Agents view shows thread claims in their own section. It landed as 385a7194c at 12:39Z, ff, 9 commits, manifest 8fa859a0. Run 3 was green: bus 793, webui 345/0/1, ten shells 425/0 under the 3x limits (mos-1275), M1-M6 killed. Runs 1 and 2 were void, on fixture fixes 964b6b184 (placed by Sage) and 9696d1fcb (ruled by Mos, mos-1275). Webby approved (c30588, rev 3200). Done rev 3202, #1783 closed c30589. Row 281 (#1786, Megavolt, Dewey): a fire or a delete revokes the instance's host and Gitea grants. It landed as e479521d3 at 13:06Z, ff, 17 commits, manifest b73d4c81. Slot on 7979fb41e: bus 819, business 231, cli 837, dewey-bus 76 with the Docker gitea-grant test, shells green, M1-M22 killed. The M1 hang was fixed at its cause in b4a (mos-1273). Dewey approved (c30614, rev 3207). Done rev 3209, #1786 closed c30621. Skips on both: test-task's row 244 live-provider pair and test-queue's canonical-root check, plus webui's esbuild rebuild test on 278. After: refactor at e479521d3. DEFERRED moves the grant gap to Done and opens Dewey's N1/N2 text gaps. LIVE is still 86f95bb27.
|
||||
|
||||
2026-10-22 14:18:23 UTC | Sage | Row 264a LANDED (#1769, Gizmoduck, Duckworth): sessions per thread, schema 3k-3l. Before: refactor at e479521d3 (row 281). It landed as 84f53540d, ff, 21 commits, manifest 257408e9 (Duckworth approve rev 3214, comment 30640). Gate r13 on 2cc34aef8: 20 touched files, bus 827, cli 837, webui 345/1 named skip, ten shells under 3x limits, mutants M1-M4 killed on assertions. Corrections on the way: the kit stopped on first red, so four reds surfaced one at a time. r10 later3l missed a trigger (mos-1276). T2 had failed since r2 because refusals log action.refused (mos-1277, my sage-2257 wording). threads.test:341 kept the old trigger text (mos-1278). M2 was caught by the store trigger before any assertion, so it now drops the trigger too (mos-1279, four files with the SCHEMA_SHA256 pin). Deferred: actor_run pin binds row 269, esbuild skip to a later gate, session-scoped user reply binds row 272. No live change; LIVE is still 86f95bb27.
|
||||
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.
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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');"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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")"`
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,82 @@
|
||||
[
|
||||
{
|
||||
"old": "39c30f1c200a1f5f27767dae4b730cade7703777",
|
||||
"new": "a9c094d0657a52c22ba51b35e690868358cf2bd0",
|
||||
"old_patch_id": "895656745186c2dc4cb1ee1261c323af90bf9fb9",
|
||||
"new_patch_id": "2b17f1d6773aa17909fc959081eec3f3821c3dee",
|
||||
"old_without_build_log": "86a00d372cedf03f5a8c573f48065de11bbbff29",
|
||||
"new_without_build_log": "86a00d372cedf03f5a8c573f48065de11bbbff29"
|
||||
},
|
||||
{
|
||||
"old": "d004b44982db5196777710e20163f877b07bf54d",
|
||||
"new": "2e81f78722ccd43d7524bb6a7d27d7ac26d0cea8",
|
||||
"old_patch_id": "d597cc7402b568d845a87c1f142bde74ce6ae9da",
|
||||
"new_patch_id": "d597cc7402b568d845a87c1f142bde74ce6ae9da",
|
||||
"old_without_build_log": "d597cc7402b568d845a87c1f142bde74ce6ae9da",
|
||||
"new_without_build_log": "d597cc7402b568d845a87c1f142bde74ce6ae9da"
|
||||
},
|
||||
{
|
||||
"old": "9e4bbee25d3a97e9dfec743d92c20f50154222da",
|
||||
"new": "3c8edc358c1f30ea7b5cc9a79d30282e59092715",
|
||||
"old_patch_id": "899327761a9bde7e7b4fefd790c1a523488e8029",
|
||||
"new_patch_id": "899327761a9bde7e7b4fefd790c1a523488e8029",
|
||||
"old_without_build_log": "899327761a9bde7e7b4fefd790c1a523488e8029",
|
||||
"new_without_build_log": "899327761a9bde7e7b4fefd790c1a523488e8029"
|
||||
},
|
||||
{
|
||||
"old": "69474277277fe87e7c398c508f6450aa0d691de7",
|
||||
"new": "3114087efe655f1d38c5e30f8327f65980854043",
|
||||
"old_patch_id": "38dcf7f0ec2a39bad2696d971a684a729b154747",
|
||||
"new_patch_id": "38dcf7f0ec2a39bad2696d971a684a729b154747",
|
||||
"old_without_build_log": "38dcf7f0ec2a39bad2696d971a684a729b154747",
|
||||
"new_without_build_log": "38dcf7f0ec2a39bad2696d971a684a729b154747"
|
||||
},
|
||||
{
|
||||
"old": "d85cf93d64a4ebf37865cbb74719d842105a1864",
|
||||
"new": "911e470b76d78e6a1ddd73c8e3e88a3ce821e43c",
|
||||
"old_patch_id": "1cf1916dab2a8552fadd07ce81246fce5f105b3d",
|
||||
"new_patch_id": "1cf1916dab2a8552fadd07ce81246fce5f105b3d",
|
||||
"old_without_build_log": "1cf1916dab2a8552fadd07ce81246fce5f105b3d",
|
||||
"new_without_build_log": "1cf1916dab2a8552fadd07ce81246fce5f105b3d"
|
||||
},
|
||||
{
|
||||
"old": "463a97f49f6bcaa65a10a54c2ad40e428e7fd135",
|
||||
"new": "4a8b5c3ff98bfd9c31d6bfeb5356570df240e98c",
|
||||
"old_patch_id": "08a16c862148ddae13044130cac740064d0e86fe",
|
||||
"new_patch_id": "c93ea22b2651163be712ebb7ce96c8230ee41da8",
|
||||
"old_without_build_log": "197948a3ddc789f46161ad573d52956990dbfcfb",
|
||||
"new_without_build_log": "197948a3ddc789f46161ad573d52956990dbfcfb"
|
||||
},
|
||||
{
|
||||
"old": "de93d8e038f591ea7be2f850c5fb0dea272c3d63",
|
||||
"new": "e18b90e95dd928743c53d63449a82ffaffe360e8",
|
||||
"old_patch_id": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3",
|
||||
"new_patch_id": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3",
|
||||
"old_without_build_log": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3",
|
||||
"new_without_build_log": "aee9e5492bf07ee71687fd899c4e632bb36bc1e3"
|
||||
},
|
||||
{
|
||||
"old": "36bd3c3a8bda8c31da8219cc726ce005ec2dbd2b",
|
||||
"new": "67776714515b562dd29a15d6a0a3bea9d642e6f2",
|
||||
"old_patch_id": "d2aecebf222474b0e06c5109b5f22caf768176ce",
|
||||
"new_patch_id": "d2aecebf222474b0e06c5109b5f22caf768176ce",
|
||||
"old_without_build_log": "d2aecebf222474b0e06c5109b5f22caf768176ce",
|
||||
"new_without_build_log": "d2aecebf222474b0e06c5109b5f22caf768176ce"
|
||||
},
|
||||
{
|
||||
"old": "67224933425e9687bf366cd7ba7c31c1daa4e9f3",
|
||||
"new": "c733371c5c297291b9d06c2dd2772e22670fe653",
|
||||
"old_patch_id": "efb06f08af899d8f03390ab24ab65f6558cbae58",
|
||||
"new_patch_id": "efb06f08af899d8f03390ab24ab65f6558cbae58",
|
||||
"old_without_build_log": "efb06f08af899d8f03390ab24ab65f6558cbae58",
|
||||
"new_without_build_log": "efb06f08af899d8f03390ab24ab65f6558cbae58"
|
||||
},
|
||||
{
|
||||
"old": "8de88458d2c8bef57c8af51a1e4d459cf8feb8d9",
|
||||
"new": "141b79c293d78e78b9ce05bcf0fcaa18ba4d6963",
|
||||
"old_patch_id": "e29f40a61cc88532701f0b063154a0cbe8d7ba7b",
|
||||
"new_patch_id": "e29f40a61cc88532701f0b063154a0cbe8d7ba7b",
|
||||
"old_without_build_log": "e29f40a61cc88532701f0b063154a0cbe8d7ba7b",
|
||||
"new_without_build_log": "e29f40a61cc88532701f0b063154a0cbe8d7ba7b"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
1: 39c30f1c2 ! 1: a9c094d06 gander: design tester socket migration for host scripts
|
||||
@@ Commit message
|
||||
|
||||
## BUILD-LOG.md ##
|
||||
@@ BUILD-LOG.md: Before: managed project/thread verbs had no Console conversation area. After: fi
|
||||
- 2026-10-22 02:01:30 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 202 #1700 FED R5 landing | ddfb29af7 on 9c9732983: 6 manifest paths from a74cd9f87 (digests match CANDIDATE.sha256, patch-id 9d89077b equal); Duckworth APPROVE r5 rev 3026; C1 gate by Quackerjack on merge 3b86180b9: cli 771/771 skip 0 (run 2, run 1 red on gate TMPDIR length), 10 test-*.sh rc 0, M1-M13 killed; done rev 3108; landing note refused for length before the done move, so the record is the commit body and #1700 c30265; 191 and 194 rebased onto it (one scripts/mosaic case line)
|
||||
+ ### 2026-10-22 | Gander | row278 gate run3
|
||||
|
||||
- 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).
|
||||
+ Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version skip), all ten shell files425/0, touched files15/0, M1-M6 killed6/6. No timeout; sweep0 run-created leftovers. Runs1/2 void. Seven-path manifest8fa859a0 goes to Webby for final review. No landing or live change.
|
||||
+2026-10-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.
|
||||
|
||||
## agents/gander/work/queue-253/BASE-PORT-INVENTORY.json (new) ##
|
||||
2: d004b4498 = 2: 2e81f7872 test: prepare row253 socket fixtures and fix gate collisions
|
||||
3: 9e4bbee25 = 3: 3c8edc358 docs: propose row253 C4 session supervisor and draft hop fake
|
||||
4: 694742772 = 4: 3114087ef docs(row253): freeze C4 repair for static review
|
||||
5: d85cf93d6 = 5: 911e470b7 docs(row253): store repair patch without context whitespace
|
||||
6: 463a97f49 ! 6: 4a8b5c3ff fix(row253): supervise socket queries across sudo sessions
|
||||
@@ Commit message
|
||||
## BUILD-LOG.md ##
|
||||
@@ BUILD-LOG.md: Before: managed project/thread verbs had no Console conversation area. After: fi
|
||||
|
||||
- 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).
|
||||
+ Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version skip), all ten shell files425/0, touched files15/0, M1-M6 killed6/6. No timeout; sweep0 run-created leftovers. Runs1/2 void. Seven-path manifest8fa859a0 goes to Webby for final review. No landing or live change.
|
||||
2026-10-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
|
||||
7: de93d8e03 = 7: e18b90e95 fix(row253): bound cleanup retries after initial inventory
|
||||
8: 36bd3c3a8 = 8: 677767145 docs(row253): record static approval and pre-confirmation residual
|
||||
9: 672249334 = 9: c733371c5 docs(row253): record integration onto refactor 378a76b85
|
||||
10: 8de88458d = 10: 141b79c29 docs(row253): integrate row191 base and refresh manifest
|
||||
@@ -0,0 +1,242 @@
|
||||
[
|
||||
{
|
||||
"id": "M1",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M2",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M3",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M4",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M5",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M6",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M7",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M8",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M9",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M10",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M11",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M12",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M13",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M14",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M15",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M16",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M17",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M18",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M19",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M20",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M21",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M22",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M23",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M24",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M25",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M26",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M27",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"occurrences": 1,
|
||||
"before": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd"
|
||||
},
|
||||
{
|
||||
"id": "M28",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M29",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M30",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M31",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M32",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M33",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M34",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M35",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M36",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M37",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M38",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M39",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
},
|
||||
{
|
||||
"id": "M40",
|
||||
"file": "scripts/case2-unit.sh",
|
||||
"occurrences": 1,
|
||||
"before": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,417 @@
|
||||
[
|
||||
"BUILD-LOG.md",
|
||||
"agents/gander/work/queue-278/BASE-DRIFT.json",
|
||||
"agents/gander/work/queue-278/BUILD.md",
|
||||
"agents/gander/work/queue-278/CANDIDATE-SHAS.json",
|
||||
"agents/gander/work/queue-278/CANDIDATE.patch",
|
||||
"agents/gander/work/queue-278/MANIFEST.md",
|
||||
"agents/gander/work/queue-278/MANIFEST.sha256",
|
||||
"agents/gander/work/queue-278/SHELL-LIMITS.json",
|
||||
"agents/gander/work/queue-278/SOURCE.json",
|
||||
"agents/gander/work/queue-278/UNIQUENESS.json",
|
||||
"agents/gander/work/queue-278/changes.json",
|
||||
"agents/gander/work/queue-278/checks.py",
|
||||
"agents/gander/work/queue-278/launch.py",
|
||||
"agents/gander/work/queue-278/manifest-check.txt",
|
||||
"agents/gander/work/queue-278/report.py",
|
||||
"agents/gander/work/queue-278/run1/SUMMARY.json",
|
||||
"agents/gander/work/queue-278/run1/claim.log",
|
||||
"agents/gander/work/queue-278/run1/focused-controller.log",
|
||||
"agents/gander/work/queue-278/run1/focused/focused-0.command.json",
|
||||
"agents/gander/work/queue-278/run1/focused/focused-0.log",
|
||||
"agents/gander/work/queue-278/run1/focused/results.json",
|
||||
"agents/gander/work/queue-278/run1/red-controller.log",
|
||||
"agents/gander/work/queue-278/run1/red/red-0.command.json",
|
||||
"agents/gander/work/queue-278/run1/red/red-0.log",
|
||||
"agents/gander/work/queue-278/run1/red/red-1.command.json",
|
||||
"agents/gander/work/queue-278/run1/red/red-1.log",
|
||||
"agents/gander/work/queue-278/run1/red/results.json",
|
||||
"agents/gander/work/queue-278/run1/start.txt",
|
||||
"agents/gander/work/queue-278/run1/sweep-initial.json",
|
||||
"agents/gander/work/queue-278/run1/sweep.json",
|
||||
"agents/gander/work/queue-278/run1/syntax-controller.log",
|
||||
"agents/gander/work/queue-278/run1/syntax/results.json",
|
||||
"agents/gander/work/queue-278/run1/syntax/syntax-0.command.json",
|
||||
"agents/gander/work/queue-278/run1/syntax/syntax-0.log",
|
||||
"agents/gander/work/queue-278/run1/syntax/syntax-1.command.json",
|
||||
"agents/gander/work/queue-278/run1/syntax/syntax-1.log",
|
||||
"agents/gander/work/queue-278/run1/syntax/syntax-2.command.json",
|
||||
"agents/gander/work/queue-278/run1/syntax/syntax-2.log",
|
||||
"agents/gander/work/queue-278/run1/syntax/syntax-3.command.json",
|
||||
"agents/gander/work/queue-278/run1/syntax/syntax-3.log",
|
||||
"agents/gander/work/queue-278/run1/syntax/syntax-4.command.json",
|
||||
"agents/gander/work/queue-278/run1/syntax/syntax-4.log",
|
||||
"agents/gander/work/queue-278/run1/tmpdir.txt",
|
||||
"agents/gander/work/queue-278/run2/KILL-TABLE.md",
|
||||
"agents/gander/work/queue-278/run2/SUMMARY.json",
|
||||
"agents/gander/work/queue-278/run2/changes-controller.log",
|
||||
"agents/gander/work/queue-278/run2/changes/M1-base.command.json",
|
||||
"agents/gander/work/queue-278/run2/changes/M1-base.log",
|
||||
"agents/gander/work/queue-278/run2/changes/M1.command.json",
|
||||
"agents/gander/work/queue-278/run2/changes/M1.log",
|
||||
"agents/gander/work/queue-278/run2/changes/M2.command.json",
|
||||
"agents/gander/work/queue-278/run2/changes/M2.log",
|
||||
"agents/gander/work/queue-278/run2/changes/M3-base.command.json",
|
||||
"agents/gander/work/queue-278/run2/changes/M3-base.log",
|
||||
"agents/gander/work/queue-278/run2/changes/M3.command.json",
|
||||
"agents/gander/work/queue-278/run2/changes/M3.log",
|
||||
"agents/gander/work/queue-278/run2/changes/M4-base.command.json",
|
||||
"agents/gander/work/queue-278/run2/changes/M4-base.log",
|
||||
"agents/gander/work/queue-278/run2/changes/M4.command.json",
|
||||
"agents/gander/work/queue-278/run2/changes/M4.log",
|
||||
"agents/gander/work/queue-278/run2/changes/M5-base.command.json",
|
||||
"agents/gander/work/queue-278/run2/changes/M5-base.log",
|
||||
"agents/gander/work/queue-278/run2/changes/M5.command.json",
|
||||
"agents/gander/work/queue-278/run2/changes/M5.log",
|
||||
"agents/gander/work/queue-278/run2/changes/M6.command.json",
|
||||
"agents/gander/work/queue-278/run2/changes/M6.log",
|
||||
"agents/gander/work/queue-278/run2/changes/results.json",
|
||||
"agents/gander/work/queue-278/run2/focused-controller.log",
|
||||
"agents/gander/work/queue-278/run2/focused/focused-0.command.json",
|
||||
"agents/gander/work/queue-278/run2/focused/focused-0.log",
|
||||
"agents/gander/work/queue-278/run2/focused/focused-1.command.json",
|
||||
"agents/gander/work/queue-278/run2/focused/focused-1.log",
|
||||
"agents/gander/work/queue-278/run2/focused/results.json",
|
||||
"agents/gander/work/queue-278/run2/red-controller.log",
|
||||
"agents/gander/work/queue-278/run2/red/red-0.command.json",
|
||||
"agents/gander/work/queue-278/run2/red/red-0.log",
|
||||
"agents/gander/work/queue-278/run2/red/red-1.command.json",
|
||||
"agents/gander/work/queue-278/run2/red/red-1.log",
|
||||
"agents/gander/work/queue-278/run2/red/results.json",
|
||||
"agents/gander/work/queue-278/run2/start.txt",
|
||||
"agents/gander/work/queue-278/run2/sweep.json",
|
||||
"agents/gander/work/queue-278/run2/syntax-controller.log",
|
||||
"agents/gander/work/queue-278/run2/syntax/results.json",
|
||||
"agents/gander/work/queue-278/run2/syntax/syntax-0.command.json",
|
||||
"agents/gander/work/queue-278/run2/syntax/syntax-0.log",
|
||||
"agents/gander/work/queue-278/run2/syntax/syntax-1.command.json",
|
||||
"agents/gander/work/queue-278/run2/syntax/syntax-1.log",
|
||||
"agents/gander/work/queue-278/run2/syntax/syntax-2.command.json",
|
||||
"agents/gander/work/queue-278/run2/syntax/syntax-2.log",
|
||||
"agents/gander/work/queue-278/run2/syntax/syntax-3.command.json",
|
||||
"agents/gander/work/queue-278/run2/syntax/syntax-3.log",
|
||||
"agents/gander/work/queue-278/run2/syntax/syntax-4.command.json",
|
||||
"agents/gander/work/queue-278/run2/syntax/syntax-4.log",
|
||||
"agents/gander/work/queue-278/run2/tmpdir.txt",
|
||||
"agents/gander/work/queue-278/run2/whole-controller.log",
|
||||
"agents/gander/work/queue-278/run2/whole/bus.command.json",
|
||||
"agents/gander/work/queue-278/run2/whole/bus.log",
|
||||
"agents/gander/work/queue-278/run2/whole/results.json",
|
||||
"agents/gander/work/queue-278/run2/whole/webui.command.json",
|
||||
"agents/gander/work/queue-278/run2/whole/webui.log",
|
||||
"agents/gander/work/queue-278/run3/KILL-TABLE.md",
|
||||
"agents/gander/work/queue-278/run3/SUMMARY.json",
|
||||
"agents/gander/work/queue-278/run3/TMPDIR",
|
||||
"agents/gander/work/queue-278/run3/changes-controller.log",
|
||||
"agents/gander/work/queue-278/run3/changes/M1-base.command.json",
|
||||
"agents/gander/work/queue-278/run3/changes/M1-base.log",
|
||||
"agents/gander/work/queue-278/run3/changes/M1.command.json",
|
||||
"agents/gander/work/queue-278/run3/changes/M1.log",
|
||||
"agents/gander/work/queue-278/run3/changes/M2.command.json",
|
||||
"agents/gander/work/queue-278/run3/changes/M2.log",
|
||||
"agents/gander/work/queue-278/run3/changes/M3-base.command.json",
|
||||
"agents/gander/work/queue-278/run3/changes/M3-base.log",
|
||||
"agents/gander/work/queue-278/run3/changes/M3.command.json",
|
||||
"agents/gander/work/queue-278/run3/changes/M3.log",
|
||||
"agents/gander/work/queue-278/run3/changes/M4-base.command.json",
|
||||
"agents/gander/work/queue-278/run3/changes/M4-base.log",
|
||||
"agents/gander/work/queue-278/run3/changes/M4.command.json",
|
||||
"agents/gander/work/queue-278/run3/changes/M4.log",
|
||||
"agents/gander/work/queue-278/run3/changes/M5-base.command.json",
|
||||
"agents/gander/work/queue-278/run3/changes/M5-base.log",
|
||||
"agents/gander/work/queue-278/run3/changes/M5.command.json",
|
||||
"agents/gander/work/queue-278/run3/changes/M5.log",
|
||||
"agents/gander/work/queue-278/run3/changes/M6.command.json",
|
||||
"agents/gander/work/queue-278/run3/changes/M6.log",
|
||||
"agents/gander/work/queue-278/run3/changes/results.json",
|
||||
"agents/gander/work/queue-278/run3/end.txt",
|
||||
"agents/gander/work/queue-278/run3/focused-controller.log",
|
||||
"agents/gander/work/queue-278/run3/focused/focused-0.command.json",
|
||||
"agents/gander/work/queue-278/run3/focused/focused-0.log",
|
||||
"agents/gander/work/queue-278/run3/focused/focused-1.command.json",
|
||||
"agents/gander/work/queue-278/run3/focused/focused-1.log",
|
||||
"agents/gander/work/queue-278/run3/focused/focused-2.command.json",
|
||||
"agents/gander/work/queue-278/run3/focused/focused-2.log",
|
||||
"agents/gander/work/queue-278/run3/focused/results.json",
|
||||
"agents/gander/work/queue-278/run3/preexisting-processes.txt",
|
||||
"agents/gander/work/queue-278/run3/red-controller.log",
|
||||
"agents/gander/work/queue-278/run3/red/red-0.command.json",
|
||||
"agents/gander/work/queue-278/run3/red/red-0.log",
|
||||
"agents/gander/work/queue-278/run3/red/red-1.command.json",
|
||||
"agents/gander/work/queue-278/run3/red/red-1.log",
|
||||
"agents/gander/work/queue-278/run3/red/results.json",
|
||||
"agents/gander/work/queue-278/run3/start.txt",
|
||||
"agents/gander/work/queue-278/run3/sweep-initial.json",
|
||||
"agents/gander/work/queue-278/run3/sweep.json",
|
||||
"agents/gander/work/queue-278/run3/syntax-controller.log",
|
||||
"agents/gander/work/queue-278/run3/syntax/results.json",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-0.command.json",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-0.log",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-1.command.json",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-1.log",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-2.command.json",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-2.log",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-3.command.json",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-3.log",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-4.command.json",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-4.log",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-6.command.json",
|
||||
"agents/gander/work/queue-278/run3/syntax/syntax-6.log",
|
||||
"agents/gander/work/queue-278/run3/whole-controller.log",
|
||||
"agents/gander/work/queue-278/run3/whole/bus.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/bus.log",
|
||||
"agents/gander/work/queue-278/run3/whole/results.json",
|
||||
"agents/gander/work/queue-278/run3/whole/test-auth.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/test-auth.log",
|
||||
"agents/gander/work/queue-278/run3/whole/test-conductor.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/test-conductor.log",
|
||||
"agents/gander/work/queue-278/run3/whole/test-config.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/test-config.log",
|
||||
"agents/gander/work/queue-278/run3/whole/test-discord.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/test-discord.log",
|
||||
"agents/gander/work/queue-278/run3/whole/test-extension-package.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/test-extension-package.log",
|
||||
"agents/gander/work/queue-278/run3/whole/test-foundation.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/test-foundation.log",
|
||||
"agents/gander/work/queue-278/run3/whole/test-land-work.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/test-land-work.log",
|
||||
"agents/gander/work/queue-278/run3/whole/test-queue.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/test-queue.log",
|
||||
"agents/gander/work/queue-278/run3/whole/test-release.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/test-release.log",
|
||||
"agents/gander/work/queue-278/run3/whole/test-task.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/test-task.log",
|
||||
"agents/gander/work/queue-278/run3/whole/webui.command.json",
|
||||
"agents/gander/work/queue-278/run3/whole/webui.log",
|
||||
"agents/gander/work/queue-278/sweep.py",
|
||||
"agents/gander/work/queue-278/window.py",
|
||||
"agents/gizmoduck/work/queue-268/BUILD.md",
|
||||
"agents/honker/work/queue-194/BUILD.md",
|
||||
"agents/honker/work/queue-194/DESIGN.md",
|
||||
"agents/honker/work/queue-194/PRD-1.8-DRAFT.md",
|
||||
"agents/honker/work/queue-256/BUILD.md",
|
||||
"agents/honker/work/queue-256/DESIGN.md",
|
||||
"agents/megavolt/work/queue-236/BUILD.md",
|
||||
"agents/morgana/work/fed-r3/BUILD.md",
|
||||
"agents/morgana/work/queue-205/BUILD.md",
|
||||
"agents/morgana/work/queue-205/DESIGN.md",
|
||||
"agents/morgana/work/queue-277/BUILD.md",
|
||||
"agents/rocko/work/queue-191/stoprun-build/BUILD.md",
|
||||
"agents/rocko/work/queue-279/BUILD.md",
|
||||
"agents/rocko/work/queue-279/base.txt",
|
||||
"agents/rocko/work/queue-279/build.patch",
|
||||
"agents/rocko/work/queue-279/candidate-manifest.sha256",
|
||||
"agents/rocko/work/queue-279/candidate.txt",
|
||||
"agents/rocko/work/queue-279/changed-paths.txt",
|
||||
"agents/rocko/work/queue-279/commit.txt",
|
||||
"agents/rocko/work/queue-279/logs/M1.log",
|
||||
"agents/rocko/work/queue-279/logs/processing-control-whole.log",
|
||||
"agents/rocko/work/queue-279/logs/regression-red.log",
|
||||
"agents/rocko/work/queue-279/logs/stop-run-whole.log",
|
||||
"agents/rocko/work/queue-279/mutant-plan.json",
|
||||
"agents/rocko/work/queue-279/packet-manifest.sha256",
|
||||
"agents/rocko/work/queue-279/reader-search.txt",
|
||||
"agents/rocko/work/queue-279/results.json",
|
||||
"agents/rocko/work/queue-279/run-step.py",
|
||||
"agents/rocko/work/queue-279/scan-result.json",
|
||||
"agents/rocko/work/queue-279/skips.txt",
|
||||
"agents/rocko/work/queue-279/source-scope-check.json",
|
||||
"agents/rocko/work/queue-279/sweep.json",
|
||||
"agents/rocko/work/queue-279/tmpdir.txt",
|
||||
"agents/rocko/work/queue-280/BUILD.md",
|
||||
"agents/rocko/work/queue-280/assertion-preservation.json",
|
||||
"agents/rocko/work/queue-280/base.txt",
|
||||
"agents/rocko/work/queue-280/build.patch",
|
||||
"agents/rocko/work/queue-280/candidate-manifest.sha256",
|
||||
"agents/rocko/work/queue-280/candidate.txt",
|
||||
"agents/rocko/work/queue-280/changed-paths.txt",
|
||||
"agents/rocko/work/queue-280/commit.txt",
|
||||
"agents/rocko/work/queue-280/final-sweep.json",
|
||||
"agents/rocko/work/queue-280/mutant-restore.json",
|
||||
"agents/rocko/work/queue-280/mutants.json",
|
||||
"agents/rocko/work/queue-280/packet-manifest.sha256",
|
||||
"agents/rocko/work/queue-280/post-phase1-pgrep.txt",
|
||||
"agents/rocko/work/queue-280/reader-search.txt",
|
||||
"agents/rocko/work/queue-280/repeat-commit.txt",
|
||||
"agents/rocko/work/queue-280/repeat-results.json",
|
||||
"agents/rocko/work/queue-280/repeat-summary.json",
|
||||
"agents/rocko/work/queue-280/restored-hashes.json",
|
||||
"agents/rocko/work/queue-280/results.json",
|
||||
"agents/rocko/work/queue-280/scan-result.json",
|
||||
"agents/rocko/work/queue-280/skips.txt",
|
||||
"agents/rocko/work/queue-280/sweep-phase1.json",
|
||||
"agents/rocko/work/vikunja-502/BUILD.md",
|
||||
"agents/rocko/work/vikunja-502/RESULTS.md",
|
||||
"agents/rocko/work/vikunja-502/all-mutants.json",
|
||||
"agents/rocko/work/vikunja-502/base-drift-ce6a.json",
|
||||
"agents/rocko/work/vikunja-502/base.txt",
|
||||
"agents/rocko/work/vikunja-502/build.patch",
|
||||
"agents/rocko/work/vikunja-502/candidate-manifest.sha256",
|
||||
"agents/rocko/work/vikunja-502/candidate.txt",
|
||||
"agents/rocko/work/vikunja-502/changed-paths.txt",
|
||||
"agents/rocko/work/vikunja-502/commit.txt",
|
||||
"agents/rocko/work/vikunja-502/cwd-sweep.json",
|
||||
"agents/rocko/work/vikunja-502/focused-results.json",
|
||||
"agents/rocko/work/vikunja-502/gate-plan.json",
|
||||
"agents/rocko/work/vikunja-502/logs/bus-tasks-whole.log",
|
||||
"agents/rocko/work/vikunja-502/logs/processing-http-whole.log",
|
||||
"agents/rocko/work/vikunja-502/logs/python-regressions.log",
|
||||
"agents/rocko/work/vikunja-502/logs/replays.log",
|
||||
"agents/rocko/work/vikunja-502/logs/runner-green.log",
|
||||
"agents/rocko/work/vikunja-502/logs/runner-red.log",
|
||||
"agents/rocko/work/vikunja-502/logs/stop-run-whole.log",
|
||||
"agents/rocko/work/vikunja-502/mutant-logs/KS43.tap",
|
||||
"agents/rocko/work/vikunja-502/mutant-logs/KS44.tap",
|
||||
"agents/rocko/work/vikunja-502/mutant-logs/KS54.tap",
|
||||
"agents/rocko/work/vikunja-502/mutant-logs/SR08.tap",
|
||||
"agents/rocko/work/vikunja-502/mutant-logs/V502-status.tap",
|
||||
"agents/rocko/work/vikunja-502/mutant-results.json",
|
||||
"agents/rocko/work/vikunja-502/mutant-resume-summary.json",
|
||||
"agents/rocko/work/vikunja-502/packet-manifest.sha256",
|
||||
"agents/rocko/work/vikunja-502/pre-rebase-commit.txt",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/BUILD.md",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/base.txt",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/candidate-manifest.sha256",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/candidate.txt",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/cwd-sweep.json",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/logs/bus-tasks-whole.log",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/logs/processing-http-whole.log",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/logs/python-regressions.log",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/logs/stop-run-whole.log",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/packet-manifest.sha256",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/range-diff.txt",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/restored-hashes.json",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/results.json",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/scan-result.json",
|
||||
"agents/rocko/work/vikunja-502/rebase-980c/tmpdir.txt",
|
||||
"agents/rocko/work/vikunja-502/rebase-proof.json",
|
||||
"agents/rocko/work/vikunja-502/rebase-range-diff.txt",
|
||||
"agents/rocko/work/vikunja-502/restored-candidate.json",
|
||||
"agents/rocko/work/vikunja-502/run-mutants.baseline.py",
|
||||
"agents/rocko/work/vikunja-502/run-mutants.py",
|
||||
"agents/rocko/work/vikunja-502/runner-change.diff",
|
||||
"agents/rocko/work/vikunja-502/runner.sha256",
|
||||
"agents/rocko/work/vikunja-502/scan-summary.json",
|
||||
"agents/rocko/work/vikunja-502/skips.json",
|
||||
"agents/rocko/work/vikunja-502/tests/test_timeout_cause.py",
|
||||
"agents/rocko/work/vikunja-502/tmpdir.txt",
|
||||
"agents/sage/work/vikunja-setup/comment-r191-landed.mjs",
|
||||
"agents/sage/work/vikunja-setup/file-thread-claims.mjs",
|
||||
"agents/sage/work/vikunja-setup/post-w12-86f95bb27.mjs",
|
||||
"agents/sage/work/window-plan/repin-86f95bb27.md",
|
||||
"docs/SESSIONS.md",
|
||||
"docs/case2/SETUP-SHEET.md",
|
||||
"docs/case2/SURVEY-RUN.md",
|
||||
"docs/plans/2026-10-22_fire-revokes-grants.md",
|
||||
"docs/plans/2026-10-22_owner-thread-claims.md",
|
||||
"docs/plans/2026-10-22_sshd-port-race.md",
|
||||
"docs/plans/2026-10-22_sso-phase2-window.md",
|
||||
"docs/plans/2026-10-22_stoprun-deadline.md",
|
||||
"docs/plans/DEFERRED.md",
|
||||
"docs/plans/QUEUE.md",
|
||||
"docs/plans/queue.json",
|
||||
"packages/bus/README.md",
|
||||
"packages/bus/schema.sql",
|
||||
"packages/bus/src/broker.mjs",
|
||||
"packages/bus/src/client.mjs",
|
||||
"packages/bus/src/credentials.mjs",
|
||||
"packages/bus/src/gitea-grant.mjs",
|
||||
"packages/bus/src/human-cli.mjs",
|
||||
"packages/bus/src/migrations/3j-3k.sql",
|
||||
"packages/bus/src/process.mjs",
|
||||
"packages/bus/src/runtime.mjs",
|
||||
"packages/bus/src/server.mjs",
|
||||
"packages/bus/src/store.mjs",
|
||||
"packages/bus/src/user-cli.mjs",
|
||||
"packages/bus/tests/account-migration.test.mjs",
|
||||
"packages/bus/tests/channel-proof.test.mjs",
|
||||
"packages/bus/tests/chief-of-staff.test.mjs",
|
||||
"packages/bus/tests/comment-migration.test.mjs",
|
||||
"packages/bus/tests/creative.test.mjs",
|
||||
"packages/bus/tests/fake-gitea-api.mjs",
|
||||
"packages/bus/tests/fixtures/schema-3j.sql",
|
||||
"packages/bus/tests/gitea-grant-docker.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-host.test.mjs",
|
||||
"packages/bus/tests/grant-migration.test.mjs",
|
||||
"packages/bus/tests/host-run.test.mjs",
|
||||
"packages/bus/tests/instances-verb.test.mjs",
|
||||
"packages/bus/tests/lifecycle-revoke.test.mjs",
|
||||
"packages/bus/tests/lifecycle-verbs.test.mjs",
|
||||
"packages/bus/tests/message-filter.test.mjs",
|
||||
"packages/bus/tests/name-migration.test.mjs",
|
||||
"packages/bus/tests/project-runtime.test.mjs",
|
||||
"packages/bus/tests/reload.test.mjs",
|
||||
"packages/bus/tests/schema.test.mjs",
|
||||
"packages/bus/tests/scratch-sshd.mjs",
|
||||
"packages/bus/tests/spec-migration.test.mjs",
|
||||
"packages/bus/tests/tasks.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/usage-migration.test.mjs",
|
||||
"packages/bus/tests/user-principal.test.mjs",
|
||||
"packages/business/README.md",
|
||||
"packages/business/src/access.mjs",
|
||||
"packages/business/src/business.mjs",
|
||||
"packages/business/src/credentials.mjs",
|
||||
"packages/business/src/index.mjs",
|
||||
"packages/business/src/instance-settings.mjs",
|
||||
"packages/business/src/launch-settings.mjs",
|
||||
"packages/business/src/organization.mjs",
|
||||
"packages/business/src/role.mjs",
|
||||
"packages/business/src/vocabulary.mjs",
|
||||
"packages/business/tests/business.test.mjs",
|
||||
"packages/business/tests/chief-of-staff.test.mjs",
|
||||
"packages/business/tests/cli.test.mjs",
|
||||
"packages/business/tests/hard-waits.test.mjs",
|
||||
"packages/business/tests/instance-settings.test.mjs",
|
||||
"packages/business/tests/launch-settings.test.mjs",
|
||||
"packages/business/tests/lifecycle-settings.test.mjs",
|
||||
"packages/business/tests/role.test.mjs",
|
||||
"packages/cli/README.md",
|
||||
"packages/cli/src/cli.mjs",
|
||||
"packages/cli/src/format.mjs",
|
||||
"packages/cli/src/grants.mjs",
|
||||
"packages/cli/src/instances.mjs",
|
||||
"packages/cli/src/launcher.mjs",
|
||||
"packages/cli/tests/format.test.mjs",
|
||||
"packages/cli/tests/grants.test.mjs",
|
||||
"packages/cli/tests/instances.test.mjs",
|
||||
"packages/cli/tests/lifecycle.test.mjs",
|
||||
"packages/cli/tests/stack-release.test.mjs",
|
||||
"packages/cli/tests/tester.test.mjs",
|
||||
"packages/harness/src/runner.mjs",
|
||||
"packages/harness/src/tools.mjs",
|
||||
"packages/harness/tests/bundle.test.mjs",
|
||||
"packages/harness/tests/runner.test.mjs",
|
||||
"packages/harness/tests/tools.test.mjs",
|
||||
"packages/library/README.md",
|
||||
"packages/library/src/mapping.mjs",
|
||||
"packages/library/src/scope.mjs",
|
||||
"packages/library/tests/mapping.test.mjs",
|
||||
"packages/library/tests/scope.test.mjs",
|
||||
"packages/processing-control/src/runtimes.mjs",
|
||||
"packages/processing-control/tests/stop-run.test.mjs",
|
||||
"packages/webui/README.md",
|
||||
"packages/webui/src/bus.mjs",
|
||||
"packages/webui/src/cli.mjs",
|
||||
"packages/webui/src/public/agents.js",
|
||||
"packages/webui/src/public/bus.js",
|
||||
"packages/webui/src/public/index.html",
|
||||
"packages/webui/src/serve.mjs",
|
||||
"packages/webui/tests/agents-browser.test.mjs",
|
||||
"packages/webui/tests/agents.test.mjs",
|
||||
"packages/webui/tests/browser.test.mjs",
|
||||
"packages/webui/tests/instances.test.mjs",
|
||||
"packages/webui/tests/processing.test.mjs",
|
||||
"packages/webui/tests/shell.test.mjs",
|
||||
"packages/webui/tests/sso.test.mjs",
|
||||
"packages/webui/tests/thread-claims-browser.test.mjs",
|
||||
"packages/webui/tests/thread-claims.test.mjs",
|
||||
"roles/chief-of-staff.json",
|
||||
"roles/chief-of-staff.md",
|
||||
"scripts/mosaic"
|
||||
]
|
||||
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"packages/cli/tests/case1-accept.test.mjs": {
|
||||
"timeout_seconds": 900,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 930,
|
||||
"reason": "sage-2709: six CLI whole files use900s; measured durations establish later3x basis."
|
||||
},
|
||||
"packages/cli/tests/case2-unit.test.mjs": {
|
||||
"timeout_seconds": 900,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 930,
|
||||
"reason": "sage-2709: six CLI whole files use900s; measured durations establish later3x basis."
|
||||
},
|
||||
"packages/bus/tests/account-migration.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/chief-of-staff.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/comment-migration.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/end-launch.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/fire-revokes-grants.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/gitea-grant.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/gitea-migration.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/grant-executor.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/grant-migration.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/lifecycle-revoke.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/message-filter.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/name-migration.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/schema.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/sessions.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/spec-migration.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/tester-writes.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/thread-claims.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/thread-migration.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/threads.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/bus/tests/usage-migration.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
},
|
||||
"packages/cli/tests/lifecycle.test.mjs": {
|
||||
"timeout_seconds": 900,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 930,
|
||||
"reason": "sage-2709: six CLI whole files use900s; measured durations establish later3x basis."
|
||||
},
|
||||
"packages/cli/tests/stack-release.test.mjs": {
|
||||
"timeout_seconds": 900,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 930,
|
||||
"reason": "sage-2709: six CLI whole files use900s; measured durations establish later3x basis."
|
||||
},
|
||||
"packages/cli/tests/thread-cli.test.mjs": {
|
||||
"timeout_seconds": 900,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 930,
|
||||
"reason": "sage-2709: six CLI whole files use900s; measured durations establish later3x basis."
|
||||
},
|
||||
"packages/cli/tests/thread-launch.test.mjs": {
|
||||
"timeout_seconds": 900,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 930,
|
||||
"reason": "sage-2709: six CLI whole files use900s; measured durations establish later3x basis."
|
||||
},
|
||||
"packages/webui/tests/chat.test.mjs": {
|
||||
"timeout_seconds": 300,
|
||||
"kill_after_seconds": 10,
|
||||
"outer_harness_seconds": 330,
|
||||
"reason": "Whole-file bound under sage-2705; no prior repaired-C4 whole-file timing claimed."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Row253 final base and pre-gate scope
|
||||
|
||||
Authority sage-2705. New branch gander/row253-aca1 on aca16404cf522b03f39acaf140e4f6185a507b88. Rebased source tip ed3bb376cdd5128dc58b3aa193282e1c2bfcf57d. Thirteen commits replayed from b78918850..288cdecf6; all thirteen final-hop range-diff pairs equal, no conflict. Earlier branches remain. The requested comparison from d213ef651 is range-diff-from-d213.txt; the two extra interim packet commits have no old counterpart, and the original BUILD-LOG context changes are documented in integration-b789/.
|
||||
|
||||
Seven manifest blobs are byte-identical to288cdecf6. Digest573c51cf72f2398532a6ea768536f04e3c1681997cb8b9a6f14a8aef884870ae. All seven sha256sum checks pass. Both append-only paths keep the entire final base as exact byte prefix. Final-hop upstream changes only Gizmoduck BUILD and queue JSON/rendering; none is a253 focused read or pinned asset. The earlier264a Store/schema dependency is present on the base and is covered by whole-file scope.
|
||||
|
||||
Duckworth's requested prefix-resolution material remains in integration-b789/NOTES.md, manifest-drift.diff and whole-test-union.json. This folder's drift is385a7194c..finalbase on the manifest paths and its whole union was independently recomputed:27 runnable files, unchanged from interim. Fixtures are exercised through importers, not represented as standalone tests. N9 is the exact sandbox bind case in case2; report its runtime skip reason verbatim.
|
||||
|
||||
## Bounds written before any run
|
||||
|
||||
Step2 runs all27 files whole, own case1/case2 first, then remaining sorted paths. Every call is timeout -k10 300 node --test --test-reporter=tap FILE; Python harness outer wait330s is a fallback. FILE-LIMITS.json records every file. Busy check gates each start, pipefail and tee capture raw logs and structured command JSONs. Any timeout is a stop, never a kill/pass; any unexpected red stops with no retry. Print HEAD and manifest digest first. No run starts before focused CLEAR. Expected duration10-15min; status every15min and immediate stop report.
|
||||
|
||||
The later whole gate remains separately gated. Every whole shell uses SHELL-LIMITS.json (ceil3xmax+120), recorded in BUILD.md. Script bytes unchanged; no no-record case, so900 fallback unused. Whole CLI needs a separately stated bound in its whole-gate plan because the historical whole result took597s; a300s file limit is not a claim about full-suite runtime. No carried results. No product/test/assertion changed during final integration.
|
||||
|
||||
No tests, Node, syntax commands, browser, probe, queue write or push have run while writing this packet. Publishing is authorized by sage-2705 after this record is committed. The next action is request focused CLEAR, then27 whole files, Duckworth rebase check and later whole-gate CLEAR.
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"authority": "sage-2705",
|
||||
"base": "aca16404cf522b03f39acaf140e4f6185a507b88",
|
||||
"source_tip": "ed3bb376cdd5128dc58b3aa193282e1c2bfcf57d",
|
||||
"branch": "gander/row253-aca1",
|
||||
"manifest": "573c51cf72f2398532a6ea768536f04e3c1681997cb8b9a6f14a8aef884870ae",
|
||||
"final_hop_paths": [
|
||||
"agents/gizmoduck/work/queue-264/BUILD.md",
|
||||
"docs/plans/QUEUE.md",
|
||||
"docs/plans/queue.json"
|
||||
],
|
||||
"final_hop_conflicts": [],
|
||||
"manifest_unchanged": true,
|
||||
"all_7_blobs_equal_b789": true,
|
||||
"append_prefix_checks": true,
|
||||
"test_union_count": 27,
|
||||
"tests_run": false
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
diff --git a/docs/plans/DEFERRED.md b/docs/plans/DEFERRED.md
|
||||
index 4c0c736f1..f990eb3a9 100644
|
||||
--- a/docs/plans/DEFERRED.md
|
||||
+++ b/docs/plans/DEFERRED.md
|
||||
@@ -431,17 +431,6 @@ at every gate. Started 2026-09-12 during the control board MVP.
|
||||
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.)
|
||||
@@ -460,6 +449,44 @@ 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)
|
||||
+
|
||||
+- **Row 281's refusal texts point the wrong way.** (Dewey N1 and N2 on
|
||||
+ the row 281 manifest round, dewey-0842/0843 and dewey-0846; Sage
|
||||
+ sage-2653.) N1: when a host decision is still open and the host is
|
||||
+ removed, step G's fire or delete job refuses `host-unknown`, and the
|
||||
+ inbox hint names a verb that can't help. The way out is to decline or
|
||||
+ withdraw the decision (`packages/bus/src/broker.mjs`, the closed check
|
||||
+ near :1684 and `decision.withdraw` near :5015). T14 now pins that path.
|
||||
+ N2: the chief's fire tool text in `packages/harness/src/tools.mjs`
|
||||
+ (near :186) doesn't say the fire's job can come back for an answer.
|
||||
+ Neither changes behaviour. Binds the next row that changes the step G
|
||||
+ hint text in `broker.mjs` or the fire tool text in `tools.mjs`: fix
|
||||
+ both wording gaps and pin the hint in a test. (2026-10-22)
|
||||
+
|
||||
+- **Row 264a's refusal check in sessions.test doesn't pin the actor.**
|
||||
+ (Duckworth N1 on the row 264a r12 delta, duckworth-0138; Sage sage-2695.)
|
||||
+ `refusedOnce` in `packages/bus/tests/sessions.test.mjs` checks that each
|
||||
+ refused call adds one `action.refused` event with code `not-holder`, but
|
||||
+ not that the event's `actor_run` is the refused run. Binds row 269 (264b,
|
||||
+ #1774), or the next row that changes `sessions.test.mjs`: pin `actor_run`
|
||||
+ in `refusedOnce`. (2026-10-22)
|
||||
+
|
||||
+- **editor-provenance skips its rebuild check on esbuild 0.28.1.** (Duckworth
|
||||
+ N2 on the row 264a manifest round, duckworth-0146.) The webui test
|
||||
+ "rebuilding from the source gives the same bytes" needs esbuild 0.28.2,
|
||||
+ the checkout has 0.28.1, and the gate logs a skip (webui.log:99). The file
|
||||
+ is at base and was not in 264a's manifest. Binds the next gate that
|
||||
+ touches the webui build: install the pinned esbuild in the gate checkout
|
||||
+ so the check runs, or record why it can't. (2026-10-22)
|
||||
+
|
||||
## Queue
|
||||
|
||||
Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
|
||||
@@ -531,3 +558,10 @@ Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
|
||||
`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.
|
||||
+- A fired or deleted instance kept its grants (2026-10-22, Dewey on the
|
||||
+ row 236 rebase look, mos-1271): closed by row 281 (#1786), landing
|
||||
+ e479521d3. A fire or a delete now runs a step G job that revokes every
|
||||
+ host and Gitea grant the instance holds, fail closed, and tells the
|
||||
+ human in the inbox when it can't. Megavolt built it; Dewey approved the
|
||||
+ design (d6) and the manifest (c30614). The text gaps it left are open
|
||||
+ above.
|
||||
@@ -0,0 +1,13 @@
|
||||
1: e8b096948 = 1: 730a78aa9 gander: design tester socket migration for host scripts
|
||||
2: 0d7f9da11 = 2: 643d16528 test: prepare row253 socket fixtures and fix gate collisions
|
||||
3: 7c1fcbd96 = 3: 6f320b4b0 docs: propose row253 C4 session supervisor and draft hop fake
|
||||
4: 565302e71 = 4: 62de84882 docs(row253): freeze C4 repair for static review
|
||||
5: bcf4e2501 = 5: c8cca3a68 docs(row253): store repair patch without context whitespace
|
||||
6: f2c594d7b = 6: cca936716 fix(row253): supervise socket queries across sudo sessions
|
||||
7: 29d5f0d5e = 7: 950cf47f3 fix(row253): bound cleanup retries after initial inventory
|
||||
8: e24d7b559 = 8: 3b142c9c6 docs(row253): record static approval and pre-confirmation residual
|
||||
9: 94c2b76b8 = 9: ad67d9595 docs(row253): record integration onto refactor 378a76b85
|
||||
10: 049088866 = 10: 72b17562d docs(row253): integrate row191 base and refresh manifest
|
||||
11: 7916f0ee6 = 11: e75ef1b19 docs(row253): integrate 385a base and record dependency drift and bounds
|
||||
12: c75c9bbd3 = 12: cd269e733 docs(row253): record schema3l integration before final base correction
|
||||
13: 288cdecf6 = 13: ed3bb376c docs(row253): preserve doc sweep appends on corrected interim base
|
||||
@@ -0,0 +1,50 @@
|
||||
1: a9c094d06 ! 1: 730a78aa9 gander: design tester socket migration for host scripts
|
||||
@@ Commit message
|
||||
gander: design tester socket migration for host scripts
|
||||
|
||||
## BUILD-LOG.md ##
|
||||
-@@ BUILD-LOG.md: Before: managed project/thread verbs had no Console conversation area. After: fi
|
||||
- ### 2026-10-22 | Gander | row278 gate run3
|
||||
+@@ BUILD-LOG.md: 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.
|
||||
|
||||
- Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version skip), all ten shell files425/0, touched files15/0, M1-M6 killed6/6. No timeout; sweep0 run-created leftovers. Runs1/2 void. Seven-path manifest8fa859a0 goes to Webby for final review. No landing or live change.
|
||||
+ 2026-10-22 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.
|
||||
|
||||
## agents/gander/work/queue-253/BASE-PORT-INVENTORY.json (new) ##
|
||||
2: 2e81f7872 = 2: 643d16528 test: prepare row253 socket fixtures and fix gate collisions
|
||||
3: 3c8edc358 = 3: 6f320b4b0 docs: propose row253 C4 session supervisor and draft hop fake
|
||||
4: 3114087ef = 4: 62de84882 docs(row253): freeze C4 repair for static review
|
||||
5: 911e470b7 = 5: c8cca3a68 docs(row253): store repair patch without context whitespace
|
||||
6: 4a8b5c3ff ! 6: cca936716 fix(row253): supervise socket queries across sudo sessions
|
||||
@@ Commit message
|
||||
fix(row253): supervise socket queries across sudo sessions
|
||||
|
||||
## BUILD-LOG.md ##
|
||||
-@@ BUILD-LOG.md: Before: managed project/thread verbs had no Console conversation area. After: fi
|
||||
+@@ BUILD-LOG.md: Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version
|
||||
|
||||
- Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version skip), all ten shell files425/0, touched files15/0, M1-M6 killed6/6. No timeout; sweep0 run-created leftovers. Runs1/2 void. Seven-path manifest8fa859a0 goes to Webby for final review. No landing or live change.
|
||||
+ 2026-10-22 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
|
||||
@@ agents/gander/work/queue-253/MANIFEST.sha256 (new)
|
||||
|
||||
## docs/plans/DEFERRED.md ##
|
||||
@@ docs/plans/DEFERRED.md: Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
|
||||
- `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
|
||||
+ human in the inbox when it can't. Megavolt built it; Dewey approved the
|
||||
+ design (d6) and the manifest (c30614). The text gaps it left are open
|
||||
above.
|
||||
+
|
||||
+## Row253 F5: legacy Case AS_USER supervision
|
||||
7: e18b90e95 = 7: 950cf47f3 fix(row253): bound cleanup retries after initial inventory
|
||||
8: 677767145 = 8: 3b142c9c6 docs(row253): record static approval and pre-confirmation residual
|
||||
9: c733371c5 = 9: ad67d9595 docs(row253): record integration onto refactor 378a76b85
|
||||
10: 141b79c29 = 10: 72b17562d docs(row253): integrate row191 base and refresh manifest
|
||||
11: d213ef651 = 11: e75ef1b19 docs(row253): integrate 385a base and record dependency drift and bounds
|
||||
-: --------- > 12: cd269e733 docs(row253): record schema3l integration before final base correction
|
||||
-: --------- > 13: ed3bb376c docs(row253): preserve doc sweep appends on corrected interim base
|
||||
@@ -0,0 +1,29 @@
|
||||
[
|
||||
"packages/cli/tests/case1-accept.test.mjs",
|
||||
"packages/cli/tests/case2-unit.test.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/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"
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
# Row253 corrected base, pending final docs tip
|
||||
|
||||
Sage2700 moves the base tob78918850;2702 says one docs-only Gizmoduck BUILD commit still lands. Rebased on new branch gander/row253-b789, preserving gander/row253-264a. Resolved only BUILD-LOG and DEFERRED append conflicts, keeping every upstream entry first, then253. Both current files retain the full base blobs as exact byte prefixes. No source/test/assertion change.
|
||||
|
||||
Source tipc75c9bbd3339cb2343467715a14455044c2e4aea, manifest573c51cf72f2398532a6ea768536f04e3c1681997cb8b9a6f14a8aef884870ae. Six manifest blobs remain identical to the prior integration; DEFERRED absorbs the doc sweep. Runnable blobs match Duckworth-approved preview. The27-file union is unchanged. SHELL-LIMITS and script bytes are unchanged. No tests, Node, syntax execution, queue command, helper push or host action. Hold locally until Sage names final tip; then another rebase, no force-push, and focused CLEAR before any test.
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"authority": [
|
||||
"sage-2700",
|
||||
"sage-2702"
|
||||
],
|
||||
"base": "b78918850",
|
||||
"tip": "c75c9bbd3339cb2343467715a14455044c2e4aea",
|
||||
"manifest": "573c51cf72f2398532a6ea768536f04e3c1681997cb8b9a6f14a8aef884870ae",
|
||||
"conflicts": [
|
||||
"BUILD-LOG.md first design append",
|
||||
"docs/plans/DEFERRED.md first F5 append"
|
||||
],
|
||||
"resolution": "Kept upstream exact byte prefix then appended253; later253 appends replayed. No existing entry edited/dropped.",
|
||||
"unchanged_runnable_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"
|
||||
],
|
||||
"whole_test_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"
|
||||
],
|
||||
"tests_run": false,
|
||||
"published": false,
|
||||
"next": "Wait final docs-only tip, rebase on a new name, publish when authorized, ask focused CLEAR. No test before CLEAR."
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
diff --git a/docs/plans/DEFERRED.md b/docs/plans/DEFERRED.md
|
||||
index 4c0c736f1..f990eb3a9 100644
|
||||
--- a/docs/plans/DEFERRED.md
|
||||
+++ b/docs/plans/DEFERRED.md
|
||||
@@ -431,17 +431,6 @@ at every gate. Started 2026-09-12 during the control board MVP.
|
||||
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.)
|
||||
@@ -460,6 +449,44 @@ 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)
|
||||
+
|
||||
+- **Row 281's refusal texts point the wrong way.** (Dewey N1 and N2 on
|
||||
+ the row 281 manifest round, dewey-0842/0843 and dewey-0846; Sage
|
||||
+ sage-2653.) N1: when a host decision is still open and the host is
|
||||
+ removed, step G's fire or delete job refuses `host-unknown`, and the
|
||||
+ inbox hint names a verb that can't help. The way out is to decline or
|
||||
+ withdraw the decision (`packages/bus/src/broker.mjs`, the closed check
|
||||
+ near :1684 and `decision.withdraw` near :5015). T14 now pins that path.
|
||||
+ N2: the chief's fire tool text in `packages/harness/src/tools.mjs`
|
||||
+ (near :186) doesn't say the fire's job can come back for an answer.
|
||||
+ Neither changes behaviour. Binds the next row that changes the step G
|
||||
+ hint text in `broker.mjs` or the fire tool text in `tools.mjs`: fix
|
||||
+ both wording gaps and pin the hint in a test. (2026-10-22)
|
||||
+
|
||||
+- **Row 264a's refusal check in sessions.test doesn't pin the actor.**
|
||||
+ (Duckworth N1 on the row 264a r12 delta, duckworth-0138; Sage sage-2695.)
|
||||
+ `refusedOnce` in `packages/bus/tests/sessions.test.mjs` checks that each
|
||||
+ refused call adds one `action.refused` event with code `not-holder`, but
|
||||
+ not that the event's `actor_run` is the refused run. Binds row 269 (264b,
|
||||
+ #1774), or the next row that changes `sessions.test.mjs`: pin `actor_run`
|
||||
+ in `refusedOnce`. (2026-10-22)
|
||||
+
|
||||
+- **editor-provenance skips its rebuild check on esbuild 0.28.1.** (Duckworth
|
||||
+ N2 on the row 264a manifest round, duckworth-0146.) The webui test
|
||||
+ "rebuilding from the source gives the same bytes" needs esbuild 0.28.2,
|
||||
+ the checkout has 0.28.1, and the gate logs a skip (webui.log:99). The file
|
||||
+ is at base and was not in 264a's manifest. Binds the next gate that
|
||||
+ touches the webui build: install the pinned esbuild in the gate checkout
|
||||
+ so the check runs, or record why it can't. (2026-10-22)
|
||||
+
|
||||
## Queue
|
||||
|
||||
Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
|
||||
@@ -531,3 +558,10 @@ Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
|
||||
`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.
|
||||
+- A fired or deleted instance kept its grants (2026-10-22, Dewey on the
|
||||
+ row 236 rebase look, mos-1271): closed by row 281 (#1786), landing
|
||||
+ e479521d3. A fire or a delete now runs a step G job that revokes every
|
||||
+ host and Gitea grant the instance holds, fail closed, and tells the
|
||||
+ human in the inbox when it can't. Megavolt built it; Dewey approved the
|
||||
+ design (d6) and the manifest (c30614). The text gaps it left are open
|
||||
+ above.
|
||||
@@ -0,0 +1,49 @@
|
||||
1: 0c44ba7c4 ! 1: e8b096948 gander: design tester socket migration for host scripts
|
||||
@@ Commit message
|
||||
gander: design tester socket migration for host scripts
|
||||
|
||||
## BUILD-LOG.md ##
|
||||
-@@ BUILD-LOG.md: Before: managed project/thread verbs had no Console conversation area. After: fi
|
||||
- ### 2026-10-22 | Gander | row278 gate run3
|
||||
+@@ BUILD-LOG.md: 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.
|
||||
|
||||
- Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version skip), all ten shell files425/0, touched files15/0, M1-M6 killed6/6. No timeout; sweep0 run-created leftovers. Runs1/2 void. Seven-path manifest8fa859a0 goes to Webby for final review. No landing or live change.
|
||||
+ 2026-10-22 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.
|
||||
|
||||
## agents/gander/work/queue-253/BASE-PORT-INVENTORY.json (new) ##
|
||||
2: a1f3b4f31 = 2: 0d7f9da11 test: prepare row253 socket fixtures and fix gate collisions
|
||||
3: 1ecc23eb1 = 3: 7c1fcbd96 docs: propose row253 C4 session supervisor and draft hop fake
|
||||
4: 18523ce92 = 4: 565302e71 docs(row253): freeze C4 repair for static review
|
||||
5: 7450fb712 = 5: bcf4e2501 docs(row253): store repair patch without context whitespace
|
||||
6: d202f6d63 ! 6: f2c594d7b fix(row253): supervise socket queries across sudo sessions
|
||||
@@ Commit message
|
||||
fix(row253): supervise socket queries across sudo sessions
|
||||
|
||||
## BUILD-LOG.md ##
|
||||
-@@ BUILD-LOG.md: Before: managed project/thread verbs had no Console conversation area. After: fi
|
||||
+@@ BUILD-LOG.md: Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version
|
||||
|
||||
- Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version skip), all ten shell files425/0, touched files15/0, M1-M6 killed6/6. No timeout; sweep0 run-created leftovers. Runs1/2 void. Seven-path manifest8fa859a0 goes to Webby for final review. No landing or live change.
|
||||
+ 2026-10-22 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
|
||||
@@ agents/gander/work/queue-253/MANIFEST.sha256 (new)
|
||||
|
||||
## docs/plans/DEFERRED.md ##
|
||||
@@ docs/plans/DEFERRED.md: Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
|
||||
- `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
|
||||
+ human in the inbox when it can't. Megavolt built it; Dewey approved the
|
||||
+ design (d6) and the manifest (c30614). The text gaps it left are open
|
||||
above.
|
||||
+
|
||||
+## Row253 F5: legacy Case AS_USER supervision
|
||||
7: 37c97b41d = 7: 29d5f0d5e fix(row253): bound cleanup retries after initial inventory
|
||||
8: 7bca9b042 = 8: e24d7b559 docs(row253): record static approval and pre-confirmation residual
|
||||
9: 3a2022038 = 9: 94c2b76b8 docs(row253): record integration onto refactor 378a76b85
|
||||
10: 614bdc8e9 = 10: 049088866 docs(row253): integrate row191 base and refresh manifest
|
||||
11: b9d3d4c75 = 11: 7916f0ee6 docs(row253): integrate 385a base and record dependency drift and bounds
|
||||
12: 2c383ef65 = 12: c75c9bbd3 docs(row253): record schema3l integration before final base correction
|
||||
Loaded 100 of 967 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user