Compare commits

..
Author SHA1 Message Date
jason.woltje d372102882 row 251: T27 adds its key with -t 2 and sleeps 3.5 s, because ssh-agent counts lifetimes in whole seconds (#1756)
Candidate darkwing/row251 27f01ef63 on 3b0aef995, manifest f2bdd684
(BUILD.md and packages/bus/tests/host-run.test.mjs). Dewey APPROVE r2,
rev 2996 (#1756 c30168). Gate on Sage's slot: bus suite whole in a clean
worktree on f7a77721a plus the two manifest paths (b79ad5f2c adds docs only),
627/627 at 21:30:33Z.
Darkwing's slot runs (darkwing-0114) are on the old base and are not the
gate.
2026-10-21 16:30:50 -05:00
jason.woltje b79ad5f2cb brief: the human deletes a creative instance, chained after row 194 (REQ-COS-3, mos-1215); window plan W11 posts on 435 2026-10-21 16:20:12 -05:00
jason.woltje 3fb00018e0 window plan: draft 1 correction, 7333 acceptance was Mos's (mos-1015), Q1/Q3/Q4 answered (mos-1214) 2026-10-21 16:18:35 -05:00
jason.woltje f7a77721a9 window plan: 10-24 re-pin draft 1, NEW 7247d80c7 with --tester-socket (mos-1145, mos-1211) 2026-10-21 16:15:48 -05:00
jason.woltje 829e5f8340 sessions: Sage row 196 landing, rows 253 and 254 launched, 194 b4 ruling, queue revs to 2994, window draft 1 2026-10-21 16:14:06 -05:00
jason.woltje abb352d27e queue: revs 2977-2993; Morgana 244 start, in-review and outcome (2977-2979), Gander 196 r6 manifest (2980/2981), Sage 244 and 196 jarvis review requests, Duckworth 196 m2 approve record (2986), Sage 196 landing note and done, row 254 issue #1759, Darkwing 251 r2 in-progress and in-review (2990/2991), rows 253 and 254 briefed 2026-10-21 16:13:56 -05:00
jason.woltje c3d88840c5 row 196: BUILD-LOG lines and two DEFERRED entries after the landing (#1694)
Doc-only append after 7247d80c7. BUILD-LOG takes Gander's ten row 196 lines from 527b9a1e6 unchanged. DEFERRED records Duckworth's C1 (round 1 N2, the dead testerNoSignIn, binding the next serve.mjs row) and round 2 N5 (the Console library routes row decides on library.sync for the tester listener). No test reads either file's content.
2026-10-21 15:59:48 -05:00
jason.woltje 7247d80c7a row 196: human boundary on peer credentials, interpreter receipt and the Console tester socket (#1694)
Candidate 527b9a1e6 (gander/row196-r6-prd19, base f48725cfc), the 39 manifest paths of CANDIDATE.sha256 537d30aef70b6cd1 at ec1e7fad8. Duckworth approved round 1 outside B1/B2 (rev 2973, comment 30145) and round 2 merge-scoped (rev 2986, comment 30156). Gate gander-0066 on grant sage-2145: 2334 pass, 0 fail, 1 skip; mutants 38/38 killed. BUILD-LOG lines and Duckworth's C1 DEFERRED entry follow as a doc-only append.
2026-10-21 15:55:39 -05:00
jason.woltje 6661732925 sessions: Sage row 222 landing, PRD 1.9 and 1.10, Pocket ID row 255, 196 r6 grants, overlap class rule (mos-1204) 2026-10-21 15:46:03 -05:00
jason.woltje 21def28502 queue: revs 2968-2976; Gander 196 r5 manifest and r6 changes move, Sage 222 review request and done, Duckworth 196 m1 changes record, rows 254 and 255 added 2026-10-21 15:45:45 -05:00
jason.woltje add0f1aae5 PRD 1.10: REQ-INST-2, a bundled Pocket ID for users with no OIDC provider, post-v1 (Q203, mos-1204)
Adds REQ-INST-2 after REQ-INST-1 as drafted in sage-2144 and ruled in mos-1204: Jason's Q203 sentence verbatim, marked post-v1 the way REQ-FED-1 is, with Mos's reading (mos-1202) labelled as his. A Version 1.10 technical note records, as fact, that REQ-WEB-2 uses the Console's own OIDC sign-in while the out-of-scope list still names 'Pocket ID and SSO', which Jason hasn't ruled on (vikunja:19/459). The list and the Pocket ID technical note are unchanged. requirements.json goes to 1.10 with 70 ids, and both requirements tests are updated to match. Design row 255 (#1760, Vikunja 458).
2026-10-21 15:26:45 -05:00
jason.woltje 7d08237e17 brief: bundled Pocket ID for users with no OIDC provider, design (Q203, Vikunja #458, mos-1202) 2026-10-21 15:21:38 -05:00
jason.woltje f48725cfc1 PRD 1.9: REQ-WEB-2, Console decide buttons for a human the Console signed in, and the REQ-DEC-3 amendment (mos-1202)
Jason approved the buttons in decision 00ed58e2 (vikunja:19/266) and chose
O6 in Q203 (OIDC-only), relayed by Mos in mos-1202 with "Land PRD 1.9 with
REQ-WEB-2 as written". The wording is sage-2067's draft. requirements.json
goes to 1.9 with 69 ids; tasks suite 97/0 and bus requirements.test.mjs 3/0
on Sage's slot.
2026-10-21 15:19:43 -05:00
jason.woltje 0de97f429d brief: library sync follow-up, client timeout, STATUS codes and README (row 222 deferrals, #1723) 2026-10-21 15:16:54 -05:00
jason.woltje 0b33f5e9db row 222: C2-03a fetch-only library sync, askpass helper, bus verb and timer (#1723)
Candidate c9aa9c2ee (megavolt, manifest c2121b55, 15 paths), approved by
Dewey (rev 2966, comment 30138). Gate on Sage's slot by Megavolt,
20:08:37Z to 20:11:33Z: library 77/77, bus 609/609, webui library-cli 1/1
and pinned-copy 10/10, cli stack-copy 17/17, every scripts/test-*.sh rc=0,
fixture tokens 0 hits; mutant kills carried from 4a0e0ab3a with equal blobs.
2026-10-21 15:13:31 -05:00
jason.woltje 3586955286 sessions: Sage row 250 landing, 196 r5 run 4 and resume, F27 row 253; Dewey 66 r4, 251 r1 and 222 r1 lines; Darkwing 251 line 2026-10-21 15:04:41 -05:00
jason.woltje a54a17f3d8 queue: revs 2905-2967 (rows 66, 215, 243, 250 landings; 196, 222, 244, 250, 251 rounds; row 253 added)
Includes other seats' revs: dewey 2905 2966; filbert 2906 2924 2950 2957; herb 2917-2919; morgana 2928 2933 2934; gizmoduck 2935 2945 2946 2951-2953; darkwing 2938 2944 2947; webby 2939; megavolt 2960 2961.
2026-10-21 15:04:30 -05:00
jason.woltje 0cdead66cd brief: F27, case2-unit.sh and case1-accept.sh move to --tester-socket, chained after row 196 (#1758)
Mos ruling mos-1198 item 4. Owner Gander, reviewer Duckworth, gate owner Sage.
2026-10-21 14:47:32 -05:00
jason.woltje 1b6158ce26 row 250: K12 holds the stop's release while it reads the shim, the release answers the events read's reason, and R13 closes its controller (#1755)
Candidate gizmoduck/row250-r2 c1a858975 on 3b0aef995, manifest eca0b8e2.
Filbert APPROVE rev 2957 (#1755 c30130). Gate on Sage's slot (grant sage-2109,
gizmoduck-0150): P1/P2/M1 probes as predicted, conversation whole 3x 183/183
and a concurrent pair 183/183 each, control-board whole 124/124.
2026-10-21 14:42:48 -05:00
jason.woltje 081ff6b128 sessions: Sage row 215 landing, 196 r4 grant after the base change, slot order for 250, 222, 244 and 251; Herb row 215 line 2026-10-21 14:35:05 -05:00
jason.woltje be86adbff2 sessions: Filbert row 250 round 2 static review, approve 2026-10-21 14:35:05 -05:00
jason.woltje d163afc47f sessions: Filbert row 250 round 1 static review, changes 2026-10-21 14:35:04 -05:00
jason.woltje e54e054632 row 215: C2-04 the library entry in the business file, consumer skips, M9 organization-projects-protected, the bare mirror, library.file and library.status (#1716)
The business file accepts a kind "library" project entry, and each consumer
that assumed a project root skips it. An organization write whose projects
differ from the file's is refused (M9). The bare mirror is created at the
projectRepo path with no remote. library.file and library.status are human
only and refuse an agent session with human-required.
Candidate herb/row215-on197 dde3445ab on a36523713, Filbert APPROVE rev 2924.
2026-10-21 14:24:37 -05:00
jason.woltje 6096b051b1 row 243: tests that depend on the gate umask set their file modes; stack-instances bootstraps under an explicit umask with a 077 subtest (#1754)
process.test, proof-cli.test and sso.test set the too-open mode with chmod
instead of taking it from the umask. stack-instances.test bootstraps under an
explicit umask and asserts the 0700 config dir under umask 077.
Candidate morgana/row243 6581f750e (code 137bda3af), Webby APPROVE rev 2939.
2026-10-21 14:12:03 -05:00
jason.woltje 9a8c8860d3 sessions: Sage row 66 landing, J2 brief and REQ-WEB-2 wording, gate coordination for 196 r2, 215, 243, 250, 251 2026-10-21 14:06:16 -05:00
jason.woltje 6195a00f8d sessions: Filbert row 215 round 2 merge-scoped review, approve 2026-10-21 13:55:22 -05:00
jason.woltje 3b0aef9952 deferred: row 66 live checks, Claude Code at the first re-pin with row 66 and pi after #383, Vikunja #454 (mos-1186) 2026-10-21 13:55:07 -05:00
jason.woltje 2c38325eca row 66: managed sessions run the unslop check on both harnesses, the operator note on the RESULT, a correction notice on the next turn, and unslop.jsonl counts (#1559)
Lands Sage's sage/row66-r5 at 7ffb640ac, round 4 (r4d 82c830779) rebased onto a36523713 after row 197,
approved by Dewey (comment 30085) and Filbert (30086) on manifest 6cfd812da4ca (23 paths, rev 2905). The
rebase had no conflict. The range-diff is = except one pi-session.test.mjs hunk header. Patch-id without
agents/ is d67275ac4251 on 7b9737953, a36523713 and this commit's parent. Gate (mos-1186): unslop,
harness, cli and every scripts/test-*.sh green on r4d, harness 149/0 again on r5. Landing sessions on a
scratch root from the candidate tree: Claude Code r4ab24630d323 showed the note, the injected notice and
counts checked 2, flagged 1, notices 1. Pi r1dd94171bede is launch only until #383 (Vikunja #454).
2026-10-21 13:51:28 -05:00
jason.woltje 640aa03f27 brief: Console inbox Approve and Deny for grant requests, Vikunja #435 (mos-1193) 2026-10-21 13:44:03 -05:00
Herbandjason.woltje a365237133 test: resolve the real Docker binary inside lane fixtures
(cherry picked from commit c45ddeb6bd)
2026-10-21 13:28:40 -05:00
jason.woltje 5afff54908 brief: T27 row 251 records the monotonic-boundary check, 8 of 80 adds lost the key (mos-1185 item 2) 2026-10-21 12:20:21 -05:00
jason.woltje f33932921c brief: T27 lists no identities right after ssh-add -t 1, Vikunja #453 (mos-1185 item 2); row 197 lane-b observation (item 3) 2026-10-21 12:12:46 -05:00
jason.woltje 1be656ac97 brief: K12 reads cgroup.events after cgroup.kill under load, Vikunja #450 (mos-1182 item 3) 2026-10-21 11:09:37 -05:00
jason.woltje 19d3d5f026 sessions: Filbert row 66 round 4 review, approve 2026-10-21 10:21:00 -05:00
jason.woltje 6c75887b0c sessions: Sage 10-21 afternoon (rows 249, 197, 222, 66, queue revs 2642-2904), with pending Dewey and Researcher lines 2026-10-21 10:19:14 -05:00
jason.woltje 0636721e60 queue: revs 2642-2904 (rows 234, 235, 249 landed; 212, 213, 218, 221, 231, 247 landed; 228, 233, 238, 240, 195, 245, 248 landed; 197 r8 approve, 215 r1 approve, 66 round 4; Case 2 and installer row notes)
Sweeps other seats' revs: Gizmoduck (234, 235, 249), Duckworth (234, 235, 195, 245, 249 verdicts),
Darkwing (233, 240, 195, 245), Filbert (233, 240, 215 verdicts), Gosalyn (212, 221, 231, 247),
Morgana (213), Researcher (228), Launchpad (238), Herb (197, 215), Webby (197, 212, 213, 218, 221,
228, 231, 238, 247, 248 verdicts). The rest are Sage's notes, landings, moves and review requests.
2026-10-21 10:18:29 -05:00
jason.woltje 7b9737953a deferred: row 248 Webby N1, the grant executor test sleep 30 can outlive its test; Duckworth deferred it in the row 249 review (#1751 c30070) 2026-10-21 09:55:13 -05:00
jason.woltje 9de5a57abd row 249: row 235 follow-up, a grant whose install never returned records hostLineLeft, since the remote add can outlive an abort or a timeout (#1751)
An abort or a timeout kills only the local ssh. A non-pty sshd command gets no SIGHUP, so the remote
add can still write its line after the undo's remove. The broker now marks install done only after
`await install(...)` returns. When it never returned, the undo records hostLineLeft with the tag, so
grant.failed and status name the line, hosts.remove refuses until `mosaic hosts sweep` clears it.
The :432 test comment now matches. Row a's enroll does no remote add, so it has no such race.

Candidate gizmoduck/row249-on-8a04 05595794b (Gizmoduck), manifest 3a969bb136bc, approved by
Duckworth in round 1 (rev 2889-2890 request c30068, verdict c30070). Gate on Sage's slot 14:53Z
(sage-2026): grant-executor.test.mjs whole 39/0 and bus 598/0 with no stub left, and C1, which
drops the `if (!installed)` branch, fails G8 and both G23 tests. Webby's N1 on X5 `sleep 30` is
deferred by Duckworth (DEFERRED.md).
2026-10-21 09:54:29 -05:00
jason.woltje 8ca064158a sessions: Filbert row 215 candidate review r1, approve 2026-10-21 08:43:38 -05:00
jason.woltje 8a04f4aa4a row 248: row 235 follow-up, the grant-executor hold-add and hold-remove stubs stop when their test dir is gone, so no stub outlives its test (#1750)
The row 197 r8 D5 proof stopped on a leftover hold-add stub. G23 and G13 write release, finish and
remove the test dir before the stub polls again, so the stub looped forever on sshd's side. 4 runs
of the file left 7 stubs. Both hold loops now also stop once $D is gone.

Candidate 4d99c178b, approved by Webby in round 1 (rev 2883, webby-0080). Gate on Sage's slot
13:05-13:07Z: the whole file 3 runs at 37/0 with no stub left, bus 596/0, and a mutant that drops
the hold-add check leaves 2 stubs. The late remote add the leak exposed is row 249 (#1751).
2026-10-21 08:16:49 -05:00
jason.woltje bc579f7a84 docs: host run sheet section 4, the apt-get download W: line as root is expected, and the profile's sha256 is checked again at /etc/apparmor.d (mos-1168) 2026-10-21 08:14:11 -05:00
jason.woltje 6de1ebc629 plans: row 235 follow-up brief, the grant undo records a host line it can't prove gone (mos-1166, gizmoduck-0139, duckworth-0091) 2026-10-21 08:11:21 -05:00
jason.woltje 540239b6b7 plans: row 235 follow-up brief, the held grant-executor stubs stop when their test dir is gone 2026-10-21 08:03:55 -05:00
jason.woltje 6d5545d442 row 245: row 195 follow-up, F9 runs node as root only at the pinned root-only path, the header and TOOLS.md list what root runs, F6 refuses .. after claude-<pin>/ (Duckworth N1-N3, #1693)
Darkwing's ef8b4439e (darkwing/row245), Duckworth round 1 APPROVE (rev 2849, comment 30043),
manifest 69bc05f4f706, 5 paths. Slot run sage-1961 (darkwing-0107): singles 23/0, mutants 16/16
killed, N1 and N3 among them. DESIGN.md:336-338 stays unedited, superseded by gosalyn-0087.
Sage gate on 856c1826b plus these 5 paths (12:24-12:35Z): case1-accept.test.mjs alone 23/0,
cli 681/0, every scripts/test-*.sh 429/0.
2026-10-21 07:35:39 -05:00
jason.woltje 856c1826bc row 247: C2-20b persona import follow-up, keys from config.yaml print as JSON strings, the dry run applies C2-08's fence and refuses, privateDir refuses an EEXIST race by name (#1749)
Gosalyn's 6d43618a3 (gosalyn/c2-20b), Webby round 1 APPROVE webby-0076 (rev 2852), manifest 6d3f279f692b,
4 paths. Webby's row 221 notes N1, N2 and N4. Slot runs sage-1969 (red base, the builder's comma
regression) and sage-1971 (14/0, M1-M4 killed). M1/M2 sit at :248 in this commit, not :237 as
BUILD.md says (gosalyn-0111). JSON.stringify leaves DEL, C1, U+2028/9 and bidi raw, a known limit
by sage-1964.
Sage gate on 5c6f73ed7 plus these 4 paths (12:10-12:21Z): the test file alone 14/0, cli 680/0,
every scripts/test-*.sh 429/0. f8276c5eb adds only docs/SESSIONS and docs/case2 sheets no test reads.
2026-10-21 07:22:07 -05:00
jason.woltje f8276c5eb3 row 218: C2-12 setup sheet for Melanie and the library survey run sheet (#1719)
Doc only, Sage's sage/row218 89ba79d94 (three commits squashed). Webby APPROVE round 1 (webby-0075,
rev 2848), round 2 (webby-0077, rev 2856) and round 3 (webby-0078, rev 2860). docs/case2/SETUP-SHEET.md
tells Melanie what the agents read, the playground, link and property forms, and what changes for
Bean and Chet, with M5's 1,200 character reply total (mos-1165). It goes to her only after C2-06
lands and on Jason's go (#419). docs/case2/SURVEY-RUN.md is design 10 step 7's run sheet: the
bare mirror as the source, a node check, output lines and refusals from survey.mjs. Claim sources
and rulings are in agents/sage/work/queue-218/BUILD.md. No test reads either doc path.
2026-10-21 07:05:39 -05:00
jason.woltje b65f3a0e84 sessions: Darkwing row 245 slot green, round 1 in review 2026-10-21 06:50:41 -05:00
jason.woltje 5c6f73ed75 plans: C2-03a brief, the mirror HEAD is a symbolic ref to refs/heads/served so the C2-12 survey reads the served commit
The survey reads HEAD and has no ref flag, and design 10 step 7 surveys the bus mirror. Row 222 is not
built yet, so this adds one requirement and one test to its brief before the build starts.
2026-10-21 06:39:33 -05:00
jason.woltje 947101d8a3 sessions: Sage 235, 221 and C2-20b landings, 197 r7 request and proof prep 2026-10-21 06:28:49 -05:00
jason.woltje 335bb3e30f plans: C2-20b brief (row 221 follow-up, Webby N1, N2 and N4) and DEFERRED for N3, Morgana's 213 B3 anchor
Webby approved row 221 round 1 (webby-0073) with four notes that don't block. N1, N2 and N4 are in
scripts/case2-persona-import.mjs and go in before Jason's one import run (sage-1941), as row C2-20b,
owned by Gosalyn and reviewed by Webby. N3 is tooling outside the product and waits in DEFERRED.
2026-10-21 06:23:41 -05:00
jason.woltje 3007664504 row 221: C2-20 Bean and Chet persona import, stage and install script with a count-only dry run, personaTextFault exported from bundle.mjs, yaml 2.9.0 exact, fixture Hermes home and read trace (#1722)
Lands Gosalyn's round 1 candidate, 8f403d0d4 on gosalyn/c2-20-r2 (48f044ccc plus its BUILD.md, on
62ddc96cc), approved by Webby (webby-0073, REVIEW-r1 a0a00630e, rev 2808). 22 paths, manifest
20f05e3a6ad5. No 221 path changed between 62ddc96cc and this commit's parent. Patch-id dfbb754c6fb4, the
same as 62ddc96cc..8f403d0d4. The two state.db fixtures and auth.json are short text stand-ins.

Webby's N1, N2 and N4 go to the C2-20b follow-up row before Jason's import run. N3 (mutants.py:48
anchor) is in DEFERRED.md. The script itself runs only under Jason's go (design 10 step 6, 9.1).

Evidence: Gosalyn's singles on my slot, 419/0 on 48f044ccc (gosalyn-0104).
Gate on my slot, 11:07:50-11:19Z on b1fece6f0 plus the 22 paths: case2-persona-import.test.mjs alone
11/0, harness 140/0 (bundle.mjs changed), cli 677/0, and every scripts/test-*.sh green (auth 15,
conductor 17, config 31, discord 78, extension-package 18, foundation 44, land-work 86, queue 156 and 28,
release 14, task 98, all 0 failed). Lands before row 197 r7: no shared paths, and r7's proof candidates
both carry it (sage-1954).
2026-10-21 06:19:41 -05:00
jason.woltje b1fece6f0a row 235: machine grant row b, raise binding, executor, reconcile, revoke and sweep, creative roles refused at raise, recheck and host.run, N10 (#1741)
Lands Gizmoduck's round 1 candidate, ed4193316 on gizmoduck/row235-e (one commit on 62ddc96cc), approved
by Duckworth (duckworth-0089, REVIEW-r1 d9ecec514 on duckworth/reviews, #1741 comment 30022). 13 paths,
manifest dad377ca8fe4. No 235 path changed between 62ddc96cc and this commit's parent. Patch-id
c355d2fc457d, the same as 62ddc96cc..ed4193316. broker.mjs keeps row 213's import and decision.raise
creative refusal as landed.

Creative roles (mos-1163, mos-1164, sage-1908): #hostDefinition refuses host-creative-role at the raise
and at the step 1 recheck, and #hostAccess refuses it at host.run before reading the grant record, which
stays for hosts.revoke. Duckworth checked from the code that a creative role can't raise, approve or
spend a host grant, and ruled the upsert mutant equivalent.

Evidence: Gizmoduck's runs on my slot. B1-B37 on 5df4b52c5, and run 2 on 534f2254a (10:12:52-10:14:51Z):
grant-host 9/9, grant-executor 37/37 and grant-enroll 24/24, each run whole, with B16, B23, B27, B38 and
B39 killed. B1-B39: 38 killed, and B17 is equivalent (sage-1904).
Gate on my slot, 10:30:55-10:42:43Z on 29ce1e360 plus the 13 paths: bus 596/0 (its first whole run on
this head, duckworth-0089 N1), business 200/0, cli 666/0, and every scripts/test-*.sh green (auth 15,
conductor 17, config 31, discord 78, extension-package 18, foundation 44, land-work 86, queue 156 and 28,
release 14, task 98, all 0 failed).
2026-10-21 05:42:52 -05:00
jason.woltje aa19a2464e sessions: Darkwing row 245 built, slot asked 2026-10-21 05:12:45 -05:00
jason.woltje 29ce1e3602 row 231: C2-01b library reader and survey fixes: control and bidi escapes, front matter list links, an unresolved body embed to a non-.md target is an attachment (#1733)
Lands Gosalyn's round 2 candidate, f37a65857 on gosalyn/c2-01b-r2 (base fefd54e13; commits 3647a5a93,
88daaac50, f37a65857), approved by Webby at merge scope (webby-0072, REVIEW-r2 efd6c1f02 on webby/reviews,
#1733 comment 30005, rev 2787). Nine paths, manifest 312c16fc4f65. No 231 path changed between fefd54e13
and this commit's parent. Patch-id 59b98b1b3e00, the same as fefd54e13..f37a65857.

Evidence: Gosalyn's singles on my slot, 61/0 on r2 (gosalyn-0099; run 1 60/1 was a test order defect).
Gate on my slot, 10:05:31-10:07:52Z on 62ddc96cc plus the nine paths: library 45/0, and every scripts/test-*.sh
green (auth 15, conductor 17, config 31, discord 78, extension-package 18, foundation 44, land-work 86,
queue 156 and 28, release 14, task 98, all 0 failed).
Webby's N4 (README:88 "A link to ... an attachment is R4" next to the embed paragraph) is doc-only and
stays as restored (sage-1917).
2026-10-21 05:09:23 -05:00
jason.woltje 62ddc96cc7 plans: row 195 follow-up (Duckworth N1-N3) and the .53 window row, which carries 195's done condition after its early done move 2026-10-21 05:03:21 -05:00
jason.woltje d6cff0cdd8 deferred: Duckworth's N1-N3 on row 195, the Case 1 acceptance script's root parse order, header and prefix match 2026-10-21 05:01:46 -05:00
jason.woltje 361ba2387f row 213: Case 2 C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file (#1714)
Lands Morgana's round 4 candidate, 2b3e67e36 (base c771a36f4, branch morgana/row213-r4), approved by Webby
(webby-0071, REVIEW-r4 02bb0afb3, rev 2775). Manifest digest 26d4d0ecff56, 35 paths, plus Morgana's
four packet files under agents/morgana/work/queue-213/ (base.txt, BUILD.md, candidate-manifest.sha256,
mutants.py): 39 paths, blobs and modes from the candidate. No 213 path changed between c771a36f4 and
this commit's parent. Patch-id f313e69b6f7e, the same as c771a36f4..2b3e67e36.

Evidence: Morgana's slot run on 2b3e67e36 (sage-1844): singles 191/0 and 31/31 mutants killed.
Gate on my slot (sage-r195land, base 38b83ea5c plus 195's 5 and 213's 39 paths, 09:45:52-09:57:52Z): business 200/0, bus 542/0, harness 140/0, cli 663/0; scripts auth 15/0, conductor 17/0, config 31/0, discord 78/0, extension-package 18/0, foundation 44/0, land-work 86/0, queue 156/0 plus 28/0, release 14/0, task 98/0; the models mutant L1 killed (launch-settings 15/1), restore matched.
Between the gate base 38b83ea5c and 4364dddb0 only docs/plans/2026-10-20_host-credentials.md changed (01e1fe512, 4364dddb0), which no test reads. Row 195 landed as this commit's parent; it shares no path with 213.
Webby's N2 (a host grant for a creative role) is a row 235 acceptance item (mos-1163), not a deferral.
2026-10-21 04:58:30 -05:00
jason.woltje b650146479 row 195: scripts/case1-accept.sh, root's Case 1 acceptance (F1-F11, P1-P11), its test, TOOLS.md entry (#1693)
Lands Darkwing's manifest round 1 candidate, 37171f65b (base 4ced5e63d), approved by Duckworth
(duckworth-0088, REVIEW-m1 6afcee2a5, #1693 comment 29988, rev 2758). Five paths:
scripts/case1-accept.sh (100755), packages/cli/tests/case1-accept.test.mjs, docs/TOOLS.md,
agents/darkwing/work/queue-195/DESIGN.md (r7, approved duckworth-0087) and BUILD.md. No 195 path changed
between 4ced5e63d and this commit's parent. Patch-id 656e43ff25de, the same as 4ced5e63d..37171f65b.

Evidence: Darkwing's slot runs: single 22/22 (sage-1829), 12/12 mutants killed and gate cli 662/0 plus
selftests 429/0 on ddf7dce6c (sage-1865, sage-1870).
Gate on my slot (sage-r195land, base 38b83ea5c plus 195's 5 and 213's 39 paths, 09:45:52-09:57:52Z): business 200/0, bus 542/0, harness 140/0, cli 663/0; scripts auth 15/0, conductor 17/0, config 31/0, discord 78/0, extension-package 18/0, foundation 44/0, land-work 86/0, queue 156/0 plus 28/0, release 14/0, task 98/0; the models mutant L1 killed (launch-settings 15/1), restore matched.
Between the gate base 38b83ea5c and 4364dddb0 only docs/plans/2026-10-20_host-credentials.md changed (01e1fe512, 4364dddb0), which no test reads.
Duckworth's notes N1-N3 go to docs/plans/DEFERRED.md in the next commit.
2026-10-21 04:58:17 -05:00
jason.woltje 4364dddb0d plans: row b (235) host.run also refuses a creative agent at spend time, grant record kept for revoke (mos-1164, sage-1908) 2026-10-21 04:49:11 -05:00
jason.woltje 01e1fe5128 plans: row b (235) refuses a host grant to a creative role, after row 213 lands (mos-1163, Webby N2 on 213 r4) 2026-10-21 04:40:22 -05:00
jason.woltje 38b83ea5c6 sessions: Sage, 212 landing, 197 proof stop and r6, rows 243/244, row 66 r4b 2026-10-21 04:20:46 -05:00
jason.woltje fefd54e137 deferred: N7 lane image base inspect; brief for two gate environment rows (mos-1162)
N7: lane-image.mjs:41 inspects a base BuildKit never tagged (row 197 proof try 1).
Brief: the process.test.mjs mode chmod row and the fixture PI_AUTH_FILE row, both after row 197.
2026-10-21 04:15:06 -05:00
jason.woltje ee7b2f1376 row 212: packages/library, mapping, scope, reader, resolver, R-rules, renderItems, survey (#1713)
Lands Gosalyn's round 3 candidate, a2c72ac95 (commit candidate, base c771a36f4), approved by Webby
(29979, rev 2731). 21 paths: the new packages/library (src, tests, fixtures, package.json, README),
scripts/stack-copy.sh (packages/library/src in COPY_PATHS), packages/cli/tests/stack-copy.test.mjs,
scripts/mosaic and agents/gosalyn/work/queue-212/BUILD.md. The 21-path patch-id, ea84d3e35470, equals
the candidate's.

Evidence: Gosalyn's slot run on round 3: 20 files 451 pass, 0 fail; test-queue 28/0 (gosalyn-0086).
Landing gate (Sage, 08:51-09:03Z, base 4ced5e63d) on a tree that also held row 197's 32 paths:
GATE_PASSED, node tests 981 pass 0 fail (library 39, cli 681, control-board 38, harness 16, webui 51, plus
the scripts/test-*.sh node suites), selftests 429 passed 0 failed. Row 212 lands alone (mos-1162); row
197 waits for an r6 after its lane proof stopped. No 212 path changed between c771a36f4 and this commit's
parent.
`queue review verify-commit 212` -> ok row 212 round 3: a2c72ac95 matches the commit candidate (21 paths).
2026-10-21 04:12:08 -05:00
jason.woltje f6c70f9441 sessions: Darkwing, row 195 build in review (37171f65b, rev 2756), slot green 22/22, 12/12 mutants, gate 662/0 + 429/0 2026-10-21 03:52:15 -05:00
jason.woltje b3b2374eaf sessions: Filbert, row 215 manifest r2 approve 2026-10-21 03:37:47 -05:00
jason.woltje 696255f883 sessions: Filbert, row 215 manifest review, changes requested (B1-B3) 2026-10-21 03:29:39 -05:00
jason.woltje 059577886c deferred: gate runs mount the host pi auth file (Webby N4 on the row 197 proof driver, traced by Herb) 2026-10-21 03:28:33 -05:00
jason.woltje 4ced5e63d4 sessions: Sage, rows 240, 228 and 238 landed (c515273db, 86249ac55, d49b1f2bd), 197 r5 rebuild, Gosalyn 212 slot (sage-1788..1826) 2026-10-21 02:29:11 -05:00
jason.woltje e7078e6a85 deferred: Webby's row 238 N1, the sso-config first-step hint has no business (#1744) 2026-10-21 02:27:43 -05:00
jason.woltje d49b1f2bd3 row 238: stack-release's sso-config first step carries --instance NAME on a named instance (#1744)
Lands Launchpad's round 3 candidate, manifest 1bf831f3 (4c90099ea on launchpad/row238-r4, base
c515273db), approved by Webby (29956, rev 2717). Three paths: scripts/stack-release.sh, packages/cli/tests/
stack-release.test.mjs and docs/TOOLS.md. It is row 200's follow-up N1. Round 3 only merges docs/TOOLS.md
onto the row 240 landing: the landed blob plus the one hunk at :599. The 3-path patch-id, b9d0bb0ee3c7,
is the same in all three rounds, and stack-release.sh stays 100755.

Evidence: Launchpad's slot run on round 2, 2afbfbb01: stack-release.test.mjs 196/0 (sage-1794). Sage's
combined gate with row 228 on c515273db plus both rows' paths (07:11:30Z to 07:23:34Z): packages/seat 165 pass, 0 fail; packages/cli 640 pass, 0 fail; all ten scripts/test-*.sh 585 pass, 0 fail (out/, run.log). Row 238's mutant, the sso-config first step dropping --instance NAME, was killed (2 fails), survived: none, and stack-release.sh was restored to its blob.
`queue review verify-commit 238` matches the manifest on all 3 paths. Round 1's N1 stays on the #1744
carry list.
2026-10-21 02:24:25 -05:00
jason.woltje 86249ac552 row 228: ZAI_API_KEY in the session env allowlist (#1729)
Lands Researcher's round 3 candidate, manifest 82cf98c6 (cf18c42b9 on researcher/row228-r3, base
c771a36f4), approved by Webby (29949, rev 2708). Three paths: packages/seat/src/session.mjs (ZAI_API_KEY
in ENV_ALLOW), packages/seat/tests/session.test.mjs (the pass-through test) and packages/cli/README.md
(the line, with the round 1 and round 2 N1 trims at :711 and :718). Round 3 is the round 2 rebase onto
the row 234 landing; the two seat files carry the blobs approved in rounds 1 and 2.

Evidence: Researcher's slot run on cf18c42b9: session.test.mjs 13/0, host.test.mjs 42/0 (sage-1796).
Round 2's seat and cli runs, 94/0, and its killed mutant stand, since the seat blobs are unchanged.
Sage's combined gate with row 238 on c515273db plus both rows' paths (07:11:30Z to 07:23:34Z):
packages/seat 165 pass, 0 fail; packages/cli 640 pass, 0 fail; all ten scripts/test-*.sh 585 pass, 0 fail (out/, run.log). bus runtime.mjs and webui reads.mjs import only readSessions from session.mjs, which this
change doesn't touch. `queue review verify-commit 228` matches the manifest on all 3 paths.
2026-10-21 02:24:25 -05:00
jason.woltje 59f817c321 sessions: Darkwing, row 195 DESIGN r7 pushed (5425ea2f3), Duckworth review requested (Gitea 29960), row 226 items to Gosalyn 2026-10-21 02:15:49 -05:00
jason.woltje c515273db7 row 240: installer I4, root's host check, its pins and the boot unit; I3's N1, N4 and N5 (#1746, Vikunja #432)
Lands Darkwing's round 1 candidate, manifest 573ece95a4a9 (ad1b23ab2 on darkwing/row240-r3, base
c771a36f4), approved by Filbert (29945, rev 2696). Nine paths: scripts/case1-hostcheck.sh (new),
scripts/case1-pins.sh (new), packages/cli/systemd/mosaic-hostcheck.service (new),
packages/cli/tests/case1-hostcheck.test.mjs (new), scripts/stack-upgrade.sh,
packages/cli/tests/stack-upgrade.test.mjs, docs/guides/host-run-sheet.md, docs/TOOLS.md and
agents/darkwing/work/queue-240/BUILD.md. File modes match the candidate. case1-hostcheck.sh and
stack-upgrade.sh are 100755. case1-pins.sh is 100644, because the host check sources it.

Evidence: Darkwing's first slot run failed one test, the N1 test's after hook (ENOENT once the
fixture had removed its root). The fix b45852daa is test-only. The second run on b45852daa was green:
singles 37/0, mutants M1-M8 8 of 8 killed, cli 639/0, scripts 585/0. Since c771a36f4 no commit
touches the nine paths. Sage's gate on 060a17b54 plus the nine paths (06:45:05Z to 06:56:21Z):
packages/cli 639 pass, 0 fail, and all ten scripts/test-*.sh 585 pass, 0 fail (out/, run.log). `queue review verify-commit 240` matches the manifest on all 9 paths.

Filbert's notes: N1 (the boot unit orders only mosaic-bus@mosaic-stack) and N2 (the run sheet writes
the apt hook before the first --check) are in docs/plans/DEFERRED.md. N3, repeated DRIFT lines after
bwrap is closed, is by design and is noted with N2.
2026-10-21 01:56:47 -05:00
jason.woltje da3051ccc6 deferred: Filbert's row 240 N1 and N2 (29945, #1746); 10-24 window draft Q1 takes the mos-1161 ruling 2026-10-21 01:50:34 -05:00
jason.woltje 060a17b549 sessions: Filbert, row 240 round 1 approve (29945, rev 2696) 2026-10-21 01:41:13 -05:00
jason.woltje ffd90e7fc4 sessions: Darkwing, row 240 in review round 1 (ad1b23ab2, manifest 573ece95, revs 2694 and 2695), with its BUILD-LOG entry 2026-10-21 01:37:57 -05:00
jason.woltje 02709125a6 sessions: Sage, row 234 landing c771a36f4, 235 briefed, rebuilt-row slot order, 10-24 draft NEW (sage-1766..1787)
Doc only. The 10-24 window draft 0 now names NEW c771a36f4 and adds row 233 and the T46 untimed tables.
2026-10-21 01:20:30 -05:00
jason.woltje c771a36f4b row 234: machine grant row a, host enrollment, store records and bus schema 3i to 3j (#1740, Vikunja #436)
Lands Gizmoduck's round 3 candidate, manifest 4782c8b5ecb7 (c783d5e39 on gizmoduck/row234-r3, one commit
on 57f59e0c3), approved by Duckworth: content on round 1 (861182fa6, 29919), the round 2 rebase check
(29928, rev 2670) and round 3 (REVIEW-r3.md 7c8c38b69, posted 29935, rev 2683). 41 paths.

Round 3 fixes the two failures in Sage's combined gate with row 233: bus schema.test.mjs now counts 18
tables and 71 triggers for the new hosts and host_grants tables, and stack-release.test.mjs T46 puts both
tables in the untimed restore-guard list, since every write shares a transaction with a grant.* event.
Round 3 rebuilt on the row 233 landing as one commit. 38 of the 39 round 2 blobs are unchanged and BUILD.md
is append-only. Gizmoduck's singles on round 3: stack-release 196/0, schema 1/0.

Evidence: Gizmoduck's slot runs, 25 of 25 mutants killed (A22b and A23 included, A9 structural), singles on
r2 387/0 over 47 files. Sage's landing gate on the c783d5e39 tree: 06:02:12Z to 06:15:26Z, every package except queue 2890 pass, 0 fail (15 packages; bus 539, cli 622), and all ten scripts/test-*.sh 585 pass, 0 fail.

This is code only. The live stack runs from a pinned copy at schema 3i. The 3i to 3j migration runs in the
10-24 window (mos-1145), agent-run under Q173, with a backup first. Duckworth's N10 (hosts.mjs
Object.hasOwn) and N12 (doc-only) go to row 235's carry list.
2026-10-21 01:15:43 -05:00
jason.woltje caa4b59440 sessions: Sage, row 233 landing 57f59e0c3, 234 gate stop and r3, 240 briefed, 197/238 drift (sage-1742..1758) 2026-10-21 00:47:45 -05:00
jason.woltje 57f59e0c39 row 233: installer I3, stack-upgrade.sh and the host run sheet (#1735, Vikunja #432)
Lands Darkwing's round 1 candidate, manifest 464ce9eb177a (1a1dc80ca on darkwing's branch), approved by
Filbert (29922, rev 2659). Seven paths: scripts/stack-upgrade.sh (new), scripts/stack-release.sh,
docs/guides/host-run-sheet.md (new), docs/TOOLS.md, packages/cli/tests/stack-upgrade.test.mjs (new),
packages/cli/tests/stack-release.test.mjs and agents/darkwing/work/queue-233/BUILD.md.

Evidence: Darkwing's gate on d99166ade (base e65a3ab4d) 1201/0, mutants m1-m8 8 of 8 killed. The N1 fix
08db6ac3f (flock sleep leak in stack-upgrade.test.mjs) reran c0 20/0 with m1 killed (sage-1708); BUILD.md
doesn't record that rerun. Since e65a3ab4d no commit touches the seven paths. Rows 241 and 242 changed bus
and cli files. Only cli tests read the seven paths, so Sage ran the cli suite on 5465fd52d plus the seven
paths: 617 pass, 0 fail (out233/pkg-cli.log, 05:26:33Z to 05:35:00Z). A combined gate with row 234 failed bus schema.test.mjs:522 on 234's new tables, and
Gizmoduck found a second 234 failure (stack-release.test.mjs T46), so Sage stopped it. Row 234 round 3 fixes
both and rebuilds on this landing, since its T46 lines go in a file this commit changes.

Filbert's N1, N4 and N5 go to row 240 (brief b00d7d94d). N2 and N3 are in DEFERRED.md.
2026-10-21 00:35:18 -05:00
jason.woltje 5465fd52d5 sage: DEFERRED, checkCreative doesn't refuse credentials (row 213 r3 Webby N1, mos-1160) 2026-10-21 00:12:52 -05:00
jason.woltje b00d7d94d0 sage: row 240 (I4) carries Filbert N1, N4 and N5 from the 233 review; N2 and N3 go to DEFERRED (29922, sage-1727) 2026-10-21 00:01:26 -05:00
jason.woltje 642a9b87de sessions: Filbert, row 233 round 1 approve (29922, rev 2659) 2026-10-20 23:58:01 -05:00
jason.woltje 16019b80e6 sessions: Darkwing, row 233 in review round 1 (1a1dc80ca, manifest 464ce9eb, revs 2644 and 2647), with its BUILD-LOG entry 2026-10-20 23:51:01 -05:00
jason.woltje 9b53620185 sage: 10-24 window draft 0 counts grant look-alike events before --migrate (row 234 A22b, gizmoduck-0107) 2026-10-20 23:29:34 -05:00
jason.woltje 7662b04902 sage: 10-24 window draft 0 adds rows 232 (e65a3ab4d), 241 (5d4a61776) and 242 (5fb736b92, fc1cb856b); 199 and 210 evidence list due in draft 1 2026-10-20 23:24:00 -05:00
jason.woltje 04ce8dfd56 queue: revs 2639-2641 (row 242 Duckworth approve record, landing note and done) 2026-10-20 23:22:41 -05:00
jason.woltje fc1cb856b0 publish: row 242 follow-up docs, the git socket comments name the hold and the late record; BUILD.md residual and :311 cite (Duckworth N1, N2, N4, #1748)
Megavolt's c5a125f4b on megavolt/row242-docs, parent 5fb736b92 (the row 242
landing). Comments and .md only: git-socket.mjs changes comment lines at
:39-44 and :433-435, BUILD.md records N2 and N4. No test pins either path's
hash. Landed without a gate under the doc-only rule.
2026-10-20 23:17:14 -05:00
jason.woltje c0c4907af9 sessions: Sage, rows 241 (5d4a61776) and 242 (5fb736b92) landed, queue revs 2628-2641 2026-10-20 23:14:49 -05:00
jason.woltje 5fb736b92d publish: a retry after publish-timeout pushes once; the bus holds the decision until its push ends and records a late push (row 242, #1748, P19 440)
Megavolt's candidate 635c10924 on megavolt/row242, manifest 1fbfab6c (3 paths:
agents/megavolt/work/queue-242/BUILD.md, packages/bus/src/runtime.mjs,
packages/cli/tests/publish-socket.test.mjs). Duckworth approved (comment 29889, rev 2639).

Mutants M1 and M2, run by Megavolt on Sage's slot: both killed (publish-socket 8/1 each).
Sage's landing gate on 12b8538e0 (96bac588f plus these 3 paths): 15 packages 2826 pass 0 fail (bus 500, cli 594, seat 164), 10 scripts/test-*.sh suites 429 pass 0 fail plus 156 node tests in the queue suite, 03:59Z to 04:12:44Z

The 3 paths are unchanged on refactor since the candidate's base 8fede5fc6. Follow-ups:
Duckworth's doc notes N1, N2 and N4 go in a later doc-only commit; the late-record notice
to the run is Vikunja 440 comment 432. No live change.
2026-10-20 23:13:06 -05:00
jason.woltje 96bac588fb queue: revs 2628-2638 (row 241 approve record, landing note and done; row 232 landing note and done; row 233 brief re-pin and Darkwing claim; row 242 in review round 1, request 29888) 2026-10-20 22:47:48 -05:00
jason.woltje 5d4a617762 bus: row 241 one decision lands once; a broker test with two checkLand tickets on one decision, where the second recordLand refuses decision-consumed and records nothing (row 241, #1747, P19 441)
Megavolt's candidate 3e3ed303e on megavolt/row241, manifest e8266864 (2 paths:
agents/megavolt/work/queue-241/BUILD.md, packages/bus/tests/land.test.mjs). Tests and
packet only, no source change. Duckworth approved (comment 29870, rev 2628).

Sage's slot on bbf3b1ee8 (0444acd08 plus these 2 paths):
- mutants M1 (#spent recheck, broker.mjs:963), M2 (CAS old value, seat working.mjs:391),
  M3 (land-busy, cli git-socket.mjs:351): all killed
- full gate: 15 packages 2797 pass 0 fail (bus 500, cli 565), 10 scripts/test-*.sh suites 429 pass 0 fail plus 156 node tests in the queue suite, 03:21:13Z to 03:34:16Z

Row 232's landing e65a3ab4d sits between that base and this commit. Its 9 paths are
disjoint from these 2 and touch no bus code. No live change.
2026-10-20 22:34:30 -05:00
jason.woltje e65a3ab4d2 stack-release: --units external for root-owned units, with confirm, revert and gate --since; stack-copy copy --bundle and wrapper --create (row 232, #1734, installer I1)
Blobs of Darkwing's round 2 da2da2f6c on origin/darkwing/row232-r2 (base
8fede5fc6), the 9 manifest paths, manifest 7af3c943. Round 2 is a
rebase-only round over row 230 (c8bc5cf4c): packages/cli/README.md is a
clean 3-way merge with 232's hunk unchanged, and the other 8 paths keep the
round 1 hashes (manifest 7a3b4ea1). Filbert approve round 1 (comment 29829)
and round 2 (comment 29865).

Under --units user every command line and every byte of output is the
base's. Under external the script reads root's system units, starts and
stops none, stages a copy behind AWAITING-UNITS, and confirm gates root's
start on the bus unit's ActiveEnterTimestamp and launches list.

Mutants 17 of 17 killed (BUILD.md). Darkwing's landing gate on da2da2f6c,
03:07:15Z to 03:17:48Z: packages/cli 593 pass 0 fail, 10 scripts/test-*.sh
suites 429 pass 0 fail (plus 156 node tests in the queue suite). Refactor
moved to 0444acd08 since, three commits touching no manifest path. No live
change: the stack runs from copy 4429a104f, and --units user stays the
default.
2026-10-20 22:19:47 -05:00
jason.woltje 0444acd084 queue: revs 2618-2627 (row 232 round 2 approve; row 241 in review round 1, request 29868; row 234 and row 242 claimed)
Gizmoduck: row 234 claim (2618). Darkwing: row 232 rebase round 2 onto 8fede5fc6, in-progress and in-review with request comment 29860 (2619-2621). Filbert: row 232 round 2 record, approve, comment 29865 (2622). Megavolt: row 241 in-review round 1, request failed on the token (2623-2624), row 242 claim (2626). Sage: the row 241 request as jarvis and its resolve to comment 29868 (2625, 2627).
2026-10-20 22:06:29 -05:00
jason.woltje 052ba95c7c sessions: Filbert row 232 I1 review round 2, approve (#1734) 2026-10-20 22:01:43 -05:00
jason.woltje a52d64ed41 sage: 10-24 window draft 0 names the landed commits for rows 210 (f5753c0e8), 237 (d7d943199) and the PRD (0adf8ddfe) 2026-10-20 21:52:36 -05:00
jason.woltje 8fede5fc6a sessions: Sage, row 210 landed f5753c0e8, queue revs 2604-2617 2026-10-20 21:51:47 -05:00
jason.woltje c084d8d423 queue: revs 2604-2617 (row 210 done, landed f5753c0e8; rows 241 and 242 added and briefed, 241 claimed by Megavolt; row 234 briefed)
Sage: rows 241 and 242 added and briefed (#1747, #1748), the row 210 round 3 request as jarvis, the row 210 landing note and done move, row 234 queued to briefed. Gizmoduck: row 210 r5 start and in-review round 3 (2607-2611). Duckworth: row 210 round 3 record, approve (2614). Megavolt: row 241 claim (2606).
2026-10-20 21:50:59 -05:00
jason.woltje f5753c0e8d host access: agent host sudo through the credential store, host.run over broker.sock (row 210, #1707)
Blobs of Gizmoduck's r5 0485384e9 on origin/gizmoduck/row210-r5 (base
ad95c888c), the 23 manifest paths, manifest 688d38b9. Duckworth approve
round 2 (rev 2592) and round 3 (comment 29851, rev 2614). Round 3 is a
rebase-only round over row 237 (d7d943199): broker.mjs is a clean 3-way
merge (610af0c4), the 210 hunks sit away from 237's insert, and range-diff
against r4 c43008749 is all equal.

Mutants 52 of 53 killed on the merged tree. M22b is equivalent (rev 2584);
its coverage moves to row 235. Preflight singles: seat sandbox 10/0, bus
project-runtime 3/0. Gate on ad95c888c plus these paths: 15 package suites
2796 pass 0 fail, 10 scripts/test-*.sh suites 429 pass 0 fail (plus 156 node
tests in the queue suite). Refactor moved to 9d165be8c since, two briefs
touching no manifest path. No live change: the stack runs from copy
4429a104f until the 10-24 re-pin window.
2026-10-20 21:47:11 -05:00
jason.woltje 9d165be8c7 plans: row 237 follow-up N1 brief, a retry after publish-timeout pushes once (duckworth-0069, P19 440) 2026-10-20 20:54:33 -05:00
jason.woltje c25785d3d5 plans: row 237 follow-up brief, one decision lands once, tests at the broker (megavolt-0069, sage-1651, P19 441) 2026-10-20 20:52:31 -05:00
jason.woltje ad95c888c3 sessions: Sage, row 237 and PRD 1.7+1.8 landed, queue revs 2540-2603 2026-10-20 20:49:55 -05:00
jason.woltje 584e2bd827 queue: revs 2540-2603 (rows 199, 200, 230, 237 done; 210 r4 and 238 in review; 232 in review round 1 approve; 233 briefed; rows 239 and 240 added)
Sweeps 64 uncommitted revs. Seats: sage, duckworth (2540, 2581, 2592), gosalyn (2550-2551, 2558-2560),
megavolt (2555, 2574, 2576-2577), webby (2557, 2563, 2599), launchpad (2570, 2590-2591),
gizmoduck (2585-2587), darkwing (2595-2596), filbert (2600).
Landings: 200 9e1085816, 199 cb5fdb064, 230 c8bc5cf4c, 237 d7d943199.
2026-10-20 20:49:45 -05:00
jason.woltje 0adf8ddfe2 prd: Mosaic Stack PRD versions 1.7 and 1.8, status approved version 1.8 (DECISIONS Q192, decision d52febb5, Vikunja #434)
1.7 is PRD-1.7-DRAFT.md (sha256 e896746d), approved by decision d52febb5 (prd.approve, yes). 1.8 is r7
82df17347 (sha256 de079281), approved by Jason in T3, relayed by Mos (mos-1154, DECISIONS Q192), so it has no
in-Stack prd.approve. packages/tasks/requirements.json moves to version 1.8 with 68 ids: COS-3, CRED-3, CRED-4,
GRANT-1 to 4, TRUST-1, TRUST-2 and LAND-2 are new. The tasks and bus requirement pins move to 68 and 1.8.
Written by Honker (honker/prd-17-18 2884ee496), reviewed by Duckworth (duckworth-0075, -0076, -0077).
2026-10-20 20:46:55 -05:00
jason.woltje d7d9431990 bus: one decision, one open publish; checkPublish refuses publish-busy while another ticket holds the decision (row 237, #1743)
The decision is spent only at recordPublish, after the push, so two checkPublish calls on one decision could
both push and land the commit on two fork refs. checkPublish now refuses publish-busy while an open ticket
holds the same business and decision. A refusal drops the ticket and leaves the decision unspent. Broker test
and a socket race test with exactly one git-receive-pack. Residuals 1-3 in BUILD.md; residual 3 (checkLand)
is a follow-up row.

Built by Megavolt (megavolt/row237 e42415935, duckworth-0068 N1), reviewed by Duckworth (approve, comment
29810, manifest bb777cb0). Sage's gate on 816d48b03 plus these paths: M1 killed, every package suite and every
scripts/test-*.sh green.
2026-10-20 20:45:08 -05:00
jason.woltje a0e3d7cfdd plans: installer I4 hostcheck row brief, split out of I3 (darkwing row 233 G2, sage-1650) 2026-10-20 20:38:37 -05:00
jason.woltje 9d24ff9b28 sessions: Filbert row 232 I1 review round 1, approve (#1734) 2026-10-20 20:34:13 -05:00
jason.woltje 6c04b89dae plans: row 194 follow-up brief, the revoke under the writer's lock and a fire test (duckworth-0074, mos-1158) 2026-10-20 20:29:43 -05:00
jason.woltje 6f7dacb417 sage: 10-24 window draft 0 carries PRD 1.7+1.8 (mos-1154, mos-1156, DECISIONS Q192); the live pm cites 1.6 until the window 2026-10-20 20:28:39 -05:00
jason.woltje d21b2615c1 darkwing: row 232 in review round 1 (#1734), BUILD-LOG and SESSIONS lines 2026-10-20 20:28:11 -05:00
jason.woltje 816d48b03e sage: row 42 run log, Jason's pre-S7 checks (mos-1154); PRD 1.8 r7 raise dropped, Jason approved in T3 (DECISIONS Q192, NOTE.md) 2026-10-20 20:22:19 -05:00
jason.woltje 011d41ba6a sage: row 42 S7 dates in the run log (sage-1622, mos-1148 C1 C2, Vikunja 442), 10-24 window draft 0 (mos-1145; NEW open), Vikunja scripts and records: 412/424 landing comments, 237 follow-ups 440/441, P19 405-408 and 411 re-plan comments 416-420, read-tasks.mjs (#1524) 2026-10-20 20:06:39 -05:00
jason.woltje c8bc5cf4cb case2: unit render and host precheck, case2-unit.sh and case2-precheck.sh (row 230, #1732)
Squash of Gosalyn's 96768766c on gosalyn/c2-13a-r2 (base 722136772), the
5 manifest paths, build.patch 56649f9a, manifest fbb0d7e42c67 (Webby
approve round 2, rev 2563). Gosalyn's gate on 96768766c: cli 541 pass
0 fail; scripts/test-*.sh 429 pass 0 fail (summed by script); mutants
10 of 10 killed. packages/cli/README.md is a 3-way merge with row 199
(cb5fdb064): the hunks don't overlap (line 710 vs 1022). host.test.mjs:855
is the only test that reads that file. On cb5fdb064 plus the patch,
case2-unit.test.mjs 30/0 and host.test.mjs 42/0. Refactor moved to
390ea5261 since, a README fix touching no manifest path. The script
runs on a host only in a separate window with Jason's go.
2026-10-20 19:36:22 -05:00
jason.woltje 390ea5261c business: README names nine hard waits, git.push.remote included; row 199 added it to GATED_ONLY (row 199 review N3, duckworth-0068, #1697)
Megavolt's 5d7e4d26e on megavolt/n3-readme, from cb5fdb064. Doc-only: no
test or scripts/test-*.sh reads packages/business/README.md. The text
matches packages/business/src/vocabulary.mjs, where GATED_ONLY has nine
actions.
2026-10-20 19:29:22 -05:00
jason.woltje cb5fdb064f credentials: a credential store per agent, its own git identity, pushes to a remote (row 199, #1697)
Squash of Megavolt's 627979a17..86963d281 on origin/megavolt/row199-on-190
(base 00d4cc1f6, row 190's landing), the 37 manifest paths, build.patch
05383075, manifest 35f900fd (Duckworth approve round 1, comment 29779,
rev 2540). Mutant batches 1 and 2 in agents/megavolt/work/queue-199/BUILD.md:
batch 2 26 of 28 killed, M17 and M27 equivalent. docs/TOOLS.md is a 3-way
merge with row 200 (9e1085816): row 199's hunk applies unchanged (patch-id
d88379111e2e) and no test reads the file. Gate on 9e1085816 plus the patch:
15 package suites 2735 pass 0 fail, 10 scripts/test-*.sh suites 429 pass
0 fail (plus 156 node tests in the queue suite). Refactor moved to 76134dfde
since, a brief touching no manifest path. No live change: the stack runs from
copy 4429a104f until the 10-24 re-pin window.
2026-10-20 19:23:12 -05:00
jason.woltje 76134dfde1 plans: row 200 follow-up N1 brief (#1744, Launchpad, Webby): the sso-config hint carries --instance NAME 2026-10-20 19:12:34 -05:00
jason.woltje 9e10858160 stack-release: activate --sso for the Q175 phase 2 window (row 200, #1698)
Squash of Launchpad's 30e4bb1a3..9df40878f (round 3 on row 190's landing
00d4cc1f6), the 3 manifest paths: scripts/stack-release.sh,
packages/cli/tests/stack-release.test.mjs, docs/TOOLS.md. Manifest
b3ead55c8b48 (Webby approve round 3, rev 2539, comment 29772).
Patch b2a8f5de18b7. Gate on 9df40878f: 15 package suites 2675 pass
0 fail, 10 scripts/test-*.sh suites 429 pass 0 fail (plus 156 node tests
in the queue suite). Mutants 13 of 14 killed, M12 the ruled equivalent
(sage-1446). Refactor moved to 722136772 since the base, touching none of
the 3 paths. No live change: the stack runs from copy 4429a104f until a
re-pin.
2026-10-20 19:09:00 -05:00
jason.woltje 7221367728 plans: row 194 item 7 takes the restart route (honker-0044) and item 8 revokes on fire and delete before the business file changes (row 199 D13), with tests and mutants 2026-10-20 18:51:20 -05:00
jason.woltje a304dad947 plans: C2-03a askpass ruling (megavolt-0060 to -0063, dewey-0765, gosalyn-0068): fixed argv with followRedirects=false and credential.helper reset, LC_ALL=C, exact-prompt helper with row 199's HOST/NAME checks, no userinfo; tests and four mutants 2026-10-20 18:42:52 -05:00
jason.woltje 919a84738b plans: #436b build brief (#1742, schema 3j-3k, design r5 1be535aca) and row 199 N1 follow-up (#1743); row 194 carries 199 N2; 193b2 moves to 3k-3l after #436b 2026-10-20 18:39:24 -05:00
jason.woltje 2d8687bcfd sessions: pending lines from Darkwing, Rocko, Launchpad, Morgana, Honker, Gander, Herb, Dewey and Sage (2026-10-20 18:45Z to 23:32Z) 2026-10-20 18:32:42 -05:00
jason.woltje 1c46d0abf6 queue: revs 2456-2539. Morgana row 213 in-review rounds 1-2 (2456-2476); Gizmoduck row 210 in-review 1 (2458-2459); Gosalyn row 212 in-review r1 and row 230 start (2464-2470); Duckworth row 210 record (2466); Webby row 213 verdicts r1-r2 and row 200 r3 approve (2469, 2500, 2539); Darkwing row 190 r3 in-progress through row 232 in-progress (2501-2514); Filbert row 190 record and row 66 r3 approve (2504, 2526); Dewey row 66 r3 approve (2529); Megavolt row 199 in-review round 1 (2535-2536); Launchpad row 200 r3 in-progress and in-review (2509-2525); Sage requests, row 190 done, rows 232/233 added and briefed, row 195 after 190 and 233, brief re-pins of 226/230/232/233, row 66 round 3, the row 200 r3 request as jarvis, machine grant rows 234 and 235 added and row 206 after 234 with its 3k brief and piece, the row 199 r1 request as jarvis (2460-2538, 57 revs) 2026-10-20 18:32:06 -05:00
jason.woltje f519fb1d26 plans: machine grant rows a (#1740, schema 3i-3j) and b (#1741) briefs from design r6 f9039682e; 193b2 moves to schema 3j-3k after row a 2026-10-20 18:24:28 -05:00
jason.woltje b8236a7e13 sessions: Filbert row 66 round 3 APPROVE 2026-10-20 18:20:31 -05:00
jason.woltje c7675476b1 sage: P19 catch-up comments 411-413 on 413 (row 189 landed, no-send activate), 424 (row 210 status) and 414 (rows 201 and 204 landed) 2026-10-20 18:19:49 -05:00
jason.woltje 9de7ad8292 sage: row 66 r3 packet, rebase of r2 onto c40d846cb (sage/row66-r3 2369c00e36, same patch-id, manifest 42e3876ca168) 2026-10-20 18:15:59 -05:00
jason.woltje c40d846cb8 sage: Vikunja comment scripts and read-backs for row 192 landing (P19 412) and the row 190 W11 comments 406-409 2026-10-20 18:12:18 -05:00
jason.woltje 01b40d3576 plans: C2-13a rulings for gosalyn-0065. G1: default instance only, no --instance, a named instance is a follow-up. G2: library refuses --sso for 10-27, preview set and proof pair stay, --sso waits for Q175 phase 2. Line cites moved to their places after rows 190 and 192 (webui-service.sh, stack-copy.sh, bus-service.sh, serve.mjs), and the Console's WantedBy replaces :41 2026-10-20 18:09:04 -05:00
jason.woltje ad6f0be7aa plans: installer I1/I3 brief, the stack user's stack-release.sh runs from its own clone of the release bundle (darkwing question, sage-1561) 2026-10-20 18:05:14 -05:00
jason.woltje ffc124bf68 plans: installer I2 re-brief of C2-13a (row 230, #1732) and C2-13b (row 226, #1727) for Vikunja #432. Q189: both units WantedBy=multi-user.target, root's 2c step enables both and starts the bus then the Console, no hand start at step 9 (gosalyn-0064, mos-1122, mos-1123). Row 190 landed first, so row 230 renders @CONFIG_ENV@ and @SESSION_ENV@ 2026-10-20 18:02:39 -05:00
jason.woltje 8a14931fa4 plans: installer briefs I1 (Gitea #1734) and I3 (#1735) for Vikunja #432 (mos-1118, mos-1122). I1 builds on row 190's landing 00d4cc1f6; I3 carries the root lock finding, duckworth-0055's notes, the per-host enrollment step and the credential slot table 2026-10-20 17:56:07 -05:00
jason.woltje 0272383f58 sessions: Darkwing row 190 landed as 00d4cc1f6, row 195 r6 approved and held 2026-10-20 17:53:27 -05:00
jason.woltje 00d4cc1f69 stack: several Mosaic Stack instances on one host (row 190, #1688)
Squash of Darkwing's 176a6bf4b..df65a3d4c on origin/darkwing/row190-r3,
the 25 manifest paths, build-r3.patch efe898c2, manifest cc0ea9e8
(Filbert approve round 3, comment 29713). Design revision 3 in
agents/darkwing/work/queue-190/DESIGN.md. Gate on base d0bbc5c1c with
the patch: 15 package suites 2664 pass 0 fail, 10 scripts/test-*.sh
suites 429 pass 0 fail (plus 156 node tests in the queue suite).
Refactor moved to 99a406ed3 since, a window record touching no manifest
path. Mutants 20 of 20 killed (BUILD.md, round 3). No live change: the
stack runs from copy 4429a104f until a re-pin.
2026-10-20 17:49:06 -05:00
jason.woltje 99a406ed3c sage: re-pin window 490e2771d to 4429a104f (row 192) record, STATE.md (sha256 a4d0a7b12f431f15), W.txt (ca9ad4a09059d3b7), S1/S2, matrices, W10 browser check and diagnosis (screenshots not committed), Mos's P3 rehearsal and flags check, W11 reads. Plan corrections after the window: NEW is 103 commits past OLD (not 106), the P3 flags note (mos-1107), and W10's line g and corrected browser checks (mos-1135, mos-1137). PRD 1.8 raise record, decision 3a6c8fb9 (mos-1140) (#1690; P19 386, 412) 2026-10-20 17:46:36 -05:00
jason.woltje d0bbc5c1cb business: 193a access level check widened to viewer (row 204, #1702)
Squash of Morgana's 7f07da4aa..f869fd062 on origin/morgana/row193a (diff
from R1's candidate 44e36fc4c), the three manifest paths, manifest d38de8c9
(Duckworth approve, comment 29595). LEVELS gains collaborator and viewer,
covers() takes a required least level, and an unknown grant level covers
nothing. Gate on base 4385b9ec1 with the patch: packages and
scripts/test-*.sh 2789 pass 0 fail. Refactor moved to c9f374284 since, a
SESSIONS line only. Mutants 7 of 7 killed (agents/morgana/work/queue-193a/
mutants.log on the branch). No live change.
2026-10-20 17:25:27 -05:00
jason.woltje c9f374284b sessions: Filbert row 190 manifest round 3 APPROVE 2026-10-20 17:16:20 -05:00
jason.woltje 4385b9ec18 plans: Case 2 briefs. C2-13a factory profile from gosalyn-0057 items 1-3 (--pm, --claude under the --node rule, no proxy flags), row 190 tokens to whichever lands second. New C2-01b for row 212's notes N1-N3, N4 to the C2-12 setup sheet, N5 to C2-10b (webby-0041, sage-1517) 2026-10-20 16:52:39 -05:00
jason.woltje afad4993f6 business: FED R1, access.json loader and validator with covers (row 201, #1699)
Squash of Gizmoduck's ae3c36f24..44e36fc4c, the three manifest paths,
manifest 225597146daf1c0e3ffe2049636095733f40a3395318f8ba07dad1bf08c0c520
(Duckworth approve, comment 29581). Gate on 44e36fc4c, run by Gizmoduck in
Sage's slot: business suite 173 pass 0 fail, scripts/test-*.sh 10 scripts
422 pass 0 fail, mutants 44 of 44 killed. index.mjs gains one additive
export line. No live change.
2026-10-20 16:47:42 -05:00
jason.woltje f41729178e plans: re-pin plan 490e2771d to 4429a104f (row 192, Q175 phase 1), npm ci in the new copy, for Mos's check (mos-1099 item 3) 2026-10-20 16:39:56 -05:00
jason.woltje 305857c8f9 queue: revs 2362-2455. Sage: Case 2 rows re-pinned to design r11 (5b8def2f1, row 226 to C2-13b), row 230 C2-13a added and briefed (#1732), row 215 (C2-04) assigned to Herb, row 192 landed 4429a104f with landing note and done, row 228 review request resolved, plus earlier Sage revs. Other seats' revs: Filbert 2362, 2447 (row 190 verdicts); Herb 2395-2396; Webby 2420, 2428 (rows 197/228 verdicts); Morgana 2421 (row 213 claim); Researcher subagent 2422-2424 (row 228); Gosalyn 2426 (row 212 claim); Darkwing 2431, 2433, 2438 (row 190) 2026-10-20 16:35:11 -05:00
jason.woltje 176a6bf4b9 sessions: Filbert row 190 manifest round 2 CHANGES (base drift on row 192) 2026-10-20 16:29:20 -05:00
jason.woltje 5b8def2f14 plans: Case 2 row briefs from Gosalyn's eac92d956 (design r11, approved webby-0038: C2-13 split into C2-13a unit render and host precheck and C2-13b install script and library precheck, C2-04 owner Herb, Console unit without [Install] per mos-1097) 2026-10-20 16:22:47 -05:00
jason.woltje 4429a104f6 webui: Q175 phase 1, the Console verifies SSO itself under --sso, OIDC RP with oauth4webapi 3.8.8 (row 192, #1690)
Squash of Launchpad's launchpad/row192 53bb59462..37582e820, manifest round 2
6f8cee147713c53652b58d47c5efd15ed47da1efbef6a126464857269b0b7787 (18 paths).
Sage's gate on 088087ac9: package suites 2758 pass 0 fail, scripts/test-*.sh
429 pass 0 fail; mutants 22 of 23 killed, #23 (second origin check) declared
equivalent by Launchpad (sage-1407). New dependency oauth4webapi 3.8.8: the
canonical checkout and any stack copy need npm ci. No live change; the live
re-pin is a window with Mos (mos-1099 item 3).
2026-10-20 16:21:29 -05:00
jason.woltje 32b54ac3b0 plans: Case 2 row briefs from Gosalyn's 51006710a (webby-0034 N1/N2 and webby-0035: row A README :711 one line per variable, C2-13 owns README :713 and says the precheck prints only variable names) 2026-10-20 15:57:07 -05:00
jason.woltje 6025ddd9cf plans: Case 2 row briefs from Gosalyn's a2d94b2c2 (Webby r9 notes, webby-0032: row A README line on claude-session.env and the dist/providers/data/zai.json path, C2-13 XDG_DATA_HOME precheck and last-assignment-wins key file parse) 2026-10-20 15:55:52 -05:00
jason.woltje ecbfd6deb6 plans: Case 2 row briefs from Gosalyn's 29e85196b (design r9 for mos-1090: row A out of scope points at design 10 step 2d, C2-13 precheck checks the hub key file by name and mode, claude marker check only for claude-code harness, EnvironmentFile= fixed in case2-unit.sh) 2026-10-20 15:44:09 -05:00
jason.woltje 0498c0613f agents: Gosalyn seat dir, README from gosalyn/case2-design, so Case 2 rows can name the seat as owner (#416) 2026-10-20 15:43:18 -05:00
jason.woltje 49d1f3a231 plans: Case 2 slice row briefs plus rows A and B, from Gosalyn's f325657c7 on gosalyn/case2-design (#416, design r8, C2-13 copy id and profile asks for row 195, C2-20 count-only note, row A ZAI_API_KEY per mos-1086/mos-1089, row B #428 after 10-27) 2026-10-20 15:41:39 -05:00
jason.woltje 628e2ca5cd sessions: Filbert row 190 manifest round 1 CHANGES (base drift on row 189) 2026-10-20 15:40:15 -05:00
jason.woltje 088087ac96 queue: revs 2248-2361+. Sage: rows 210 (#424 host access, Gizmoduck) and 211 (#426 force-push guard, Rocko) added and briefed; row 195 re-briefed to the Case 1 install on .53; review requests for 200 r1/r2 and 202; plus earlier Sage revs. Other seats' revs: Launchpad 2254, 2283-2284, 2286, 2311-2313, 2328-2329, 2344, 2347-2348 (rows 192/200); Honker 2255; Morgana 2257, 2320, 2326-2327 (row 204); Gander 2262; Herb 2265; Dewey 2268; Bushroot 2273; Megavolt 2276; Webby 2303, 2316, 2339, 2355 (row 192/200 verdicts); Gizmoduck 2304, 2318-2319 and the row 210 claim (rows 201/210); Quackerjack 2317, 2341-2342 (row 202); Duckworth 2323, 2335, 2353 (rows 201/204/202 verdicts); Darkwing 2354, 2357 (row 190 in-review) 2026-10-20 15:34:55 -05:00
jason.woltje a0fbefd48d plans: pre-push force-push hook and failing busy check brief (Vikunja #426, mos-1078, mos-1083; owner Rocko, reviewer Dewey) 2026-10-20 15:31:17 -05:00
jason.woltje f24f2cd40d plans: row 195 brief becomes the Case 1 install on 10.1.1.53 (Q181 hosts, Q182/Q183 recorded acceptance, Q185 existing account, chained on rows 190 and C2-13; ops-01 root steps under #423 step 1; sage-1439) 2026-10-20 15:24:33 -05:00
524 changed files with 71621 additions and 1781 deletions
+11 -6
View File
@@ -25,10 +25,15 @@ green at every step. Not production software — a proven foundation.
1. **Root is bootstrap-only.** First-class system configuration lives at the
repository root; everything else gets a dedicated directory (`roles/`,
`contracts/`, `missions/`, `tasks/`, `docs/`). Do not add new files to root.
2. **Configuration**: `~/.config/mosaic-dev/config.json` is the sole system
config — created only by `scripts/bootstrap.sh`, never overwritten,
fail-closed on any problem. Repo-scoped role authority lives in
`roles/*.json` (versioned, reviewed commits only).
2. **Configuration**: each stack instance has exactly one system config. The
default instance's is `~/.config/mosaic-dev/config.json`. Instance NAME's
is `~/.config/mosaic-NAME/config.json`, and its data root carries a marker
that names it. `~` is the home of the OS user that runs the instance. A
process reads only the config `MOSAIC_CONFIG` names, or the default's when
that is unset. Configs are created only by
`scripts/bootstrap.sh [--instance NAME]`, never overwritten, fail-closed on
any problem. Repo-scoped role authority lives in `roles/*.json`
(versioned, reviewed commits only).
3. **Secrets** never enter the repository or container images; auth is
runtime-only (read-only mount or environment variable).
4. **Contracts** (`contracts/`) are immutable and image-baked. Missions and
@@ -177,8 +182,8 @@ Full reference — usage, fields, exit codes, safety notes:
## Data map (canon)
- `~/.config/mosaic-dev/config.json` — system config (user-authored; never
auto-written).
- `~/.config/mosaic-dev/config.json`, or `~/.config/mosaic-NAME/config.json`
per named instance: system config (user-authored; never auto-written).
- `<dataRoot>` (from config; default `~/.mosaic-dev`):
- `runs/` — write-once run evidence (`result.json`, snapshots, `stderr.txt`)
- `sessions/` — pi JSONL session trees, one directory per named session
+15
View File
@@ -5174,3 +5174,18 @@ Before: managed project/thread verbs had no Console conversation area. After: fi
2026-10-20 04:44:54 UTC | Row 178 round 2 preparation | Before: Dewey C1 five render guards lacked killing tests. Added independent refusal fixtures, a temporary uid+1 id stub with cleanup, and empty loopback proof case; activation ordering recorded in r2 BUILD. Product unchanged; syntax/text preflight only, no test or mutant execution. Pending row 181 rebase and Sage slot.
2026-10-20 05:10:34 UTC | Row 178 #1676 round 2 verified | Before: C1 render refusals lacked killing tests and row 181 base was pending. Rebased onto 931651f2d, kept all COUNTED/count code and tests, appended T178 tail; added independent owner/collision/absolute/charset/header and empty-loopback proof tests. Four files singly 156/156, all 24 mutants killed, metadata and sandbox fixture probes pass; scratch sweep empty. Packet agents/rocko/work/queue-178/r2/, candidate 9300a670, inventory d0dcde7b (59/59), scan 58 files no findings; in-review revs 2166-2167, expected no-token request failure. No live actions or commits.
2026-10-21T01:28:05Z | Darkwing | row 232 #1734 installer I1 build, round 1 in review | Before: stack-release.sh managed only user units (systemctl --user), and stack-copy.sh had no bundle copy. After: --units external (adopt, activate, rollback --to, status, confirm, revert, gate --since, AWAITING-UNITS, stale-start, external-pm, --plan --expect-from) and stack-copy check --units external, copy --bundle FILE SHA, wrapper ID --create; user mode byte-identical to the base by test. First build 0af0c73c6 on 00d4cc1f6; rebased onto row 200 (cb5fdb064) as a0ffabc3f on darkwing/row232-r1; --sso refused under external (D9), row 200 config_checks run under external (D10). Gate on Sage's slot (sage-1618): cli 561/0, every scripts/test-*.sh green; 17/17 mutants killed. Results commit 84f1a91e6 (pushed, seat branch only). Manifest 7a3b4ea1 (9 paths), queue revs 2595-2596, request comment 29825, reviewer Filbert. Packet agents/darkwing/work/queue-232/BUILD.md.
2026-10-21T04:50:44Z | Darkwing | row 233 #1735 installer I3 build, round 1 in review | Before: root had no upgrade driver for root-owned units and no host run sheet; stack-release.sh had no rehearse --backup last and no external plan refusals. After: scripts/stack-upgrade.sh upgrade and rollback (r6 A1-A5, A2f, R1-R4) with 20 tests, docs/guides/host-run-sheet.md, TOOLS.md lines, and on Sage's rulings stack-release.sh --backup last, BACKUP_STORE and the --plan refusals open-activation and migrate-required (3 tests). Base e65a3ab4d (row 232's landing). Gate on Sage's slot (sage-1704) on d99166ade: cli 616/0, every scripts/test-*.sh green, 1201/0 summed by awk; m1-m8 8/8 killed. Correction: the process sweep after the gate found an orphaned sleep 60 from the lock test's flock holder (N1); 08db6ac3f fixes it test only, Sage ruled the gate stands (sage-1707), rerun c0 20/0 and m1 killed with 0 left (sage-1708). Tip 1a1dc80ca (pushed, seat branch only). Manifest 464ce9eb (7 paths), queue revs 2644 and 2647, request comment 29901, reviewer Filbert. Packet agents/darkwing/work/queue-233/BUILD.md.
2026-10-21T06:37:41Z | Darkwing | row 240 #1746 installer I4 build, round 1 in review | Before: root had no host check for the bwrap AppArmor profile, its pins or a boot unit, and stack-upgrade.sh exited 1 args-failed when the args move after A5 failed, while exit 1 means the old copy runs. After: scripts/case1-hostcheck.sh (seven items, --check, --enforce, --upstream), scripts/case1-pins.sh, mosaic-hostcheck.service and 16 tests; the args move failure exits 5 args-failed, documented in TOOLS.md and run sheet section 6 and tested; run sheet section 3 puts the pinned npm on PATH and names the bin/claude.exe --claude path; sections 8 and 10 have precheck lines. Verification: slot run r1 on 47ec9fe2f failed at the singles (the new N1 test's after hook threw ENOENT after the fixture cleanup removed the root); b45852daa drops the hook; r2 on b45852daa green: singles 16/0 and 21/0, c0 plus M1-M8 8/8 killed, cli 639/0, scripts 585/0. Candidate darkwing/row240-r3 ad1b23ab2 on c771a36f4, manifest 573ece95 (9 paths), revs 2694 and 2695, comment 29943.
2026-10-21T09:00Z | Herb | row 215 C2-04 implementation started | Before: business files reject library entries and root consumers assume code projects; the organization writer can change all project mappings. Plan: approved manifest 2522025af on C2-01 a2c72ac95, Filbert reply to herb-0056, scope sage-1848/1852/1860. Focused slot sage-1869 observed expected red mapping and M9 cases, then 14/0 green for new T1/T2 cases, no skips; clear herb-0062 with empty test/fixture process scan. After that clear, T3/T4 tests and host-only mirror/bus implementation continue under sage-1874, unrun while Sage holds the slot. Full singles, suites and implementation review remain pending. No source commit, push or live change.
2026-10-21T13:29Z | Herb | row 215 C2-04 source verified for candidate review | Before: business code assumed every project had a root and organization writes could replace project mappings. After: library mappings load without agent checkouts or publish grants, project mappings are protected before and after serialization, host-only bare mirrors share a bounded publication lock with sync, and library.file/library.status are human-only async bus reads. Base f8276c5eb preserves all nine landed overlaps. Granted sage-2008 whole singles: 16 files, 451/0, no skips or cancellations, ended 13:27:27.279728Z; all source hashes unchanged, process scan empty, no own-path drift through 8a04f4aa4. Full landing gate and implementation review remain pending; no HTTP, sync writer or live change.
2026-10-20 19:09:44 UTC | Gander row 196 #1694 design phase | Before: tester TCP reaches every local uid and human proofs do not bind the socket peer. After: design draft at agents/gander/work/queue-196/DESIGN.md inventories listeners, proposes private tester HTTP and SO_PEERCRED binding, aligns the per-request principal contract with Morgana and Gizmoduck, and lists migration, tests, mutants and same-uid limits. Python versus other credential options and Console provenance require Duckworth ruling. Documentation only. No product code, suite, live probe, secret access or deployment.
2026-10-20 19:16:20 UTC | Gander row 196 #1694 design r2 | Before: Duckworth r1 requested concurrent helper-fd evidence (B1) and selected O1 with interpreter, proof-only and image conditions. After: design adds eight concurrent genuine proof processes plus a racing copier, M20 swapped socket and post-helper socket survival, root-owned system ELF pinning and activation evidence, safe gid validation, cap traffic without helpers, and unchanged P19 R2 text. Mos accepted socket-only tester access (mos-1015 via sage-1363); any authenticated browser relay is a separate row. Documentation coverage checks passed. No product code, suites or live action.
2026-10-20 19:21:19 UTC | Gander row 196 #1694 design approval and N5 | Before: Duckworth approved r2 (duckworth-0009, cb6d840df) with a nonblocking interpreter-upgrade note. After: r3 chooses startup safety revalidation, appends actual interpreter identity to bus/peercred-starts.jsonl and retains old activation evidence. Operator re-pin uses the next reviewed copy, respecting already-active refusal. Added task-by-task PLAN.md for inline implementation and independent manifest review. Documentation checks passed. No product code, suites or live action.
2026-10-20 19:34:28 UTC | Gander row 196 build paused at design mismatch | Before: approved design promised Unix tester cleanup only for the owned inode. After: scratch test shows Node/libuv close unlinks a replacement inode before JS close handlers. Evidence agents/gander/work/queue-196/evidence/tester-cleanup-blocker.txt. Sent Sage/Duckworth gander-0016/0017, recommending explicit same-uid residual with private 0700 parent retained. Interpreter, concurrent peer, principal, human, peer-transport, transport, tester and process single files passed. Partial code uncommitted, tester transport not wired. No suites, mutant batches or live actions.
2026-10-21 18:45 UTC | Gander row 196 preservation before rebase | Sage-2051, Mos order mos-1193, explicitly authorizes commit and fast-forward push of the current build before deferred full verification. Product and evidence preserved together; prior focused checks and stopped gates are recorded in the row196 packet, with no claim of full acceptance. Next: rebase onto a36523713 on new gander/row196-r2, report range-diff/patch-id/conflicts, then await Sage's clear for M1-M29 and all requested suites. No live action, force push, branch deletion or tests during hold.
+24 -4
View File
@@ -75,7 +75,8 @@ session's workspace and its own process group. Same contract, plus:
| `MOSAIC_POLICY_FILE`, `MOSAIC_TOOLS_FILE` | The bundle's gate policy and typed tools. |
| `MOSAIC_TOOL_SOCKET` | The runner's tool socket, which the typed tools call. |
| `MOSAIC_TURN_MARKER` | `pi`: the extension writes it at `agent_end`; a pi turn that exits 0 without it failed. |
| `MOSAIC_EXTENSIONS` | `pi` only: extension files loaded with `-e`; a missing one exits 2. |
| `MOSAIC_EXTENSIONS` | `pi` only: extension files loaded with `-e`; a missing one exits 2. The pi module names two: the gate and typed tools extension, then the unslop check's `tools/unslop-hook/extension.ts` (row 66). |
| `MOSAIC_UNSLOP_LOG` | The unslop check's JSONL log, `<run>/unslop.jsonl`, which the runner sets for both adapters and reads after each turn (row 66). |
| `MOSAIC_CLAUDE_SETTINGS`, `MOSAIC_CLAUDE_MCP_CONFIG` | `claude` only: the bundle's gate hook and MCP server; required. |
| `MOSAIC_THINKING` | The route's thinking level (row 65), which the harness module's `levels()` already checked. `pi` passes `--thinking` (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`) and omits it when unset, as for `run-task.sh` workers. `claude` passes `--effort` (`low`, `medium`, `high`, `xhigh`, `max`) and requires it; unset exits 2. |
| `MOSAIC_CLAUDE_BIN` | `claude` only: absolute path of the CLI to run, from the host's environment (the bus unit pins it). Unset or empty runs `claude` from `PATH`; a relative path or a non-executable file exits 2. |
@@ -86,7 +87,8 @@ The layers each adapter relies on, and what they don't cover, are in
### A role's skills (row 64)
A managed session's skills reach both adapters as text in the bundle's
`prompt.md`, which they pass with `--system-prompt`. Neither adapter loads a
`prompt.md`, which they pass as a file: `pi` with `--system-prompt <path>`
and `claude` with `--system-prompt-file <path>` (row 213). Neither adapter loads a
skill of its own: `pi` keeps `--no-skills`, since role sessions never get
`MOSAIC_SKILLS`, and `claude` keeps `--disable-slash-commands` and
`--restricted`. Sage chose this on 2026-10-19 from a probe, recorded in
@@ -104,5 +106,23 @@ skill of its own: `pi` keeps `--no-skills`, since role sessions never get
The cost is prompt size: a role's skill text is in every turn, where the
other routes load a skill when it's used. The bundle caps a role's skills at
64 KiB together, since `claude` passes `prompt.md` as one argument and Linux
caps one argument at 128 KiB.
64 KiB together. The cap dates from when `claude` passed `prompt.md` as one
argument, which Linux caps at 128 KiB; row 213 passes the file instead, so no
local user reads the prompt in `/proc/<pid>/cmdline`. Pi 0.85.1 uses the path
string itself as the prompt when the file is missing or can't be read, so the
pi adapter exits 2 unless the path is a readable regular file.
### The unslop check (row 66)
Both adapters run the mechanical writing check from `tools/unslop-hook`,
driven by one `lists.json`. `pi` loads it as its second `-e` extension.
`claude` runs `claude-hook.js` from the bundle's `settings.json`: the Stop
hook checks the turn's replies, and the UserPromptSubmit hook adds the
correction notice as context on the next turn when the last reply had
tells. Each hook command ends `; exit 0`, so neither blocks. Both log to
`MOSAIC_UNSLOP_LOG`, and the runner adds a note for the operator to the
next RESULT it sends (`packages/harness/README.md`, the runner). The
harness modules mount the three files read-only at their own paths, and
the manifest records their digests. `MOSAIC_UNSLOP_HOOK` and `UNSLOP_LISTS`
aren't in the session environment's allowlist, so a managed session can't
switch the check off or swap its lists.
+10 -3
View File
@@ -27,6 +27,11 @@ need MOSAIC_CLAUDE_MCP_CONFIG
for f in "$MOSAIC_SYSTEM_PROMPT_FILE" "$MOSAIC_CLAUDE_SETTINGS" "$MOSAIC_CLAUDE_MCP_CONFIG"; do
[ -r "$f" ] || { echo "claude adapter: not readable: $f" >&2; exit 2; }
done
PROMPT_FILE=$MOSAIC_SYSTEM_PROMPT_FILE
case "$PROMPT_FILE" in
/*) ;;
*) PROMPT_FILE="$(pwd)/$PROMPT_FILE" ;;
esac
[ "${MOSAIC_INTERACTIVE:-}" != "1" ] || { echo "claude adapter: interactive mode isn't supported" >&2; exit 2; }
CLAUDE=claude
if [ -n "${MOSAIC_CLAUDE_BIN:-}" ]; then
@@ -56,7 +61,10 @@ fi
# Flags:
# -p one-shot: print the answer and exit
# --output-format text stdout is the answer only
# --system-prompt the generated prompt replaces the default
# --system-prompt-file the generated prompt replaces the default, given
# as a file so no local user reads it in
# /proc/<pid>/cmdline (row 213); absolute, since
# the cd above changed the working directory
# --effort the instance's thinking var (row 65; 2.1.302 takes
# low, medium, high, xhigh, max)
# --restricted no user/project/local settings files; --settings
@@ -76,10 +84,9 @@ fi
# text in the system prompt (row 64)
ALLOWED=mcp__mosaic
[ -z "${MOSAIC_TOOLS:-}" ] || ALLOWED="$MOSAIC_TOOLS,mcp__mosaic"
PROMPT_CONTENT="$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")"
"$CLAUDE" -p "$MOSAIC_REQUEST" \
--output-format text \
--system-prompt "$PROMPT_CONTENT" \
--system-prompt-file "$PROMPT_FILE" \
--model "$MOSAIC_MODEL" \
--effort "$MOSAIC_THINKING" \
--restricted \
+14 -4
View File
@@ -8,7 +8,17 @@
set -eu
[ -n "${MOSAIC_SYSTEM_PROMPT_FILE:-}" ] || { echo "pi adapter: MOSAIC_SYSTEM_PROMPT_FILE is required" >&2; exit 2; }
[ -r "$MOSAIC_SYSTEM_PROMPT_FILE" ] || { echo "pi adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; }
# Row 213 (C2-08): pi gets the prompt as a path, so no local user reads it in /proc/<pid>/cmdline.
# Pi 0.85.1 reads --system-prompt as a file only when the path exists and reads, and otherwise
# uses the path string itself as the prompt, without a warning for a missing file
# (dist/core/resource-loader.js resolvePromptInput). So the path must be a regular readable
# file here, and is made absolute before the cd below, since pi resolves it from its own cwd.
PROMPT_FILE=$MOSAIC_SYSTEM_PROMPT_FILE
case "$PROMPT_FILE" in
/*) ;;
*) PROMPT_FILE="$(pwd)/$PROMPT_FILE" ;;
esac
[ -f "$PROMPT_FILE" ] && [ -r "$PROMPT_FILE" ] || { echo "pi adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; }
# MOSAIC_AGENT_NAME is optional in headless mode (identity section is then
# omitted); interactive launches always set it via scripts/agent.sh.
@@ -94,7 +104,8 @@ fi
# All flags documented in the pi package README (CLI Reference):
# -p/--print one-shot mode: print the response and exit (omitted in
# interactive TUI mode)
# --system-prompt replace the default prompt with the generated one
# --system-prompt replace the default prompt with the generated one, given
# as the file's absolute path (checked above), never its text
# --no-* no ambient context/skills/extensions/templates/themes
# EXT_FLAGS the explicitly provided extensions only (-e)
# SESSION_FLAGS ephemeral | persistent | forked (per env)
@@ -103,7 +114,6 @@ fi
# --offline no startup network operations (update checks/telemetry)
# --no-approve ignore project-local .pi/ files (settings, SYSTEM.md)
# whatever trust is saved for the workspace
PROMPT_CONTENT="$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")"
set -- \
--offline \
--no-approve \
@@ -118,7 +128,7 @@ set -- \
--provider "$PI_PROVIDER" \
--model "$PI_MODEL" \
$THINKING_FLAG \
--system-prompt "$PROMPT_CONTENT"
--system-prompt "$PROMPT_FILE"
# One-shot mode appends -p and the request (both safely quoted);
# interactive mode appends nothing - clean TUI.
[ "$INTERACTIVE" = "1" ] || set -- "$@" -p "$MOSAIC_REQUEST"
+161
View File
@@ -0,0 +1,161 @@
# Row 195 build: `scripts/case1-accept.sh`
Row 195 (#1693), the Case 1 install on 10.1.1.53. Design r7 at 5425ea2f3, approved by Duckworth
(duckworth-0087, `agents/duckworth/work/row195/REVIEW-r7.md` at 58d930f89; Gitea 29962). Sage's go to build
is sage-1817.
## What this commit holds
- `scripts/case1-accept.sh` (new, 100755): section 5's acceptance, F1-F11 and P1-P11.
- `packages/cli/tests/case1-accept.test.mjs` (new): section 5's cases.
- `docs/TOOLS.md`: the `case1-accept.sh` entry after the hostcheck entry.
- This file.
- `DESIGN.md`, doc-only fixes Sage named in sage-1817, with no new design round:
- Duckworth's N1. Section 1's human-id row cited `broker.mjs:245`; the check is `broker.mjs:321`.
- Row 226's state at :21 and :326. It said "unclaimed"; 226 is assigned to Gosalyn, briefed and not
started (sage-1814).
- :340 said I'd send the section 6 items to Gosalyn once 226 was claimed. I sent them on 2026-10-21
and Gosalyn acknowledged (gosalyn-0085), so the line now says that.
Nothing else changes. No `case2-*.sh`, `stack-upgrade.sh`, `stack-release.sh`, `case1-hostcheck.sh` or run
sheet edit, and nothing touches .53 (sage-1809).
## Deviations from section 5, and choices it left open
1. P4's mark is the single token `accepted Q182+Q183`. Section 5 lists `accepted Q182` and `accepted
Q183` as marks and gives P4 both. One line can carry one mark, and P4 is the only Q182 path, so the
count list has `accepted Q182+Q183` in place of `accepted Q182`.
2. The test seam replaces `getent`. Under `--root`, the script reads `DIR/etc/passwd` and `DIR/etc/group`
with awk. Without it, the same helper calls `getent`. The groups of a user are its primary gid, then
each group whose member list names it, which is what `id -G` prints. So there is no fake `id`, and
the script never calls `id` for another user.
3. P10 reads the port floor from `/proc/sys/net/ipv4/ip_unprivileged_port_start` rather than calling
`sysctl`, so the seam covers it the way hostcheck's item 7 is covered. There is no fake `sysctl`.
4. There is no fake `bwrap`, `claude`, `node` or `hostcheck.sh` on PATH. The script runs each by the
absolute path the args file or the install names, so the test puts stubs at those paths under
`--root`, and the fake `sudo` runs the stack's user's commands from the tree.
5. The args file must say `--profile factory` and name `--business`. Both are new refusals beyond
`stack-upgrade.sh`'s rules. A library file, or one for another business, would make F4 and F7
check the wrong thing. After `--`, `--port` and `--tester-port` must be numeric. The args file is the
only source of the ports.
6. F8, F9 and F11 read file metadata as root: `stat`, `-L` and `find -maxdepth 1 -name '*.json'`. None
opens the credential. F9 parses `managed-settings.json` with the pinned node, run by root: that node
and that file are both root's own, installed by ops-01.
7. F7 reads `launch.by` from `$HOME/.config/mosaic-dev/businesses/<id>.json` as the stack's user. That
path is `businessFilePath(id)` under the default config, which is what the unit uses. It doesn't load
the copy's `business.mjs` as `stack-release.sh`'s `plugins launchby` does. The user runs the whole
check and prints one line, and root accepts only `ok <instance> run <run>` (sage-1653: root doesn't
parse the user's output beyond a fixed pattern).
8. F5 names a process claude when its argv0's basename or its `comm` starts with `claude`. The window
confirms this against a real role session. If the bwrap'd claude shows another name, F5 fails closed
and the fix is this match, not the label check.
9. F3 and F5 run together. `talk` goes out in the background under `timeout 330`, and the `/proc` scan
repeats every half second until it finds a labelled pair or `talk` exits. F3's line prints first, then
F4, then F5, so the output keeps id order. F3 passes on exit 0 with the `sent` line and a reply line,
both counted on stdout only (`cli.mjs:395` and the `formatMessage` write); talk's error goes to stderr.
12. `--claude` must match `^/[A-Za-z0-9/._+@-]+$`. The `@` is for `node_modules/@anthropic-ai/...`;
`stack-upgrade.sh` doesn't check `--claude` at all.
10. F1 requires all four uids in `/proc/<MainPID>/status` (real, effective, saved, filesystem) to be the
user's.
11. Evidence text the stack's user controls (talk's last line, the precheck's last line, F6's and F10's
`--version`) is cut to printable ASCII, one line, 200 bytes.
## Tests
`packages/cli/tests/case1-accept.test.mjs`, 22 tests. Every case builds a scratch host with every F check
passing and every P check closed or accepted, then changes one thing:
- all pass: exit 0, ids in order, each P mark, the count lines match the check lines, and the stand-in
credential's value appears nowhere;
- each F check failing alone exits 1 with only its line failing;
- F3 on exit 0 with only the `sent` line, and its evidence taking talk's stderr;
- F1 on a failed unit and on `User=root`; F2 on healthz failing and on another pid holding the port;
- F5 on `unpriv_bwrap (enforce)`, `bwrap//&unpriv_bwrap (complain)`, a trailing space on either label,
no bwrap of the user's, and passing on `comm` alone;
- F6 on a stub that prints nothing and on a path outside `claude-<pin>`;
- F7 on two live PM launches, a live one for another business, and no `launch.by`;
- F8 on 0640, on another owner and on a symlink, never printing the credential;
- F9 on a group-writable mode, another owner and a number for the flag;
- F11 on one `*.json` under `discord/`, and passing on an empty directory;
- each P check's unnamed mark, with the line count each fault makes: P6 one line per member, P9 one per
missing item; P2, P5, P10 and P11 in more than one way;
- every P check unnamed at once still exits 0;
- nine bad args files refuse with exit 2 before any command runs, each with its own refusal text;
- usage errors, and a user's run without `--root`, exit 2;
- every in-instance call goes through `sudo -u mosaic -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin`,
read from the fake `sudo`'s argv, and root's other `sudo` calls are exactly `-l -U mosaic`,
`-l -U localadmin` and the jarvis bwrap probe;
- the host check records `--check` and never `--enforce`.
## Mutants
Section 5's twelve, each one exact replacement in `scripts/case1-accept.sh`, run by
`/mnt/storage/scratch/darkwing-195-out/mutants.py` in Sage's slot (sage-1865). All twelve were killed.
After each one the script's bytes were restored, and the final sha256 matched (`d434f15ff638...`). The
killers below are the tests that failed in each mutant's saved log.
| # | Mutant | Killed by |
|---|---|---|
| 1 | F1 passes on a MainPID owned by root | each F alone |
| 2 | F2 accepts a listener on 0.0.0.0 | each F alone |
| 3 | F3 passes without a reply | each F alone; F3 exit 0 with no reply line |
| 4 | F5 matches a substring | F5 labels (the trailing space) |
| 5 | F6 skips the `--version` check | each F alone; F6 stub and path |
| 6 | F8 skips the owner check | F8 0640, owner and symlink |
| 7 | F11 ignores `discord/` | each F alone; F11 `*.json` |
| 8 | P1 closes on any output | P unnamed; every P unnamed |
| 9 | P5 maps to `closed` with jarvis in docker | P unnamed; every P unnamed; P2 and P5 names docker |
| 10 | P9 drops the jarvis item | P unnamed; P9 per item |
| 11 | P11 accepts any `Before=` | P unnamed; every P unnamed |
| 12 | a P check's `unnamed` sets the exit to 1 | P unnamed; every P unnamed; P10 and P11; P2 and P5; P9 per item |
Mutant 1 survived the F1 variants test (a failed unit, `User=root`), because those faults fail F1 on the
unit's state and `User=`, not on the uid line. Each F alone covers the uid line.
## Pre-slot read review
Since every test run is slot work, a read-only subagent traced each case through the script before the
first slot run, executing nothing. It found four defects, all fixed in this commit:
- The `--claude` regex had no `@`, so every run refused the args file, the real .53 one included.
- The fake `stat` logged the args-file call, so the bad-args test's "no command ran" check would fail.
- The P5 and P6 faults each rewrote `etc/group` from the constant, so applying both lost P5's change.
- F3 counted stderr into its line count, so a mutant dropping the exit check survived. F3 now counts
stdout only, and a `sent`-only case covers the line count.
## Run environment
The build commit ddf7dce6c sits on base 4ced5e63d (origin/refactor when it was pushed). It was first built
on c515273db as 4a51d392c. Row 238 then changed `docs/TOOLS.md`, so I cherry-picked the three commits onto
4ced5e63d as `darkwing/row195-b`. `git range-diff` shows all three identical. node v26.8.1 on dragon-lin,
every run behind my busy check and Sage's, chained with `&&`.
- Step 1, sage-1829 (07:41:54Z to 07:42:05Z), TMPDIR `/tmp/dw195`:
`node --test packages/cli/tests/case1-accept.test.mjs`, 22 pass, 0 fail, 11.0 s.
- Step 2, sage-1865 (08:34:27Z to 08:36:21Z), TMPDIR `/mnt/storage/scratch/tmp/r195`: the twelve mutants
above, all killed.
- Step 3, the gate (08:36:21Z to 08:47:06Z), same TMPDIR, `COMPOSE_PROJECT_NAME=darkwing-r195`:
| Suite | Result |
|---|---|
| packages/cli (`node --test tests/*.test.mjs`) | 662 pass, 0 fail, 0 skipped |
| test-auth.sh | 15 passed, 0 failed |
| test-conductor.sh | 17 passed, 0 failed |
| test-config.sh | 31 passed, 0 failed |
| test-discord.sh | 78 passed, 0 failed |
| test-extension-package.sh | 18 passed, 0 failed |
| test-foundation.sh | 44 passed, 0 failed |
| test-land-work.sh | 86 passed, 0 failed |
| test-queue.sh | 156 node pass, 0 fail; 28 passed, 0 failed |
| test-release.sh | 14 passed, 0 failed |
| test-task.sh | 98 passed, 0 failed |
Summed: 818 node tests passed with 0 failed, and 429 selftests passed with 0 failed. The compose network
was removed afterwards, and the sweep found no process left in the worktree, the output directory or the
TMPDIR. This file's results are a doc-only commit on top of the build, so nothing that ran has changed.
## Correction to deviation 6 (follow-up, 2026-10-21)
Deviation 6 says the node F9 runs is root's own. At 37171f65b the script didn't check that. F9 ran
`--node` as root before F10's path check, so the header's "root itself only reads" was false
(Duckworth's N1, `agents/duckworth/work/row195/REVIEW-m1.md` at 6afcee2a5). A host installed as designed
was safe, because `case2-unit.sh` refused a non-root-owned `--node`. Row 245 carries the fix and its
tests: `agents/darkwing/work/queue-245/BUILD.md`.
+388
View File
@@ -0,0 +1,388 @@
# Row 195 design: the Case 1 install on 10.1.1.53 (r7)
Darkwing, 2026-10-21. Brief: `docs/plans/2026-10-20_hub-own-uid.md`, blob 45be7f09afc1, section "Brief: the
Case 1 install on 10.1.1.53 (row 195)". Gitea #1693. Reviewer Duckworth, gate owner Sage. Branch
`darkwing/row195-r7` on c515273db. r6 (b7239bb66 on `darkwing/row195`, approved duckworth-0055) is the base,
carried unchanged as 7398b7dd7. The r6 text is in that commit and in b7239bb66.
## Changes from r6
Q187 (mos-1108) made install and upgrade product commands, and mos-1118 split r6 into the installer rows
(`docs/plans/2026-10-20_installer-rows.md`). That brief says row 195 "shrinks to the factory profile's
values and the Case 1 acceptance". So r7 is mostly a cut. What r6 designed now ships elsewhere:
| r6 piece | Now | State |
|---|---|---|
| `stack-release.sh --units external`: `confirm`, `revert`, `gate`, `rollback --to`, external `adopt` and `status`; `stack-copy.sh check --units external` and bundle copies | row 232, I1 | landed |
| `case1-repin.sh` (A1-A5, A2f, R1-R4) | `scripts/stack-upgrade.sh`, row 233, I3 | landed |
| the host steps 2a, 2a', 2a'' and the window's root steps | `docs/guides/host-run-sheet.md` sections 1-10, row 233 | landed |
| `case1-hostcheck.sh`, `case1-pins.sh`, `mosaic-hostcheck.service` (the drift stop, r6 B2) | row 240, I4 | landed c515273db |
| the unit render, `--profile factory`, `--pm`, `--claude`, the factory serve flags, the precheck | row 230, C2-13a, I2 | landed |
| the install as `mosaic`: bootstrap, business file, copy | row 226, C2-13b, I2, `scripts/case2-install.sh` | assigned to Gosalyn, briefed, not started |
| `docs/guides/case1-install.md` | the host run sheet, plus section 6 of this file | |
Other changes:
- Claude moves under `/opt` (sage-1522, run sheet section 3). `--claude` is
`/opt/mosaic-stack/claude-<pin>/node_modules/@anthropic-ai/claude-code/bin/claude.exe`, root's, and root
installs and upgrades it. r6's `/srv/mosaic/.local/bin/claude` is gone. F6 reads the new path.
- The args file is `/etc/mosaic-stack/dev.args` (run sheet section 7), not `case1-unit.args`. It holds
`--copy` too, and `stack-upgrade.sh` rewrites only that line, after confirm passes.
- Both units are `WantedBy=multi-user.target` and start at boot (Q189, mos-1122). r6's "the Console has no
`[Install]`, root starts it by hand after a reboot" no longer holds.
- The business id is stated, with the DEFERRED 240 N1 reasoning (sage-1809, sage-1813). Section 7.
- The window can't run until row 226 lands. Sage added 226 to row 195's after list at rev 2718
(sage-1813). Section 6 lists what row 195 needs from 226's install.
- The acceptance runs as root through ops-01, not as `localadmin`. `aa-status`, `sudo -l -U`,
`systemd-run --uid` and the hostcheck all need root. It gains four functional checks for items the run
sheet marks "precheck line missing" (F8-F11) and one host line (P11). Section 5.
- The Discord row in section 2 changed. The precheck's "no Discord binding" line is C2-13b's and is
library-only, so for the factory it's F11 in the acceptance.
What row 195 still builds: `scripts/case1-accept.sh` and its test (section 5). The rest of this file is
inputs, the window order and the record the reviewer checks.
## 0. What I checked on the host
Read only, as `localadmin` over ssh, 2026-10-20 between 20:40Z and 21:51Z. Nothing in r7 touched .53
(sage-1809). The window's first step re-reads these, since they can drift in a day: unattended-upgrades
runs daily.
| Item | 10.1.1.53 on 2026-10-20 | Needed |
|---|---|---|
| OS | Ubuntu 24.04.5, kernel 6.8.0-124, x86_64, 12 cores, 94 GiB, 217 GiB free on `/` | fits |
| `mosaic` user, group, `/srv/mosaic` | absent | run sheet section 1 |
| node | `/usr/local/bin/node` v22.22.2, root-owned | 24 or later, under `/opt` (run sheet section 2) |
| claude | not on localadmin's PATH | under `/opt` (run sheet section 3) |
| bwrap | absent; apt candidate `bubblewrap 0.9.0-1ubuntu0.3` | run sheet section 4 |
| user namespaces | `kernel.apparmor_restrict_unprivileged_userns = 1`, apparmor 4.0.1, no bwrap profile loaded | run sheet section 4 |
| profiles that name bwrap | none (`grep -rlw bwrap /etc/apparmor.d` exits 1) | hostcheck item 3 |
| `fs.protected_hardlinks` | 1 | hostcheck item 6 |
| `net.ipv4.ip_unprivileged_port_start` | 1024 | run sheet section 5 |
| listeners below 1024 | 22 (sshd) and 53 (the resolver); 732 is free | the Console's port |
| ports 7330-7333 | free | 7333 is the tester port |
| `localadmin` | NOPASSWD sudo (#423 step 1, mos-1066, mos-1077) | ops-01's root path |
| `jarvis` | groups sudo, docker, lxd, adm | #423 step 4 removes them (P5) |
| unattended-upgrades | on, with the security pocket, which carries bubblewrap | the drift stop (row 240) |
| journald | `SplitMode=uid` | the PM check reads `launches list`, not the journal (row 232) |
## 1. The factory profile's values
Instance `dev`, row 190's default (mos-1118 choice 4). `~` is `mosaic`'s home.
| Input | Value | Who gives it |
|---|---|---|
| user | `mosaic`, primary group `mosaic` with no other member | fixed |
| home | `/srv/mosaic`, 0700 | fixed |
| config dir | `/srv/mosaic/.config/mosaic-dev` | row 190 |
| data root | `/srv/mosaic/.mosaic-dev`, marker `{"instanceVersion":1,"name":"dev"}` | row 190 |
| business | `mosaic-stack`, file `<configDir>/businesses/mosaic-stack.json` | fixed (section 7) |
| profile | `factory` for `case2-install.sh`, `case2-unit.sh` and `case2-precheck.sh` | fixed |
| roles | the factory set from `roles/`, with a chief that has a harness var | the go names the set (section 2) |
| human id | Jason's, not `tester` (`broker.mjs:321`) | the go |
| model credential | `<configDir>/secrets/claude-session.env`, `mosaic` 0600, one line `CLAUDE_CODE_OAUTH_TOKEN=...` | Jason places it (section 3) |
| token slot path | none | section 2 |
| node | `/opt/mosaic-stack/node-<v>/bin/node`, `<v>` 24 or later | the go names `<v>` and its sha256 |
| claude | `/opt/mosaic-stack/claude-<pin>/node_modules/@anthropic-ai/claude-code/bin/claude.exe`, `<pin>` 2.1.304 unless the go names a newer one | the go |
| PM | on (`--pm`) | fixed |
| Console | `--port 732` on 127.0.0.1, with the unit's `CAP_NET_BIND_SERVICE` | fixed |
| tester | `--tester-port 7333` on 127.0.0.1 (P10) | fixed |
| board | none | fixed |
| release | a git bundle of `refs/heads/refactor` and the 40-hex commit (run sheet section 6) | the go names both and the bundle's sha256 |
The args file. `/etc/mosaic-stack/dev.args`, `root:root` 0644, one argument per line (run sheet section 7):
```
--profile
factory
--user
mosaic
--home
/srv/mosaic
--business
mosaic-stack
--copy
<40-hex commit>
--node
/opt/mosaic-stack/node-<v>/bin/node
--claude
/opt/mosaic-stack/claude-<pin>/node_modules/@anthropic-ai/claude-code/bin/claude.exe
--pm
--
--port
732
--business
mosaic-stack
--tester-port
7333
```
`case2-unit.sh` takes exactly these before `--` (`scripts/case2-unit.sh:54`), and `stack-upgrade.sh`
refuses `args-unreadable` on anything else (`scripts/stack-upgrade.sh:126-148`). The factory profile
refuses the preview trio, `--send` and the proof pair (`case2-unit.sh:169`). .53 has no proxy, and Jason
reaches the Console with `ssh -L` (sage-1451 Q3). Changing a serve flag is a re-install with its own go,
not an upgrade.
Why the port is 732 (r6, mos-1101 item 5). Role sessions share the host's network namespace
(`packages/seat/src/sandbox.mjs:67`), so while the Console is down a session could bind a port above the
floor and answer in its place. Below 1024 only the Console unit can bind, through its capability. The
tester port can't go below 1024 (`packages/webui/src/cli.mjs:33`), so P10 records it.
PATH for `mosaic`'s steps. `sudo -u mosaic -H` resets PATH to sudo's `secure_path`, which finds
`/usr/local/bin/node`, v22. So every `mosaic` step runs as
`sudo -u mosaic -H env PATH=/opt/mosaic-stack/node-<v>/bin:/usr/local/bin:/usr/bin:/bin ...`, the list
`case2-unit.sh` renders as the units' PATH. `stack-upgrade.sh` builds it from the args file's `--node`.
## 2. Where Case 1 differs from Case 2
| Case 2 (library) | Case 1 (factory) | Reason |
|---|---|---|
| business file with the library mapping | `mosaic-stack` with no `library` key. The precheck refuses a library project under `factory` (`profile-library-business`, `case2-precheck.sh:127-128`) | the factory has no books repo |
| `bean`, `chet`, persona vars and files, specialists | the factory roles from `roles/`, with a chief that has a harness var | the go names the set. I haven't read Jason's live business file and won't. The acceptance needs only the chief (F3); a chief with no harness var answers `no-route`. The human adds roles later with `instances.set`, no new row |
| token slot `library-gitea` | none | the factory's per-agent git identity is row 199's credential store (Megavolt, #1697). Until it lands, git work stays on dragon-lin (Q182) |
| `bus.tester` off | on; the precheck refuses it off under `factory` (`case2-precheck.sh:135`) | the acceptance and the Q173 test system need the tester |
| persona modes and digests, `library.sync`, `library.status` | skipped | no persona files, no library (C2-13b brief, `factory` profile) |
| `ZAI_API_KEY` in `claude-session.env` | not checked; the file holds `CLAUDE_CODE_OAUTH_TOKEN` | no instance has provider `zai` (run sheet section 9) |
| no Discord binding (C2-13b, library only) | none either. F11 checks that `<dataRoot>/discord/` holds no binding | the factory's connector stays on dragon-lin. Two connectors on one bot token would split its events |
| the claude marker prompt | runs, since the chief's harness is claude-code | |
| serve flags: preview trio, `--send`, proof pair | `--port 732`, `--business mosaic-stack`, `--tester-port 7333` | no proxy on .53 |
| bus: no `--pm`, no `MOSAIC_CLAUDE_BIN` | `--pm`, and `Environment=MOSAIC_CLAUDE_BIN=<--claude>` | the PM dispatches the factory's work; without the path the bus finds no claude on the unit's PATH |
| Console proof file (run sheet section 9, .54) | none | no proof pair |
All of these are inputs to the landed scripts. Row 230 implemented the four factory items r6 asked for
(serve flags, `--pm`, `--claude`, row 190's tokens), and nothing in them hardcodes a Case 1 value.
## 3. Model access
Q185: Case 1 uses the account the factory uses today, the Claude subscription behind dragon-lin's
`claude-session.env`. No script copies that file and none reads its value. In the window, Jason, or the
person he names under the Q173 secrets rule, makes a credential for .53 from that account and writes it
to `/srv/mosaic/.config/mosaic-dev/secrets/claude-session.env` as `mosaic`, mode 0600 (run sheet
section 9). Nobody prints it. The check is `stat -c '%U %a %F' PATH` only (F8).
The bus unit reads it through `EnvironmentFile=-` (`case2-unit.sh:243`). The leading `-` means a missing
file doesn't stop the bus, so the chief's first launch would fail instead. The window places the file
before the units start (section 6, W9), and the precheck's claude marker prompt then proves it works
without printing it.
Quota: the factory on dragon-lin and on .53 share one subscription. Q185's "use existing" accepts that.
The pi harness stays off on .53 until P19 #383 settles (mos-0865). The business file names claude only.
## 4. Upgrade, rollback and backup on .53
All three are `scripts/stack-upgrade.sh` (row 233), run as root by ops-01 from `/root/mosaic-stack` at the
reviewed commit. Usage, exits and the step log are in `docs/TOOLS.md`, "Root's upgrade driver", and run
sheet section 6.
| Action | Command | Who | Go |
|---|---|---|---|
| upgrade | `stack-upgrade.sh upgrade --instance dev --bundle /srv/mosaic/incoming/<sha>.bundle --sha <sha>` after run sheet section 6's per-upgrade lines | ops-01 | Mos per run; `--migrate` also needs Jason present (decision 96) |
| rollback | `stack-upgrade.sh rollback --instance dev --to <copy> [--restore <ACT> [--events N]]` | ops-01 | Mos per run. Mos or Sage picks `<copy>` from `stack-release.sh status mosaic-stack --units external`, read as `mosaic`. The plan refuses unless it is the previous distinct copy |
| backup of the store | A3's `backup_store` into `<dataRoot>/backups/stack/<ACT>`, inside every upgrade | the driver | the upgrade's |
| rehearsal | `stack-release.sh rehearse` against the log's newest backup, before anything stops, on `--rehearse-port`, default 7339, which section 0 found free | the driver | the upgrade's |
| backup of the VM | a PBS backup of VM 1008 before the install window and before any `--migrate` | ops-01 | the window's go (Q182, Q183: agents keep PBS) |
Three .53 specifics:
1. The first upgrade after install needs `--no-rehearsal` in its go. The install's adopt takes no backup,
so the log records none, the rehearsal exits 3 (`no-backup`), and the driver goes on only with the
flag (`stack-upgrade.sh:323-324`). Every later upgrade refuses the flag (`rehearsal-required`).
2. Exit 5 (`args-failed`): the new stack is live and confirmed, but `dev.args` still names the old copy.
ops-01 moves the file the result line names into place with `mv -f`, and nothing else (run sheet
section 6).
3. Exit 2 leaves both units stopped and the activation open. ops-01 sends Mos the exit and the last
lines of `/var/log/mosaic-stack/upgrade-dev.log`, and Jason or Mos settles it with `confirm` or `revert`
as on dragon-lin. A unit that doesn't stop within `--timeout` ends here on purpose (A2f's
`stale-start`, duckworth-0055 N1, now in run sheet section 6).
The store backup doesn't hold the config dir or the credential. PBS backups of VM 1008 hold the whole disk,
the credential included (P4). A restore swaps only the store and leaves `claude-session.env` in place.
Re-install, for a serve flag or role change, is not an upgrade. It needs its own go and runs: stop the
Console, then the bus; rewrite `dev.args` with the same `--copy`; `case2-unit.sh` with it; both units start
again from the render. Row 230 N1 (webby-0046) holds here: a re-render with the units running leaves each on
its old copy, so ops-01 stops both first. `stack-upgrade.sh` already stops both before it renders.
## 5. Acceptance: `scripts/case1-accept.sh`
New in this row. ops-01 runs it as root on .53, from `/root/mosaic-stack`:
```
case1-accept.sh --business mosaic-stack --args /etc/mosaic-stack/dev.args --claude-pin <pin> --node-pin <v>
```
It reads the user, home, node, claude and ports from the args file and refuses (exit 2) when that file
fails `stack-upgrade.sh`'s rules. Every check inside the instance runs as
`sudo -u mosaic -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin`. It changes nothing on the host
except what F3's message writes to the store. It prints one line per check, `<id> <path> <mark>
<evidence>`, then a count of each mark. Marks: `closed`, `accepted Q182`, `accepted Q183`, `accepted
mos-1096`, `accepted mos-1101`, and `unnamed`, which means "not named in Q182, Q183 or a Mos ruling".
It exits 1 when any F check fails and 0 otherwise. A P check never changes the exit. Each `unnamed`
line goes to Mos, who takes it to Jason (R8 N1). Duckworth reads the whole output.
It prints no value from `claude-session.env` and never opens it.
Functional checks. Any failure fails the row:
| Id | Check | How |
|---|---|---|
| F1 | the units run as `mosaic` | for `[email protected]` and `mosaic-webui.service`: active, `User=mosaic`, and the uid in `/proc/<MainPID>/status` is `mosaic`'s |
| F2 | the Console answers on 127.0.0.1:732 only | `curl -fsS http://127.0.0.1:732/healthz` succeeds, and `ss -Hltnp` shows 732 bound to 127.0.0.1 only, by the Console's MainPID. Jason reaches it with `ssh -L 7332:127.0.0.1:732 [email protected]`. A LAN listener waits for Q175 SSO (sage-1451 Q3) |
| F3 | one tester message gets a reply | as `mosaic`: `mosaic talk chief <marker> --business mosaic-stack --tester --wait 300`. A stored reply to the marked message passes. The inbox shows decisions only, so it isn't used |
| F4 | the precheck passes | `case2-precheck.sh --business mosaic-stack --profile factory` as `mosaic` exits 0 |
| F5 | a role session runs under bwrap with the profile | while F3's reply is pending: a `/usr/bin/bwrap` process owned by `mosaic` has a claude child. Its `/proc/<pid>/attr/current` equals `bwrap (enforce)` and the child's equals `bwrap//&unpriv_bwrap (enforce)`, exact strings after the trailing newline is cut |
| F6 | claude is the pin | the args file's `--claude` path sits under `/opt/mosaic-stack/claude-<pin>/`, and its `--version` as `mosaic` prints `<pin> (Claude Code)` |
| F7 | the PM run is live | `launches list --json` as `mosaic` has exactly one live entry for the PM's instance (`launch.by`), the check `confirm` runs |
| F8 | the credential's file | `stat -c '%U %a %F'` on `claude-session.env` reads `mosaic 600 regular file`, and it isn't a symlink. Run sheet section 9 marks this "precheck line missing" |
| F9 | the autoupdater is off | `/etc/claude-code/managed-settings.json` is `root:root` 0644 and its `env.DISABLE_AUTOUPDATER` is `"1"`. Run sheet section 3 marks this "precheck line missing" |
| F10 | node is the pin | the args file's `--node` is `/opt/mosaic-stack/node-<v>/bin/node` with `<v>` the `--node-pin`, and its `--version` prints `<v>`. Run sheet section 2 marks this "precheck line missing" |
| F11 | no Discord binding | `/srv/mosaic/.mosaic-dev/discord/` is absent or holds no `*.json` (section 2) |
Host paths. These report and never fail the row:
| Id | Path | Mark | Evidence |
|---|---|---|---|
| P1 | `mosaic` has sudo | closed only when `sudo -l -U mosaic` prints "is not allowed to run sudo"; any other output is unnamed | the `sudo -l` result |
| P2 | `mosaic` has supplementary groups or a key | closed when `id -G mosaic` is one entry and `~mosaic/.ssh/authorized_keys` is absent or empty | `id`, file state |
| P3 | `localadmin` NOPASSWD sudo, reached with the dragon-lin key | accepted Q183 | `sudo -l -U localadmin` |
| P4 | root on pve over VM 1008, and PBS backups that hold `/srv/mosaic` with the credential | accepted Q182 and Q183 | a fixed line; the script can't see pve |
| P5 | `jarvis`, which runs Hermes, reaches `/srv/mosaic` through sudo, docker or lxd | closed when `jarvis` is in none of the three; unnamed otherwise. #423 step 4 removes all four groups before the install (mos-1091 4c) | `id jarvis` |
| P6 | other members of sudo, docker, lxd | closed when sudo holds only `localadmin` and docker and lxd are empty; each other member is its own unnamed line | `getent group` lines |
| P7 | a file under `/etc/sudoers.d` naming `mosaic` | closed when none | file names |
| P8 | the Console or tester reachable from the LAN | closed when 732 and 7333 are bound to 127.0.0.1 only, each by the Console's MainPID, and nothing listens on 7331 | listener table |
| P9 | a user namespace through `/usr/bin/bwrap` | accepted mos-1096 only while all eight hold, unnamed otherwise: hostcheck items 1 to 6, read from `/usr/local/lib/mosaic-stack/hostcheck.sh --check`'s own `ok N` lines (item 7 is P10's floor); `sudo -u jarvis /usr/bin/bwrap --version` fails with "Permission denied"; `getent group mosaic` lists no member and no other account has its gid. F5 adds the label of a real role session | the hostcheck lines and each item |
| P10 | a session binds the tester port 7333 while the Console is down | accepted mos-1101 while the port floor is above 732 and 7333 is held by the Console's MainPID | the floor, the owner pid |
| P11 | the boot check runs before the bus | closed when `mosaic-hostcheck.service` is enabled and `systemctl show -p Before` on it names `mosaic-bus@<business>.service` for the args file's business; unnamed otherwise (section 7) | the `Before=` value |
P9 reads hostcheck's output, which root's own script prints, by matching `^ok [1-6] ` lines. It runs `--check` only,
never `--enforce`, so the acceptance can't close bwrap.
Tests, in `packages/cli/tests/case1-accept.test.mjs`. The script takes a test seam, `--root DIR`, for
`/proc`, `/etc`, `/srv/mosaic` and `getent`, and refuses it when run as root, as the hostcheck does. Fake
`systemctl`, `curl`, `ss`, `sudo`, `bwrap`, `sysctl`, `stat`, `claude`, `node` and `hostcheck.sh` on PATH.
Nothing reaches .53 or dragon-lin's live state, and no test reads a real credential. Cases:
- every check passing exits 0, with the counts matching the lines;
- each F check failing alone exits 1 with its line;
- F5 fails on `unpriv_bwrap (enforce)`, `bwrap//&unpriv_bwrap (complain)` and a trailing space;
- F6 fails on a stub `claude.exe` that prints nothing and on a path outside `claude-<pin>`;
- F8 fails on mode 0640, on another owner and on a symlink, and the stand-in credential's value never
appears in the output;
- F11 fails on one `*.json` under `discord/`;
- each P check in each of its marks: P1 with output that grants nothing but lacks the refusal text
(unnamed), P9 unnamed once for each item missing, P10 unnamed with the floor at 700 and with 7333 held
by another pid, P11 unnamed with `Before=` naming another business;
- every P check `unnamed` still exits 0;
- a bad args file refuses with exit 2 before any check;
- every in-instance call goes through `sudo -u mosaic -H env PATH=<node dir>:...`, read from the fake
`sudo`'s recorded argv;
- the hostcheck fake records `--check` and never `--enforce`.
Mutants, each killed by a named case:
1. F1 passes on a MainPID owned by root.
2. F2 accepts a listener on 0.0.0.0.
3. F3 passes without a reply.
4. F5 matches a substring.
5. F6 skips the `--version` check.
6. F8 skips the owner check.
7. F11 ignores `discord/`.
8. P1 closes on any output.
9. P5 maps to `closed` with `jarvis` in docker.
10. P9 drops the jarvis item.
11. P11 accepts any `Before=`.
12. A P check's `unnamed` sets the exit to 1.
## 6. The .53 window
Nothing in this row touches .53 (sage-1809). Every root step below is ops-01's, over
`ssh [email protected]` with `sudo`, and waits on a Jason decision (mos-1086). The window needs:
- row 226 landed;
- Jason's or Mos's go naming the commit, the bundle's sha256, the node and claude pins, the role set and
Jason's human id;
- a backup first;
- a STATE report after.
| # | Step | Runs as | Source |
|---|---|---|---|
| W0 | re-read section 0's items; stop on any change and report it | `localadmin`, read only | section 0 |
| W1 | PBS backup of VM 1008 | ops-01 on pve | section 4 |
| W2 | the stack's user and home | root | run sheet 1 |
| W3 | node under `/opt` | root | run sheet 2 |
| W4 | claude under `/opt`, managed settings | root | run sheet 3 |
| W5 | bubblewrap, the profile, the statoverride, the no-new-privs test, the drift stop; `hostcheck.sh --check` exits 0 with item 7 at 1023 | root | run sheet 4 |
| W6 | the port floor (read) | root | run sheet 5 |
| W7 | the bundle, root's checkout at `<sha>`, `mosaic`'s clone | root, then `sudo -u mosaic -H` | run sheet 6 |
| W8 | `case2-install.sh --profile factory` with section 1's inputs: bootstrap, the business file, roles, human id, the copy | `sudo -u mosaic -H env PATH=...` | row 226 |
| W9 | place `claude-session.env` | Jason, or who he names | run sheet 9, section 3 |
| W10 | write `dev.args` (section 1); `case2-unit.sh` from it, which enables both units and starts the bus, then the Console | root | run sheet 7, 10 |
| W11 | `case2-precheck.sh --business mosaic-stack --profile factory` exits 0; `hostcheck.sh --check` exits 0 with item 7 at 732 | `mosaic`; root | row 230, 240 |
| W12 | `stack-release.sh adopt mosaic-stack --units external`, unless row 226's install does it | `sudo -u mosaic -H` | row 232 |
| W13 | `case1-accept.sh` (section 5); the output to Duckworth, then Mos confirms | root | this row |
| W14 | STATE report to Mos | ops-01 | brief |
Install rollback, any step before W13 passes. Stop and disable both units, remove their files and
`dev.args`, then the run sheet's rollbacks for sections 4, 3 and 2 in that order. Removing the user and
home (`userdel -r mosaic`) needs its own Mos go, since it deletes the home. W1's backup covers the rest.
What row 195 needs from row 226. Row 226 is assigned to Gosalyn, briefed and not started, so I'm listing what the window
needs from its design, not designing it:
1. `--profile factory` writes the business file with `bus.tester` on and no `library` key, and the roles
the go names.
2. It writes no token slot and no persona files (its brief says so).
3. It runs before the unit render, since the copy must exist for `case2-unit.sh --copy`, and it starts no
unit.
4. Who writes `dev.args`. The C2-13b brief says the install doesn't write unit files, and the run sheet
says "I2's install writes it". `mosaic` can't write `/etc/mosaic-stack`. So either root's part of I2
writes it, or ops-01 writes it from section 1 at W10. Either works for row 195. The file's content is
fixed above.
5. Whether it runs `adopt`. If it doesn't, W12 does.
I sent these to Gosalyn on 2026-10-21 (gosalyn-0085 acknowledged), and the run sheet takes whichever answer 226 gives.
## 7. The business id
.53's business id is `mosaic-stack` (sage-1813). It appears in:
- the args file, twice;
- the bus unit `[email protected]`;
- the business file name;
- `case1-accept.sh --business`.
The landed boot unit `packages/cli/systemd/mosaic-hostcheck.service` has
`[email protected]`. So at boot the drift stop runs before .53's bus, and DEFERRED 240
N1 (da3051ccc: `After=mosaic-hostcheck.service` in `mosaic-bus.service.in`) doesn't bind this row.
If a go ever names another id for .53, the window stops before W8, and the N1 fix lands first as its own
reviewed row. P11 is the check on the host. It reads `unnamed` when the boot unit's `Before=` doesn't
name the installed bus.
DEFERRED 240 N2 (the apt hook goes in before the first `--check`) is a run sheet order note. This row
doesn't edit the run sheet, so it stays deferred.
## 8. Rulings this design rests on
- sage-1451: Q1 system units; Q3 the Console on 127.0.0.1 with `ssh -L`; Q4 a rebase goes to a new branch
name (#426(a), mos-1086).
- mos-1091, mos-1096, mos-1098, mos-1101: the bwrap path, its profile and P9; the port squat and P10; .53
runs the PM. Their r6 sections stand, and rows 232, 233, 240 and 230 implemented them.
- mos-1108 (Q187) and mos-1118: the installer rows and the scope cut. mos-1122 (Q189): both units start at
boot.
- sage-1522: claude under `/opt`, root-owned.
- sage-1809: no step on .53, root steps named and left to ops-01, the business id stated.
- sage-1813: 226 in the after list (rev 2718); the id is `mosaic-stack` and 240 N1 doesn't apply.
- Q185: the factory's model account. Q173: credentials placed by Jason or his named person.
## 9. Manifest (planned)
- `scripts/case1-accept.sh` (new), mode 100755.
- `packages/cli/tests/case1-accept.test.mjs` (new). Section 5's cases and mutants.
- `docs/TOOLS.md`: a `case1-accept.sh` entry with usage, marks and exits, beside the hostcheck entry.
- `agents/darkwing/work/queue-195/BUILD.md` (new): run environment, the mutant results, deviations.
- No change to `case2-*.sh`, `stack-upgrade.sh`, `stack-release.sh`, `case1-hostcheck.sh` or the run
sheet. Two follow-ups stay out of this row:
- row 230 r2 N1. The "still on copy" line reads the id from the unit file. On .53 neither the install
nor `stack-upgrade.sh` reaches that line, since both start from stopped units.
- DEFERRED 240 N2.
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, dragon-lin's units or `~/.mosaic`.
Gate: Duckworth approves this design, then the manifest; the cli suite and every `scripts/test-*.sh`
green on Sage's gate; section 5's twelve mutants killed in a slot Sage grants.
+287
View File
@@ -0,0 +1,287 @@
# Row 232 (I1): stack-release external units and bundle copies
Brief: `docs/plans/2026-10-20_installer-rows.md` section I1, blob `f35e7e57daee` (commit ad6f0be7a). Gitea
#1734, Vikunja P19 #432. Owner Darkwing, reviewer Filbert, gate owner Sage. Design input: row 195 DESIGN r6,
`agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66 (approved in duckworth-0055). Where the brief and
r6 differ, the brief wins. sage-1561 ruled where the stack user's `stack-release.sh` runs: from its own
clone of the release bundle, so `check_target`, the plan's commit list and status drift are unchanged and
nothing adds `safe.directory`.
## Files
Base: cb5fdb064 (row 199's landing, on top of row 200's 9e1085816). Branch `darkwing/row232-r1`. The first
build, 0af0c73c6 on `darkwing/row232`, sat on 00d4cc1f6 (row 190); row 200 landed first, so this one is
rebased onto it (see D9 and D10).
- `scripts/stack-release.sh`: `--units user|external`, the `sc` and `jctl` backend, `confirm`, `revert`,
`gate --since`, `rollback --to`, `--plan --expect-from`, the external gate (start floor, PM check from
`launches list`), `external-pm`, and the `awaiting units:` status line.
- `scripts/stack-copy.sh`: `check --units external`, `copy --bundle FILE SHA`, `wrapper ID --create`.
- `packages/cli/tests/stack-release-external.test.mjs` (new, 25 tests).
- `packages/cli/tests/stack-release-fixture.mjs` (new): the helpers `stack-release.test.mjs` defined, moved
so both files share them. See deviation D1.
- `packages/cli/tests/stack-release.test.mjs`: the helpers leave and one import line comes in. The 74 tests
have the same names and the same bodies as on the base.
- `packages/cli/tests/stack-copy.test.mjs`: three new `row 232:` tests and a wider `node:fs` import.
- `docs/TOOLS.md`: the stack-copy usage block and three paragraphs, the stack-release usage block, and a new
subsection "Root-owned units (`--units external`, row 232)".
- `packages/cli/README.md`: one paragraph.
- This file.
The worktree's `node_modules` symlink is not part of the candidate.
## User mode is unchanged
Every `systemctl` call goes through `sc` and every `journalctl` call through `jctl`. Under `user` they
are `systemctl --user "$@"` and `journalctl --user "$@"`. Under `external`, `sc` allows only `show`,
`is-active` and `list-units`, and `jctl` returns 1, so an external run can't start, stop, install or read
the journal even through a path the tests miss.
Two tests hold the brief's "every command line and every byte of output":
- `user mode: every systemctl call carries --user, and activate, rollback and check make the base's
calls`. A fixture repo gets the base's two scripts from `git show cb5fdb064:`. The same activate,
rollback and `stack-copy.sh check` run from both trees, and the recorded argv (with the fixture root and
port replaced by placeholders) must match line for line. Every call carries `--user`.
- `user mode: a default activate --plan prints the base's bytes`, for four plans.
The base output is produced live from the base blobs, not stored as a fixture file. See deviation D3.
## r6 section 7, test by test
All in `stack-release-external.test.mjs` unless named.
| r6 :742-812 item | Test |
|---|---|
| `units-running` (N4), store, log and TAKEN left | `units-running (N4) refuses while a unit is active...`; also checks that `failed` counts as down and that the Console is checked first |
| activate leaves the pointer, writes `AWAITING-UNITS` and a backup | `activate stages: ...`, which also checks the set's file list, `UNITS.sha256`, no `--user`, no journal call and `status` |
| `open-activation` with zero, two, the wrong copy, one not open | `confirm and revert refuse open-activation ...` |
| `confirm` polls: 3 s fails at `--timeout 1`, passes at `--timeout 10` | `confirm polls: bus status good after 3 s fails ...` and `confirm polls to a pass ...` |
| `stale-start` floor with `started` at .900 | `stale-start: @1792540799 fails ...` and `stale-start is a gate code ...` |
| `confirm` pass writes pointer, ok, removes marker; timeout writes `FAILED` and exits 1 | the two polling tests |
| `activation-failed` when `FAILED` exists | `confirm polls: ... FAILED then refuses activation-failed` |
| lines carry the started op, from, to, activation and backup | `sameAsStarted` in every confirm test, and sequence 7 |
| `gate` (N2) with and without `--since` | `gate (N2, A2f): ...` and the usage test |
| `--plan --expect-from` (N5), activate and `rollback --to` | sequence 7 |
| A3 order: capture-failed, marker-failed, wrapper failure after migrate | `A3 order: ...` and sequence 2 |
| refusals log no activation; `interrupted` unchanged | `open-activation` test and sequence 5 |
| PM check: one, none, two, early, not live, other business, other instance; no journal call | `the external PM check reads launches list ...` (modes '', coder, none, twolive, early, dead, other) and `journalCalls` in every external test |
| `external-pm` | `pm-on and activate --pm-after-harness refuse external-pm` |
| `revert` restores TAKEN only after a migration | `revert puts the wrapper back and leaves the store ...` and sequences 2 and 8 |
| `confirm --rollback` per cause, `unrecorded`, `rollback-failed`, mismatch | sequences 1, 2, 3, 5 and 8, and `confirm --rollback refuses rollback-mismatch, and logs rollback-failed ...` |
| sequences 1-8 | `sequence 1:` to `sequence 8:`, each ending with the `closed` check (one started and one result line, empty `interrupted`, no marker or `FAILED`, a following `--plan` that doesn't refuse `interrupted-run`) |
| `rollback --to` refuses not-previous; `rollback --restore` finds an external backup | sequence 7, and `confirm --rollback after a --restore swap ...` |
| `gate` writes nothing | the `gate (N2, A2f)` test and sequence 4 compare the log, the pointer and the state tree |
| `confirm`, `revert`, `gate`, `--to` refuse without `--units external` | `confirm, revert, gate, --to and --expect-from need --units external ...` |
| default path: every call carries `--user`; default `--plan` bytes | the two user-mode tests above |
The brief's additions, in `stack-copy.test.mjs`:
- `row 232: copy --bundle copies a SHA the bundle's refactor reaches, byte-identical to copy SHA, and
refuses the rest`: a short SHA, a missing file, a symlink, a bundle that fails `git bundle verify`, a
bundle with no `refs/heads/refactor`, a SHA not in the bundle, and a SHA the bundle carries on another
branch that refactor doesn't reach. The bundle copy's tree matches `copy SHA`'s byte for byte, and the
`release:` and `bundle sha256:` lines are checked.
- `row 232: wrapper --create writes the first wrapper only, the same bytes wrapper ID writes, and refuses
over any file`, a symlink included. Without `--create` a missing wrapper still refuses.
- `row 232: check --units external reads the system units, the default reads the user units`.
## Mutants
`mutants.sh` in my scratch (not committed, it edits the worktree), 17 mutants: r6's re-pin list (:817-838)
minus the three `case1-repin.sh` ones, plus the brief's two. Each one applies exact replacements after a
checked backup, runs `bash -n`, runs its named killers, restores the file and checks its sha256. Two
controls run first with no change: the whole external file, and stack-copy's `row 232:` tests.
| Mutant | Killers |
|---|---|
| m01 `confirm` writes the pointer on a gate failure | `confirm polls: bus status` |
| m02 `gate_once` in place of `gate` | `confirm polls to a pass`, `stale-start is a gate code` |
| m03 PM check counts any instance | `external PM check` |
| m04 PM check drops the `startedAt` bound | `external PM check` |
| m05 `confirm` takes the newest `AWAITING-UNITS` | `open-activation` |
| m06 `rollback --to` skips the comparison | `sequence 7:` |
| m07 the default path drops `--user` from one call (`is-active`) | `user mode: every systemctl` |
| m08 the timeout writes r4's interim `rollback pending` line | `confirm polls: bus status`, `sequence 1:`, `sequence 5:` |
| m09 `AWAITING-UNITS` last, after the wrapper | `sequence 2:` |
| m10 a refusal names the open activation | `sequence 5:`, `open-activation` |
| m11 `stale-start` is a refusal | `stale-start is a gate code` |
| m12 `revert` restores without `MIGRATING` | `revert puts the wrapper back` |
| m13 `confirm` logs op `confirm` | `sequence 7:`, `confirm polls to a pass` |
| m14 `MIGRATING` after the migration | `sequence 8:` |
| m15 `gate` takes its floor from the log without `--since` | `gate (N2` |
| m16 `copy --bundle` skips the reachability check | stack-copy `row 232:` |
| m17 `wrapper --create` overwrites | stack-copy `row 232:` |
## Deviations and choices
D1. The shared helpers moved to a new file, `packages/cli/tests/stack-release-fixture.mjs`. The brief's
files list names `stack-release.test.mjs` "where a shared helper moves" but not a destination. Importing
from the other test file would run its 74 tests twice, so the helpers need a module of their own.
D2. The fixture's T17 check (what a run may never remove) now exempts a rollback set's `AWAITING-UNITS` and
`FAILED`. `confirm` removes both when it closes the activation (r6 A5 and R4). User mode writes neither, so
user-mode runs check exactly what they did.
D3. The user-mode argv and `--plan` tests read the base's scripts with `git show cb5fdb064:` rather than
storing their output as a fixture file. A later row that changes user-mode output on purpose moves the
`BASE` constant in the test to its own base, or the test fails. I chose this over a stored fixture because
a stored copy can drift from the base without anyone noticing.
D4. `marker-failed` is reached by making `state/stack-rollback` unwritable. r6 says "an existing `$RS`", but
`next_activation` never picks a taken name, so no test can make the run meet an existing `$RS`. The
exclusive `mkdir` is still what refuses.
D5. The stale-start polling case turns the enter time to `@1792540800`, not r6's `@1792540805`, with the
started line at 1792540800.900. That also checks that the floor is the whole second, so .900 doesn't push it
to 1792540801.
D6. No fake `sudo` or `case2-unit.sh`. r6's sequences ran under them because r6 had `case1-repin.sh` drive
the steps, and that script is I3's. Here the test plays root: it stops and starts the fake system units and
installs with the existing `bus-service.sh` and `webui-service.sh` stand-ins, between the real
`stack-release.sh` calls, on one log.
D7. The fake marker in the `open-activation` test is named `20000101T000000Z-<C1 prefix>` so it sorts
before the real one. A marker that sorted after it would leave m05 (newest marker) alive.
D8. m10 sets `ACT` inside `open_activation`, where r6's wording ("ACT set before the checks") lands in this
code. `ACT` is set only after every check in `confirm`.
D9. Row 200's `--sso` is a usage error under `--units external`, with the proof flags, `--no-proof` and
`--no-send`. It is a Console flag, and root's Console unit carries the Console flags; the script under
external renders no Console line and never calls `console_for_target`, so `sso_checks` doesn't run there
either. The usage text, the TOOLS.md bullet and the external usage test name it.
D10. Row 200's `config_checks` runs under `--units external` too, after `plugin_checks`, in both the
`--plan` and run paths of `activate` and `rollback --to`. It checks the target's loader against the stack
user's `config.json`, which the bus on a VM still reads, so a `console` key the target refuses would break
root's bus start the same way. The cost is one more refusal (`config-console-key`) an external run can
print; I took it over a bus that fails after root's start. The rebase changed no user-mode line: the
conflicts were the `SSO=0` globals line, the `plan` body (row 200's `SSO_PLAN` line sits in the user
branch) and the two `--plan` lines that gained `config_checks`. In the tests, row 200's fixture lines
(the loaders, `sso-config.mjs` and the `--sso` cli.mjs line for C1 and C) moved into
`stack-release-fixture.mjs` with the rest of the copy setup, and the `ALGS` import stays in
`stack-release.test.mjs`, which T57 uses.
## duckworth-0055's notes on r6 (brief line 169)
Duckworth's r6 approval (REVIEW-r5.md at 23f5520b5) carried two notes.
- N1, A2's stop timeout under `gate --since`. A unit that never stopped keeps its old `ActiveEnterTimestamp`,
so `gate --since` fails it with `stale-start`, and the run ends at exit 2 with root stopping the units.
That is intended: a unit that won't stop is a fair reason to stop. In I1 this is the gate code and its
polling (`stale-start: @1792540799 fails ...`, `gate (N2, A2f)`, m11). The run sheet's A2f line and the
re-pin test that points "exits 1 with OLD's gate passing" at an A3 exit 3 belong to I3, which owns
`scripts/stack-upgrade.sh` and `docs/guides/host-run-sheet.md`. I'll name both in the I3 build.
- N2, the claude path under `/opt/mosaic-stack` (gosalyn-0058, sage-1522). I1 doesn't touch it. Under
`--units external` the script checks no claude pin of its own (`no-claude-pin` applies only to user
mode), because root's unit carries it. The `--claude` value, the managed-settings reasoning, F6 and the
run sheet's claude step go to I2 and I3.
## Run environment
Worktree `/mnt/storage/scratch/darkwing-r232`, `TMPDIR=/mnt/storage/scratch/tmp/r232`, node from PATH.
One external file run takes about 94 s.
## Results
Singles on the rebased candidate, before the gate:
| File | Tests | Pass | Fail |
|---|---|---|---|
| `packages/cli/tests/stack-release-external.test.mjs` | 25 | 25 | 0 |
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 16 | 0 |
| `packages/cli/tests/stack-release.test.mjs` (row 200's T57 included) | 192 | 192 | 0 |
| `packages/cli/tests/stack-instances.test.mjs` | 23 | 23 | 0 |
| `packages/webui/tests/pinned-copy.test.mjs` | 10 | 10 | 0 |
| `packages/cli/tests/channel-recovery.test.mjs` (imports `channel-fixture.mjs`, which names the scripts) | 1 | 1 | 0 |
| `packages/cli/tests/host.test.mjs` (reads `packages/cli/README.md`) | 42 | 42 | 0 |
These are every test file that reads `stack-release.sh` or `stack-copy.sh` (a grep of `packages/*/tests` and
`scripts/`). None of them pins a sha256 of a changed file. `packages/cli/tests/host.test.mjs` reads
`packages/cli/README.md` for a unit file name; this row only adds a paragraph there. The grep and the runs
were repeated on the rebased tree on 2026-10-21; the reader set is the same. A dry run of `mutants.sh`
matches each of the 17 replacements once on the rebased scripts.
### Gate, 2026-10-21, Sage's slot (sage-1618)
Candidate a0ffabc3f on `darkwing/row232-r1`, run by `~/darkwing-scratch/r232/gate.sh`: the cli suite, then
every `scripts/test-*.sh`, one after another, stopping at the first failure. busy.sh passed at 01:07:49Z
after Launchpad's single `stack-release.test.mjs` run ended. The gate stopped at the first one, so the
chain waited for that PID to exit and checked again. The EXIT trap removed the compose network; no
container was left.
| Step | Time (UTC) | Result |
|---|---|---|
| `packages/cli`, `node --test tests/*.test.mjs` | 01:07:50 to 01:17:53 | 561 pass, 0 fail, 0 skipped |
| `scripts/test-auth.sh` | 01:17:53 to 01:17:54 | 15 passed, 0 failed |
| `scripts/test-conductor.sh` | 01:17:54 to 01:18:00 | 17 passed, 0 failed |
| `scripts/test-config.sh` | 01:18:00 to 01:18:02 | 31 passed, 0 failed |
| `scripts/test-discord.sh` | 01:18:02 to 01:18:12 | 78 passed, 0 failed |
| `scripts/test-extension-package.sh` | 01:18:12 to 01:18:13 | 18 passed, 0 failed |
| `scripts/test-foundation.sh` | 01:18:13 to 01:19:01 | 44 passed, 0 failed |
| `scripts/test-land-work.sh` | 01:19:01 to 01:19:14 | 86 passed, 0 failed |
| `scripts/test-queue.sh` | 01:19:14 to 01:19:37 | 156 node tests pass, queue suite 28 passed, 0 failed |
| `scripts/test-release.sh` | 01:19:37 to 01:19:51 | 14 passed, 0 failed |
| `scripts/test-task.sh` | 01:19:51 to 01:20:44 | 98 passed, 0 failed |
### Mutants, 2026-10-21, same slot
`~/darkwing-scratch/r232/mutants.sh`, right after the gate. Both controls (the external file whole, and
stack-copy's `^row 232: ` tests) exited 0 on the unmutated tree. Each mutant was applied after a checked
backup and a syntax check, ran its killers from the table above, and was put back with its sha256
checked. The script stops on any restore mismatch, and it ran to its end.
| Mutant | Killer exit | Result |
|---|---|---|
| m01 pointer on gate failure | 1 | killed |
| m02 gate once | 1 | killed |
| m03 PM from any instance | 1 | killed |
| m04 no startedAt check | 1 | killed |
| m05 newest marker | 1 | killed |
| m06 --to not compared | 1 | killed |
| m07 one call without --user | 1 | killed |
| m08 interim pending line | 1 | killed |
| m09 marker written last | 1 | killed |
| m10 refusal names the open activation | 1 | killed |
| m11 stale-start refuses | 1 | killed |
| m12 revert always restores | 1 | killed |
| m13 confirm op | 1 | killed |
| m14 MIGRATING after migrate | 1 | killed |
| m15 gate floor from the log | 1 | killed |
| m16 bundle reachability unchecked | 1 | killed |
| m17 --create overwrites | 1 | killed |
17 of 17 killed, 01:22:16 to 01:24:32Z. I read the logs for m01 and m09, two of the quick kills: each failed on
its named test's assertion (m09 on `AWAITING-UNITS` in sequence 2), not on a load error.
## Round 2: rebase onto 8fede5fc6 (sage-1673)
Rebase only. Before the landing gate, a check of BASE..HEAD on the manifest paths found that row 230
(c8bc5cf4c) had changed one of them after the round 1 base, cb5fdb064. Sage ruled a re-round with a
scoped review, the same shape as row 210 round 3.
Branch `darkwing/row232-r2`: the two round 1 commits, a0ffabc3f and 84f1a91e6, cherry-picked onto
8fede5fc6 with no conflict, then this section. No code or test changed.
- `git merge-tree --write-tree origin/refactor darkwing/row232-r1` (refactor at 8fede5fc6) gives tree
837bde79236c with no conflict. The cherry-picked tip before this section has the same tree.
- `packages/cli/README.md` is a clean 3-way merge. Row 230's hunk is at lines 710-717 (the session credential
paragraph); this row's is a new paragraph at 773. With `git patch-id --stable`, the README diff from r1 to
r2 is 02b679e2, the same as row 230's README diff `cb5fdb064..8fede5fc6`. This row's README diff is
c395ecd2 on both bases (`cb5fdb064..r1` and `8fede5fc6..r2`).
- The seven code, test and doc paths other than the README hash the same as in manifest 7a3b4ea1, which
Filbert approved in round 1: `docs/TOOLS.md`, `packages/cli/tests/stack-copy.test.mjs`,
`packages/cli/tests/stack-release-external.test.mjs`, `packages/cli/tests/stack-release-fixture.mjs`,
`packages/cli/tests/stack-release.test.mjs`, `scripts/stack-copy.sh`, `scripts/stack-release.sh`.
This file also matched until this section was appended. Its other lines are unchanged.
- The one test that reads the README, `host.test.mjs:855`, looks for the unit's `EnvironmentFile=` line.
That line is unchanged context in row 230's hunk.
- No row 230 test reads `stack-release.sh` or `stack-copy.sh`, and no test of this row reads a row 230 file.
Nothing was run for this round. The landing gate runs on the r2 tip on Sage's slot.
Follow-up, doc-only (sage-1673): `scripts/case2-unit.sh:108` and `scripts/case2-precheck.sh:157` cite
`scripts/stack-copy.sh:8` and `:22` for `COPIES` and `copy_root`. With this row those lines are `:10` and
`:26`. Only comments are affected and no test reads them. Row 233 doesn't touch those scripts, so the fix
goes on the #1732 carry list.
+249
View File
@@ -0,0 +1,249 @@
# Row 233 (I3): root's upgrade driver and the host run sheet
Brief: `docs/plans/2026-10-20_installer-rows.md` section I3, blob `e61291bcf1c6` (commit a0e3d7cfd), re-pinned
by Sage (sage-1685, rev 2631) from `f35e7e57daee` (ad6f0be7a). The later blob only appends I4; the I3 text
is the same. Gitea #1735, Vikunja P19
#432. Owner Darkwing, reviewer Filbert, gate owner Sage. Design input: row 195 DESIGN r6,
`agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66, sections 2, 4 and 7. Where the brief and r6
differ, the brief wins. Sage's rulings during the build: sage-1650 and sage-1652 (`--backup last`, the
`--no-rehearsal` mapping, `--rehearse-port`, hostcheck), and sage-1653 (`from_log newest`, the two external
plan refusals, the `BACKUP_STORE` line).
## Files
Base: e65a3ab4d, row 232's landing, on `darkwing/row233-r1`. The build started on ea1e85c7b
(`darkwing/row233`, row 232's candidate e6ca230d2 and its BUILD.md) before row 232 landed. The three row 232
files this row changes are identical on the two bases, so the uncommitted diff moved across unchanged.
- `scripts/stack-upgrade.sh` (new): `upgrade` and `rollback`, root's side of r6 A1-A5, A2f and R1-R4.
- `packages/cli/tests/stack-upgrade.test.mjs` (new, 20 tests).
- `docs/guides/host-run-sheet.md` (new).
- `docs/TOOLS.md`: a new subsection "Root's upgrade driver (`scripts/stack-upgrade.sh`, row 233)", plus the
row 232 lines below.
- Row 232 files, changed on Sage's rulings (the brief's files list doesn't name them):
- `scripts/stack-release.sh`:
- `rehearse --backup last` takes the newest backup the log records (`from_log newest`) and prints
`PASS backup-last`. With none it fails `no-backup`, exits 3 and makes no rehearsal directory.
A newest backup that isn't an activation id aborts `backup-unreadable`, exit 1.
- The rehearsal prints `BACKUP_STORE <activation id> schema <version>` before its migration step.
- `--plan` under `--units external` refuses `open-activation` when any rollback set holds
`AWAITING-UNITS`, and `migrate-required` for a migrating target without `--migrate`. Root reads
only the plan's exit, so these two have to be refusals there. User mode prints the same bytes.
- `packages/cli/tests/stack-release.test.mjs`: three new `row 233:` tests for `--backup last`.
- `docs/TOOLS.md`: `--backup ACTIVATION|PATH|last` in the rehearse usage, a paragraph on `last`, and a
paragraph on the two external plan refusals.
- This file.
## The brief, item by item
| Brief item | Where |
|---|---|
| Root runs from a root-owned checkout, never a copy | `head-mismatch` (A1); TOOLS.md; run sheet section 6 |
| I2's render with `/etc/mosaic-stack/<instance>.args`, `--copy` the one changed value | `read_args`, `args_with`; the args test |
| The args file changes only after a confirmed run | `prepare_args` before A2, `mv` after A5; sequences 1 and 7, m6 |
| `copy --bundle` as the stack's user before A1 | the `fetch`, `ff`, `copy` steps; sequence 1's sudo keys |
| A rehearsal before A2, a failed one refuses with nothing stopped | `rehearse`; three rehearsal tests, m4 |
| The root lock, held to the end, rollback included | `flock -n 9`; the lock test and sequence 3's probe, m1, m2 |
| A1 refuses `open-activation` | stack-release.sh's external plan; the AWAITING-UNITS test, m3 |
| `migrate-required` without `--migrate` | stack-release.sh's external plan; its test, m5 |
| The step log, 0640, one line per step | `logl`, `step`; every test checks lines against `STEP` |
| Exit codes 0-3 | `finish`, `refuse`; exit 4 is usage |
| duckworth-0055's notes | the next section |
| Root parses no stack user output beyond exits | the step log test; the one exception is sage-1653's `BACKUP_STORE` |
| The host run sheet | `docs/guides/host-run-sheet.md` |
## duckworth-0055's notes on r6 (brief line 169)
- N1, A2's stop timeout under `gate --since`. Row 232's BUILD.md left two pieces to I3. The run sheet's
A2f line is in section 6 ("When a run doesn't exit 0", exit 2). The test is `N1: a bus that outlives
A2's timeout keeps its old start`: a bus that ignores `stop` keeps its old `ActiveEnterTimestamp`,
`wait-down` logs `rc=1 ... state: active`, A2f restarts and gates, the gate fails `stale-start`, and the
driver stops both units and exits 2. Sequence 4 is the other half: an A3 exit 3 goes to A2f and exits
1 with the old copy's gate passing.
- N2, the claude path under `/opt/mosaic-stack`. Run sheet section 3 installs it there as root with
managed settings. The args file's `--claude` carries it, and `case2-unit.sh` already refuses a binary
that isn't root's or sits under the home. The driver passes the args file through and adds no claude
check of its own. F6 is I2's.
## Sage's rulings during the build
- sage-1650 and sage-1652. Root always runs the rehearsal and maps its exit:
- 0 goes on, or refuses `rehearsal-required` with `--no-rehearsal`.
- 3 refuses `no-backup`, or with `--no-rehearsal` logs `rehearsal-skipped` and goes on.
- Anything else refuses `rehearsal-failed`.
`--rehearse-port N`, default 7339. Hostcheck: a missing file or a nonzero `--check` refuses
`profile-drift`.
- sage-1653. `from_log newest`, and the external plan refusals `open-activation` and `migrate-required`.
Residual: the `BACKUP_STORE` id and schema land in root's step log, but nothing ties that backup to the
one the activation's own backup step takes later. A stopped backup before the upgrade would close that
gap; it's a later row, not this one.
## Tests
`stack-upgrade.test.mjs` runs the driver as the test's user against a scratch root, with the test seams
`STACK_UPGRADE_*`. The stand-ins are a fake `sudo` (logs its argv, checks the driver's form, then runs the
real command as the test's user, r6's B1 pattern), a fake `case2-unit.sh` render that installs and starts
the fixture units, a fake `systemctl` for root's system units, and a shell `hostcheck.sh`. The stack's
user's steps run the real `stack-release.sh` and `stack-copy.sh` from a clone made from a real bundle. The
units, copies and store are `stack-release-fixture.mjs`'s.
| Test | Covers |
|---|---|
| sequence 1 | confirm fails healthz, rollback with `--cause A5`, exit 1; every sudo call in the driver's form |
| sequence 2 | `activate --migrate` fails at the wrapper, store restored, `--cause A3`, exit 1 |
| sequence 3 | render fails, `--cause A4`; a probe run during `render-old` refuses `upgrade-running` |
| sequence 4 | stage exits 3, A2f restarts and gates the old units, exit 1 |
| N1 | a unit that won't stop: A2f's gate fails `stale-start`, exit 2, units stopped |
| sequence 5 | confirm of the wrong copy refused, `--cause A5`, exit 1 |
| sequence 6 | revert fails, exit 2; the next run refuses `interrupted-run` at its plan |
| sequence 7 | two upgrades and a `rollback --to` with `--restore`; the args file after each confirm; `to-not-previous` |
| sequence 8 | a migration that writes and fails, store back byte for byte, exit 1 |
| lock | a second run while the lock is held: one log line, no call |
| open-activation | any `AWAITING-UNITS` refuses at A1, nothing stopped |
| migrate-required | refused without `--migrate`, no rehearsal, nothing stopped |
| rehearsal-failed | the rehearsal's argv, exit 3, nothing stopped |
| `BACKUP_STORE` | the line after a match; four malformed lines log `BACKUP_STORE none` |
| no-backup | the real rehearsal with no backup refuses; `--no-rehearsal` logs the skip and goes on |
| backup-last | the real rehearsal names the newest backup; `--no-rehearsal` with one refuses `rehearsal-required` |
| args file | group write, other write, two `--copy`, an unknown flag, a symlink, `args-differ` |
| hostcheck | nonzero `--check` and a missing file refuse `profile-drift` before the plan |
| head-mismatch | checkout not at `--sha`; a commit the clone can't fast-forward to refuses `fetch-failed` |
| usage | twelve bad argvs exit 4 with no log and no lock |
The step log check (`STEP`) runs in every test that reads the log: each line must be a step line or the
result line, and none may hold the stack user's output (`stack-release:`, `PASS `, `FAIL `, `REHEARSAL`,
the fake rehearsal's text).
## Mutants
`~/darkwing-scratch/r233/mutants.sh`, not committed because it edits the worktree. It has the brief's six
mutants and two for sage-1650's mapping. The method is row 232's: a checked backup, exact replacements, a
syntax check, the named killers, then the file put back with its sha256 checked. One control runs first:
the whole `stack-upgrade.test.mjs` with no change. `DRY=1` matched each replacement once on the candidate
(2026-10-21).
| Mutant | File | Killers |
|---|---|---|
| m1 drop the flock | stack-upgrade.sh | `a second run while the lock is held`, `sequence 3:` |
| m2 release the lock when the rollback starts | stack-upgrade.sh | `sequence 3:` |
| m3 skip A1's open-activation check | stack-release.sh | `A1 refuses open-activation` |
| m4 skip the rehearsal | stack-upgrade.sh | `the rehearsal runs after the plan`, `sequence 1:` |
| m5 accept a migrating target without `--migrate` | stack-release.sh | `migrate-required` |
| m6 write the args file before confirm | stack-upgrade.sh | `sequence 7:`, `sequence 1:` |
| m7 `--no-rehearsal` accepted with a backup | stack-upgrade.sh | `refuses rehearsal-required` |
| m8 drop the `rehearsal-skipped` line | stack-upgrade.sh | `a real rehearsal with no backup` |
## Deviations
D1. The brief has A1 refuse `open-activation` for an open `AWAITING-UNITS` or a non-empty `interrupted`
set. The interrupted case refuses with row 232's existing code, `interrupted-run`; only `AWAITING-UNITS`
gives `open-activation`. Both reach root as `plan-refused`, exit 3 (sequence 6, the open-activation test).
D2. Root copies one line of the stack user's output into its log, `BACKUP_STORE`, on sage-1653. It
matches a fixed pattern first, takes the last match, logs `BACKUP_STORE none` otherwise, and decides
nothing on it.
D3. The stack user's steps carry no `--instance`. The driver takes `--instance dev` only, row 190's default
instance, which the stack user's commands select with no flag. A named instance's units are a later row.
D4. Most tests fake the rehearsal in the fake `sudo`: the fixture's stub copies can't pass a real one. Two
tests run the real rehearsal (`faults/real-rehearse`): no-backup and backup-last. `stack-release.test.mjs`
covers `--backup last` itself.
D5. Any nonzero `--plan` exit refuses `plan-refused`, exit 3, whatever the code. The stack user's output
above it names the refusal; root doesn't parse it.
D6. `rollback --restore ACT` is accepted without `--events`, as `stack-release.sh rollback` accepts it.
The brief's usage pairs them. `--events` without `--restore` is a usage error.
D7. The refusal of any `STACK_UPGRADE_*` seam when run as root has no test, because the tests don't run
as root. It is three lines at the top of the script.
D8. The fake `sudo` keeps the test's environment, where real `sudo` resets it. The driver passes
everything the stack user needs through `env PATH=...` and `-H`, and the fake checks that form.
D9. Exit 3 means nothing stopped, not nothing changed. A refusal after `fetch` leaves the stack user's
clone fast-forwarded and the new copy made. Both are the user's own state, and the next run reuses them.
TOOLS.md and the run sheet say so.
D10. The brief's usage has neither `--no-rehearsal` nor `--rehearse-port`. Both come from sage-1650 and
sage-1652.
## Decisions
- `rollback` has no HEAD check. Its target is an older copy than root's checkout, which already exists.
It fetches, copies and rehearses nothing (sequence 7).
- The new args file is written next to the old one (`.dev.args.XXXXXX`, same mode) before A2, so a full
`/etc` refuses before anything stops. It moves into place after A5. A2f and the rollback remove it; a failed move keeps it and names it.
- Every stack user step gets `--copies` explicitly, so the copies directory comes from the args file's
`--home`, never from the user's environment.
- The stack's user does the fetch, the fast-forward and the copy, so root never runs git on a tree that
user can write.
- A4 is the render, and `case2-unit.sh` starts the bus, then the Console. The forward path has no
`systemctl start` of its own; only A2f does.
## Run environment
Worktree `/mnt/storage/scratch/darkwing-r233`, `TMPDIR=/mnt/storage/scratch/tmp/r233`, node from PATH. One
whole `stack-upgrade.test.mjs` run takes about 56 s.
## Results
### Singles, run 1, 2026-10-21, Sage's slot (sage-1689)
`~/darkwing-scratch/r233/singles.sh` on `darkwing/row233-r1` (base e65a3ab4d, uncommitted tree), 03:48:20Z
to 03:59:00Z. These are the test files that read a changed script. `stack-instances.test.mjs` reads
`stack-release.sh`, so it joined the three named in the plan.
| File | Result |
|---|---|
| `stack-upgrade.test.mjs` | 20 pass, 0 fail |
| `stack-release.test.mjs` | 194 pass, 1 fail |
| `stack-release-external.test.mjs` | 25 pass, 0 fail |
| `stack-instances.test.mjs` | 23 pass, 0 fail |
The failure was my test, not the script. "row 233: --backup last takes the newest backup..." compares
`untouched(e)` before and after the run, and `untouched` includes `existsSync(e.log)`. The test took its
before snapshot ahead of `rlog()`, which writes the log, so that flag went from false to true. Every hash in
the snapshot matched. The fix takes the snapshot after `rlog()`. No other row 233 test calls `untouched`.
### Singles, run 2
`~/darkwing-scratch/r233/rerun.sh` on aacfa2d7a, Sage's slot (sage-1695), 04:13:05Z to 04:21:28Z:
`stack-release.test.mjs` whole, 195 pass, 0 fail, 0 skipped. The fixed test passed. The other three files
did not change after run 1. The gate and the mutants run next, in Sage's slot.
### Gate and mutants, 2026-10-21, Sage's slot (sage-1704)
`~/darkwing-scratch/r233/gate.sh` then `mutants.sh`, chained behind `busy.sh`, on d99166ade (code
aacfa2d7a; d99166ade changes only this file). Counts are an awk sum over `gate-r1.out`.
| Part | Time (UTC) | Result |
|---|---|---|
| cli suite (`node --test tests/*.test.mjs`) | 04:29:01-04:37:27 | 616 pass, 0 fail, 0 skipped |
| the 10 `scripts/test-*.sh` | 04:37:27-04:39:38 | selftests 429 passed, 0 failed; `test-queue`'s node part 156 pass, 0 fail |
| gate total | | 1201 pass, 0 fail, 11 suites, `GATE_PASSED` |
| c0 control | 04:39:38-04:40:35 | exit 0 |
| m1-m8 | 04:40:35-04:41:25 | 8 of 8 killed, every one exit 1 |
Every restore passed its sha256 check, and `git diff` was clean at d99166ade afterwards. The EXIT trap
removed the `darkwing-r233_default` network.
N1, found in the process sweep after the run: a `sleep 60` was left with its cwd in `packages/cli`.
`stack-upgrade.test.mjs:312` spawns `flock LOCK sleep 60`, and its `t.after` SIGKILLs only the flock, so
flock's child outlives the test by up to 60 s. It exited on its own. Row 232's T20
(`stack-release.test.mjs:370`) spawns its holder `detached` and kills the group with `process.kill(-pid)`.
The fix is the same two lines. That test is one of m1's killers.
Sage ruled it a fix now (sage-1707), and 08db6ac3f makes it, test only. After the gate's d99166ade
the only change outside this file is that test:
```
$ git diff --stat d99166ade 08db6ac3f
packages/cli/tests/stack-upgrade.test.mjs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
```
The gate stands on Sage's ruling. The rerun is the whole `stack-upgrade.test.mjs` singly plus m1, in
Sage's slot, with a sweep for a leftover `sleep` afterwards.
+173
View File
@@ -0,0 +1,173 @@
# Row 240 (I4): root's host check, its pins and the boot unit
Brief: `docs/plans/2026-10-20_installer-rows.md` section I4, blob `4a5a65020f00` (commit b00d7d94d, rev 2665,
the pin row 240 carries; the I3 carry items came in with it). Sage split it out of I3 (sage-1650). Gitea #1746, Vikunja P19 #432. Owner Darkwing, reviewer Filbert, gate owner Sage.
Design: row 195 DESIGN r6 B2, `agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66, :262-303 (the script,
the three callers) and :839-860 (files, test cases, mutants). Where the brief and r6 differ, the brief wins.
## Files
Drafted on `darkwing/row240-r1` from d99166ade (row 233's candidate) while row 233's gate ran, then applied
as one patch onto row 233's landing, 57f59e0c3, on `darkwing/row240-r2` (sage-1753). It applied cleanly; between
the two bases the only change on this row's paths is 233's N1 test fix in `stack-upgrade.test.mjs`. Row 234
then landed as c771a36f4 on 57f59e0c3. Sage preferred that the gate run with it in the tree (sage-1773), so both
commits were picked onto c771a36f4 as `darkwing/row240-r3`. That was a clean pick: the 42 paths changed from
57f59e0c3 to c771a36f4 share none with this row's 9.
- `scripts/case1-pins.sh` (new, 0644): `PIN_PROFILES_VERSION`, `PIN_PROFILE_SHA256`, `PIN_BUBBLEWRAP_VERSION`,
r6 section 0's values.
- `scripts/case1-hostcheck.sh` (new, 0755): the seven items and `--check`, `--enforce`, `--upstream`.
- `packages/cli/systemd/mosaic-hostcheck.service` (new): the boot oneshot.
- `packages/cli/tests/case1-hostcheck.test.mjs` (new, 16 tests). The cli suite's `tests/*.test.mjs` glob picks
it up.
- `docs/guides/host-run-sheet.md` section 4: the install lines that replace "Install them from row 240's run
sheet lines once it lands", plus rollback, drift recovery and `--upstream` before a re-pin window. Sections
3, 6, 8 and 10 for the I3 carry items below.
- `scripts/stack-upgrade.sh` and `packages/cli/tests/stack-upgrade.test.mjs`: I3 N1 below.
- `docs/TOOLS.md`: a new subsection "Root's host check (`scripts/case1-hostcheck.sh`, row 240)" after the
upgrade driver's.
Brief line 257 said I3's `stack-upgrade.sh` doesn't change. Sage's ruling sage-1727 widened the row to it for
N1 alone. Its default `STACK_UPGRADE_HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh` is where the run
sheet installs this script.
## Carried from I3
Filbert's row 233 approve (Gitea 29922) left five notes. Sage ruled N1, N4 and N5 into this row and N2 and N3
to DEFERRED.md (sage-1727).
| Note | Change |
|---|---|
| N1: `stack-upgrade.sh` exits 1 `args-failed` when the args move after A5 fails, but exit 1 means the old copy runs | The move's failure exits 5 `args-failed`. The header, TOOLS.md's step 5 and exit line, and run sheet section 6's exit list name exit 5 and what root does: move the new file the result line names into place. Test `row 240: I3 N1: a failed args move after A5 exits 5 ...` makes `/etc/mosaic-stack` read-only at confirm and checks exit 5, the step names, `args rc=1`, the result line, both units on the new copy, the old args file's bytes and the new file beside it. The step log regex takes `exit=5`. Mutant M8. |
| N4: section 3's `npm install` has no npm on root's PATH, and the `--claude` path is unnamed while `node_modules/.bin/claude` is a symlink `case2-unit.sh:140` refuses | The install line runs with section 2's pinned node first on PATH (the postinstall runs `node` too). The `--claude` path is `node_modules/@anthropic-ai/claude-code/bin/claude.exe`, and a `--version` line catches the postinstall's 500-byte stub. |
| N5: sections 8 and 10 lack precheck lines | Each gets "Precheck line: none" and its missing line. |
Exit 5 is new rather than 2 because 2 means both units are stopped, and here both run. Exit 4 stays usage.
The `claude.exe` path comes from the 2.1.304 package itself: `npm view` gives `bin.claude` as `bin/claude.exe`,
and its `install.cjs` hardlinks the platform binary over that placeholder, or copies it across devices, then
sets 0755. I downloaded the tarball into `/mnt/storage/scratch/tmp/r240/cc304` and read it; nothing ran. The
run sheet says a newer pin needs the same check.
## The brief, item by item
| Brief line | Where |
|---|---|
| 247 `case1-pins.sh` holds the three pins | `scripts/case1-pins.sh`; the pins test checks the values against r6's |
| 248-249 seven items, `--check` exits 0 or 1 | `items()` and the final dispatch; the check tests |
| 250-251 `--enforce`: `apparmor_parser -R`, `root:root 0700`, `DRIFT`, a log line, exit 0 | `enforce()` and `stop_bwrap()`; the four enforce tests |
| 252 `--upstream` compares the current deb to the pin | `upstream()`; the upstream test |
| 253 the boot oneshot | `mosaic-hostcheck.service`; the boot unit test |
| 254-255 fakes for every host tool, r6's seven case groups, seven mutants | the test file; Tests and Mutants below |
| 257 the I3 run sheet item points here | run sheet section 4 |
## Tests
Fakes on PATH, each logging its argv to `$HC/calls.log`: `aa-status`, `dpkg`, `dpkg-statoverride`,
`apparmor_parser`, `systemd-run`, `stat`, `chown`, `chmod`, `apt-get`, `dpkg-deb`. `host(t)` builds a scratch
root (`/etc`, `/proc/sys`, `/usr/bin/bwrap`, `/var/log`) and a lib dir with a copy of the script and fixture
pins that hold the fixture profile's sha256. Every run passes `--root`.
r6's case groups (:843-856) against the tests:
| r6 case | Tests |
|---|---|
| `--check` exits 0 with all seven, 1 with each failing alone, its line naming it | `check: exits 0 ...`, `check: exits 1 with each item failing alone ...`, plus one test per item for the item's edge cases |
| item 3: a second file naming `bwrap` as a word, `/{usr/,}bin/bwrap` included; passes with the profile alone | `check: item 3 ...` |
| item 4: `unpriv_bwrap (enforce)` or a trailing space fails; the argv carries `-p NoNewPrivileges=yes` and the 2a' properties | `check: item 4 ...` checks the whole argv |
| item 7: the floor at 732 fails | `check: item 6 ...; item 7 ...` (733 passes as the control) |
| `--enforce` exits 0 on a pass and every failure; unload, close, `DRIFT`, one log line; `DRIFT` present with every item passing still closes | `enforce: a pass ...`, `enforce: each failure ...`, `enforce: with DRIFT present ...` |
| `--enforce` never calls `dpkg-statoverride` with `--update`, `--add` or `--remove` | `enforce: no unload ...; never writes the statoverride` (every call is `--list /usr/bin/bwrap`) |
| a bubblewrap version other than the pin is a line, not a failure | `check: a bubblewrap version other than the install pin ...` |
| `--upstream` against a differing deb prints the difference, exits 0 | `upstream: ...` (also: a match, a deb with no profile, a failed download) |
| the unit: `Type=oneshot`, `After=apparmor.service`, `[email protected]`, runs `--enforce` | `boot unit: ...` |
The last test also checks that the repo script reads `scripts/case1-pins.sh` beside it, that bad usage exits
2, and that a non-root run without `--root` refuses.
Singles are the two files that run a changed script: `case1-hostcheck.test.mjs` and `stack-upgrade.test.mjs`.
Sage asked (sage-1759) whether `stack-release.test.mjs` or `stack-release-external.test.mjs`, which share
`stack-release-fixture.mjs`, run `stack-upgrade.sh` or `case1-pins.sh`. They don't. `grep -cF` for
`stack-upgrade`, `case1-pins`, `case1-hostcheck` and `hostcheck` gives 0 in all three files; the control
`stack-release.sh` gives 2 in the fixture and 8 in `stack-release.test.mjs`. `scripts/stack-release.sh` names
`stack-upgrade.sh` once, in a comment (:2061). The gate's cli suite runs both files anyway.
## Mutants
r6's seven (:858-860) and one for I3 N1, scripted in `~/darkwing-scratch/r240/mutants.sh` with r233's method: a checked backup,
an exact replacement that must match once, `bash -n`, the named killer by `--test-name-pattern`, the restore
and a sha256 check. A dry run matched all eight once each. Results below.
| Mutant | Change | Killer |
|---|---|---|
| M1 | `--enforce` exits 1 after the stop | `enforce: each failure exits 0 ...` |
| M2 | `[ ! -e "$DRIFT" ] \|\| had=1` becomes `true \|\| had=1` | `enforce: with DRIFT present ...` |
| M3 | item 1's `dpkg -S` becomes `owner=""; rc=1` | `check: item 1 fails when a package owns ...` |
| M4 | item 3's `grep -rlw bwrap` becomes `grep -rlF /usr/bin/bwrap` | `check: item 3 ...` |
| M5 | item 4's `[ "$label" = "$LABEL" ]` becomes a substring match | `check: item 4 ...` |
| M6 | item 4 drops `-p NoNewPrivileges=yes` | `check: item 4 ...` (argv) |
| M7 | item 7's `-gt` becomes `-ge` | `check: item 6 ...; item 7 fails with the floor at 732 ...` |
| M8 | `stack-upgrade.sh`'s `finish 5 args-failed` becomes `finish 1 args-failed` (I3 N1) | `row 240: I3 N1: a failed args move ...` |
## Deviations
- Repo name `scripts/case1-hostcheck.sh` (r6), installed as `/usr/local/lib/mosaic-stack/hostcheck.sh`, the
name in brief line 239 and I3's `stack-upgrade.sh` default. r6's apt hook line says `case1-hostcheck.sh`;
the run sheet's hook uses the installed name.
- `stat` is a fake too. r6 lists seven fakes; a test can't make a root-owned file, so item 5's `stat` reads a
fixture.
- Item 7 with no Console unit installed reads the port as 1023 and says so ("1023, because no Console unit
is installed"). Until I2 installs the unit the floor must be above 1023. A unit with no `--port` fails.
- Item 4 runs under `timeout 60`. A hung `systemd-run` would otherwise hang the apt hook and the boot unit.
## Decisions
- Fail closed where r6 is silent:
- pins with no 64-hex sha fail item 1;
- `dpkg -S` exit 1 (no owner) passes, exit 0 fails with the owner, any other exit fails with the code;
- a failed `aa-status` fails item 2 and counts as loaded, so `--enforce` tries the unload.
- `--root DIR` is the test seam (r6 :840). Root refuses it; a non-root user needs it. Both exit 2. Root runs
with `PATH=/usr/sbin:/usr/bin:/sbin:/bin`.
- `DRIFT` is append-only (`<stamp> <failed line>`), and `--enforce` never removes it. The log line is written
only when the stop runs: `<stamp> enforce stop items=<ids or none> drift-before=<0|1> unload=<rc=N|no-file|skipped> chown=N chmod=N`.
- `--upstream` exits 1 when the download or the unpack fails, since it then compared nothing. A deb without
the profile file is a difference (exit 0), per r6 :302-303.
- The bubblewrap note reads the version from `dpkg -s bubblewrap`.
- The run sheet's recovery steps (`chown root:mosaic`, `chmod 0750`, `apparmor_parser -r`, then `--check`
before root removes `DRIFT`) are mine. r6 says only that root removes `DRIFT` in a reviewed step.
## Run environment
Worktree `/mnt/storage/scratch/darkwing-r240b`, branch `darkwing/row240-r3` on c771a36f4, with `node_modules`
linked to the canonical checkout's. Node v26.8.1, kernel 7.2.2-1-cachyos, 16 CPUs, TMPDIR
`/mnt/storage/scratch/tmp/r240`, compose project `darkwing-r240b`. Sage's slot (sage-1773, 06:16Z). Each run is
one chain behind `busy.sh`, launched from `/`: `~/darkwing-scratch/r240/run-all.sh` checks the HEAD pin and a
clean tree, runs the singles, then the mutants (8 killed, none surviving, the tree clean after), then the gate
(the cli suite and every `scripts/test-*.sh`), each stopping the chain on failure, then sweeps `/proc` cwd links
for anything left in the r240 trees.
## Results
Run r1, 06:17:10Z on 47ec9fe2f, failed at the singles. `case1-hostcheck.test.mjs` passed 16/0/0, and
`stack-upgrade.test.mjs` went 20 pass, 1 fail. The failure was the N1 test's own `t.after` hook, which ran
`chmodSync(e.etc, 0o755)` after the fixture's cleanup had already removed the scratch root, so it threw ENOENT.
Every assertion in the test body had passed, exit 5 included. The fixture's cleanup runs `chmod -R u+w` on the
root before `rmSync`, so the 0555 dir never needed the hook. b45852daa drops it and changes nothing else. The
mutants and the gate didn't run. The sweep found nothing left. Sage counted r1 as this slot's one failure
(sage-1788).
Run r2, 06:20:41Z to 06:33:28Z on b45852daa, passed. The counts come from awk over the .out files
(`~/darkwing-scratch/r233/sum.sh`):
| Step | Result |
|---|---|
| `case1-hostcheck.test.mjs` | 16/0/0 |
| `stack-upgrade.test.mjs` | 21/0/0 |
| c0 control | exit 0 |
| M1-M8 | 8/8 killed, none survived, every restore's sha256 matched, tree clean after |
| `packages/cli` suite | 639/0/0 |
| `scripts/test-*.sh` | auth 15, conductor 17, config 31, discord 78, extension-package 18, foundation 44, land-work 86, queue 156 node + 28 selftest, release 14, task 98; 585 pass, 0 fail |
| Gate total | 11 suites, 1224 pass, 0 fail |
| Sweep | nothing left in the r240 trees; the compose network was removed |
This commit adds only these BUILD.md lines. No test reads BUILD.md.
+91
View File
@@ -0,0 +1,91 @@
# Row 245 build: Duckworth's N1-N3 on `scripts/case1-accept.sh` (row 195 follow-up)
Duckworth approved row 195's manifest round 1 (duckworth-0088, `agents/duckworth/work/row195/REVIEW-m1.md`
at 6afcee2a5) with three notes that don't block. Row 195's code landed as b65014647, and the notes are in
`docs/plans/DEFERRED.md` (d6cff0cdd). Row 245 carries them: brief `docs/plans/2026-10-20_hub-own-uid.md` §
"Row 195 follow-up" at 62ddc96cc, owner Darkwing, reviewer Duckworth, gate owner Sage (sage-1923).
The branch is `darkwing/row245`, on 62ddc96cc. Its first commit is a cherry-pick of `darkwing/row195-n1`
1e3c4767c, which was written on 37171f65b and never run. The script, test and TOOLS.md blobs are the same
as in 1e3c4767c, and none of those paths changed between 37171f65b and 62ddc96cc.
## What changes
N1. F9 ran `$ROOT$NODE` as root (`case1-accept.sh:298` at 37171f65b) before F10 checked that `--node` is
the pinned path. Duckworth offered two fixes: F9's parse through `AS_USER`, or F10's path check first. I
took the path check, plus one more:
- F9 now runs node only when `--node` equals `/opt/mosaic-stack/node-<--node-pin>/bin/node`.
- Then `not_root_only` walks from that file up to `/`. Each entry must be the running user's (root's on the
host) with no group or other write, the rule `case2-unit.sh` `root_only` applied when it rendered the
units. A symlink fails it, because its mode is 777. The first entry that breaks the rule goes in the
evidence.
I didn't run the parse through `AS_USER`. That would hand a check about a root file to the stack's user,
and a process running as `mosaic` shares that node's uid. Whether it could change what node prints then
turns on the host's ptrace setting, and the root-only check doesn't depend on that. Sage agreed in
sage-1877. Rechecking ownership costs one loop and doesn't depend on
how the host was installed.
N2. The header (:9-15) and the `docs/TOOLS.md` entry now list what root runs: `systemctl show` and
`is-enabled` (F1, P11), `curl` on the loopback healthz (F2), `sudo -l` (P1, P3), `sudo -u jarvis bwrap
--version` and `hostcheck.sh --check` (P9), and node for F9 under the rule above.
N3. F6 matched `--claude` by prefix, so `/opt/mosaic-stack/claude-<pin>/../claude-2.0.9/...` passed the
path check. F6 now fails on any `..` segment, before it runs `--version`. F10 compares for equality, so
it needed no change.
Row 195's BUILD.md gets a correction entry for deviation 6. Row 195's DESIGN.md is unchanged. Its "either
works" wording at `DESIGN.md:336-338` is superseded by gosalyn-0087 (root writes `dev.args` at W10), and
it stays in place: an approved design isn't edited (sage-1877).
## Tests
`packages/cli/tests/case1-accept.test.mjs`:
- The node stub touches `node-as-root` in the state directory when something runs it outside the fake
`sudo` (which sets HOME to the stack's home). `host()` sets the node file's directories and the scratch
root to 0755, so the umask can't fail F9.
- "every check passing" also asserts that root ran the pinned node.
- New: "F9 won't run --node as root off the pin path or with a writable directory above it". Another node
path fails F9 and F10, with no root run. A group-writable `bin/` and a symlinked node each fail F9
alone, with the bad path in the evidence and no root run.
- The F6 test gains a `..` case: a path under `claude-<pin>/` by prefix that names 2.0.9's binary, which
prints the pin. F6 alone fails, with "has a .. segment".
That makes 23 tests. All 23 pass on Sage's slot (results below).
## Mutants
The twelve from row 195 still apply, unchanged. These four are new, each one exact replacement:
| # | Mutant | Expected killer |
|---|---|---|
| 13 | F9 runs any `--node` (the pin check becomes `true`) | F9 won't run (another node) |
| 14 | F9 skips the ownership walk (`bad=""`) | F9 won't run (writable `bin/`, symlink) |
| 15 | `not_root_only` checks only the file, not the directories above it | F9 won't run (writable `bin/`) |
| 16 | F6 accepts a `..` segment | F6 stub, path and `..` |
The brief's gate asks for a mutant for each of N1 and N3. Mutants 13-15 are N1's, and 16 is N3's.
## Slot plan
Same shape as row 195's steps, on this row's tip: the single test file, then the sixteen mutants, then
the gate (the cli suite and every `scripts/test-*.sh`), each behind the busy checks and chained with
`&&`. The scripts are in `/mnt/storage/scratch/darkwing-245-out/`.
## Slot results (grant sage-1961, 2026-10-21)
All three steps ran on 0590a29fc with node v26.8.1, launched from `/`. Both busy checks were CLEAR
before each step.
- Singles, 11:27:46-11:27:59Z: `case1-accept.test.mjs` passed 23, failed 0.
- Mutants, 11:28:10-11:31:20Z: all 16 killed, each with exit 1. The runner restored the script, and
its sha256 875bb5b7 matches the committed blob. Rows 1-12 died as in row 195. 13, 14 and 15 died on
"F9 won't run --node as root off the pin path or with a writable directory above it". 16 died on
"F6 fails on a stub that prints nothing, on a path outside claude-<pin> and on a .. segment".
- Gate, 11:31:20-11:42:51Z: the cli suite passed 664, failed 0, skipped 0. Every `scripts/test-*.sh`
exited 0: auth 15, conductor 17, config 31, discord 78, extension-package 18, foundation 44,
land-work 86, queue 28 (its node part 156/0), release 14, task 98. That's 429 selftests, none failed.
- Afterwards no process had its cwd in the worktree, the out dir or TMPDIR, and the
`darkwing-r245_default` network was gone.
Logs: `/mnt/storage/scratch/darkwing-245-out/` (`step1.log`, `mut.out`, `mut/m1-16.log`, `gate.out`).
+191
View File
@@ -0,0 +1,191 @@
# Row 251 build: T27 lists no identities right after `ssh-add -t 1` (Vikunja #453)
Brief `docs/plans/2026-10-21_t27-ssh-add-lifetime.md` (rev 2931), Mos ruling mos-1185 item 2, assigned in
sage-2075. Owner Darkwing, reviewer Dewey, gate owner Sage. Branch `darkwing/row251` from 3b0aef995,
claimed in-progress at rev 2938.
## Step 1: the failure names its cause (e5e4b2e5a)
T27's two lists now carry a message that prints only when they fail. It gives the time from addKey's
start to its resolve and to each `ssh-add -l` call, the hrtime at each with how far into its second it
fell, `/proc/uptime` at each, and whether the agent process and its socket were still there. The checks
themselves did not change. The commit holds nothing else.
## Step 2: the cause
ssh-agent counts a key's lifetime in whole seconds of CLOCK_BOOTTIME, at the add and at the reap. I read
OpenSSH 10.5p1, the version on dragon-lin (`ssh -V`: OpenSSH_10.5p1), at tag V_10_5_P1 (ssh-agent.c
sha256 873691c4, misc.c sha256 ee467f43):
- `misc.c:1790-1798` and `:1828`: `monotime()` returns `ts.tv_sec` of `clock_gettime(CLOCK_BOOTTIME)`,
falling back to MONOTONIC. The fraction of the second is dropped.
- `ssh-agent.c:1315`: the `-t` constraint sets `*deathp = monotime() + seconds`. Lines 1410 and 1598
do the same for the agent's default lifetime.
- `ssh-agent.c:1064`: the reaper drops a key when `now >= id->death`, `now` being `monotime()` again.
- `ssh-agent.c:2650`: the main loop runs `reaper()` after every poll, before it reads requests. A list
that arrives after the boundary sees the key gone.
So a key added at second s plus a fraction f dies at s + N, and `-t N` keeps it N - f seconds, between
N - 1 and N. With `-t 1`, an add late in a second leaves the key milliseconds to live. That matches
Sage's probe in the brief: 8 of 80 `-t 1` adds missed, each one made at 0.9947-0.9973 of a monotonic
second and finished at 0.998-0.0008, and none earlier in the second missed. It also matches proof 5's
failure, where the first list ran 31.9 ms into the test and found no identities.
Node's hrtime is CLOCK_MONOTONIC. BOOTTIME differs from it by the time the host spent suspended. On
dragon-lin the difference read 0.000001 s at 19:09Z, so a monotonic second boundary is a BOOTTIME one
here.
`addKey` and the agent start are not the cause. `addKey` passes the lifetime through as `-t`
(`host-run.mjs:90`), and the agent starts with no default lifetime (`:175`). Production adds the key
with `timeout + 30` (`:180`), where losing up to a second makes no difference. The only caller that
can't afford the lost fraction is T27, which asks for 1 s. The fix belongs in the test's lifetime choice,
and `host-run.mjs` doesn't change.
The slot confirms this against T27 itself (below), not just from the source.
## Step 3: the fix (this commit)
T27 adds the key with `-t 2`. The agent then keeps it more than 1 s and at most 2 s. The first list
runs milliseconds after the add, so it has over a second of margin even under load. The sleep before
the second list grows from 2.5 s to 3.5 s, which keeps the old 1.5 s margin past the new 2 s ceiling.
Both checks stay: listed after the add, gone after the lifetime. A comment above the add says why the
lifetime is 2.
I considered `-t 3` with a 4.5 s sleep for a 2 s floor. One second of floor is far above anything the
first list has needed (31.9 ms in proof 5), and the concurrent-suite run below tests it under load, so I
took the smaller change.
## Base
The branch stays on 3b0aef995. By 19:15Z origin/refactor was at 6096b051b, after rows 66 and 243
landed. `git diff 3b0aef995 6096b051b` touches neither `packages/bus/src/host-run.mjs` nor
`packages/bus/tests/host-run.test.mjs`, so there's no rebase (sage-2084). Row 243 did change
`packages/bus/tests/process.test.mjs`. My slot's bus suites run that file as it was at 3b0aef995, and the
gate rerun on Sage's slot runs it as landed.
## Prediction for part A on e5e4b2e5a (written before the slot)
The hook starts `ssh-add -t 1` at the given fraction of a monotonic second. From the brief's probe, the
agent stores the key about 3 ms later. The first list reaches the agent a few ms after addKey resolves:
Node's exit event, the mark, and `ssh-add -l`'s own start. A red needs the add to land before the second
boundary and the list to land after it.
- 0.990: green. The add and the list both land before the boundary.
- 0.993, 0.995, 0.996, 0.997: red expected. The add lands before the boundary, the list after. At 0.993
and 0.997 I'm less sure, because the list (at 0.993) or the add (at 0.997) can land right at the
boundary. A green at either is in line with the cause, and so is a red.
- 0.998: green. The add lands after the boundary, so the key gets almost a full second.
Every red should show "first list got" an answer with no SHA256 line. It should also show addKey
resolving before a second boundary and the first list starting after it or within a few ms of it, with
the agent alive and its socket present.
Under sage-2084's rule, on e5e4b2e5a only a red at 0.990 or 0.998 counts toward the stop, and two such
reds stop the run. On the fix commit any red counts, and two stop it. In parts B to D, the first red stops
the run.
## Slot plan
All scripts are in `/mnt/storage/scratch/darkwing-251-out/`, launched from `/`, each behind the busy
checks and chained with `&&`, TMPDIR under `/mnt/storage/scratch/tmp`.
1. Confirmation. `align-hooks.mjs` is a load hook, outside the repo, that serves `host-run.mjs` with
`addKey` wrapped to start `ssh-add` at a chosen fraction of a monotonic second. T27 runs unchanged
under it, alone (`--test-name-pattern`). It runs at fractions 0.990, 0.993, 0.995, 0.996, 0.997 and
0.998, three times each:
- on e5e4b2e5a (step 1, `-t 1`). Expected: some first lists fail, and the step 1 message shows the
add's resolve and the list on either side of a second boundary, with the agent alive and the
socket present. These reds are the point of the run.
- on the fix commit (`-t 2`). Expected: all 18 pass.
2. `host-run.test.mjs` whole, once, on the fix commit.
3. The bus suite whole, three times in a row.
4. The bus suite whole in two clean worktrees at the same commit, started together.
## Slot results
Sage's CLEAR came in sage-2154 at 20:58:55Z. `slot-251.sh` ran parts A and B from 20:59:22Z to 21:01:33Z,
on node v26.8.1 and OpenSSH_10.5p1, with TMPDIR `/mnt/storage/scratch/tmp/r251`. At the start, BOOTTIME
minus MONOTONIC read 0.000001 s, then 0.000000 s twice (dewey-0769 N1). So the hrtime fractions below
put the agent's second boundary in the right place.
### Part A: T27 alone under `align-hooks.mjs`, 3 runs per fraction
The fraction is when the hook started `ssh-add`. The hook logged each start within 0.0001 of its target.
| fraction | e5e4b2e5a (`-t 1`) | 68f2dfd4b (`-t 2`) |
|---|---|---|
| 0.990 | 3 pass | 3 pass |
| 0.993 | 3 pass | 3 pass |
| 0.995 | 3 red | 3 pass |
| 0.996 | 3 red | 3 pass |
| 0.997 | 3 pass | 3 pass |
| 0.998 | 3 pass | 3 pass |
e5e4b2e5a went red 6 times out of 18, every time at a fraction the prediction named, so none of them
counted toward the stop. 68f2dfd4b passed all 18. The fix runs used 68f2dfd4b, whose test blob is the same
as c19b4167e's (280132c7).
Every red failed on the first list and got `"The agent has no identities.\n"`, with the agent alive and
its socket present. The step 1 message gave these hrtime fractions. "addKey start" is the mark before the
hook's wait, so its fraction is earlier than the ssh-add start.
| run | addKey resolved | first list mark |
|---|---|---|
| 0.995 rep 1 | 0.9992 (+317.6 ms) | 0.9993 |
| 0.995 rep 2 | 0.9991 (+846.5 ms) | 0.9991 |
| 0.995 rep 3 | 0.9988 (+845.0 ms) | 0.9988 |
| 0.996 rep 1 | 0.9999 (+839.8 ms) | 1.0000 |
| 0.996 rep 2 | 0.0004 (+845.0 ms) | 0.0004 |
| 0.996 rep 3 | 0.0002 (+835.9 ms) | 0.0002 |
At 0.995 the add finished about 4 ms in, before the boundary, and the list mark came just before it. The
`ssh-add -l` process then reached the agent after the boundary. At 0.996 the agent stored the key before
the boundary, and addKey resolved just after it. In both cases the key's whole second ended between the
add and the list, which is the cause step 2 gives. The two edge fractions came out green on the first
build. At 0.993 the list still reached the agent before the boundary, and at 0.997 the add landed after
it, so the key got almost a full second. The prediction allowed either result at those two.
### Part B: `host-run.test.mjs` whole on 68f2dfd4b
24 tests, 24 passed (21:01:18Z, 14.0 s).
### The stop before C1, and the resume
At 21:01:33Z the script stopped before C1, with exit 4 and no red. My own `busy.sh` matched two `sleep 60`
processes, mine, in `darkwing-r251/packages/bus`. Part B left them. They come from T27 "the start sweep
keeps a live bus dir and kills only a matching live process" (`host-run.test.mjs:528` and `:536`). Its
`t.after` SIGKILLs the `/bin/sh -c 'sleep 60; :'` parent, but the `sleep` child lives on for up to 60 s.
That code is outside this row's diff, so the row leaves it alone, and it's recorded below as a follow-up.
Sage confirmed (sage-2159) that the right move is to resume only the scope that hadn't run.
`slot-251-cd.sh` runs C and D as `slot-251.sh` has them, with the same two busy checks. Before each check,
it waits up to 75 s for those orphans to exit: my uid, my trees, a command line of exactly `sleep 60`.
It logs them as it waits. Any other process still trips the checks.
### Parts C and D: the bus suite whole on 68f2dfd4b
| run | start | tests | pass | fail | time |
|---|---|---|---|---|---|
| C1 | 21:02:48Z | 598 | 598 | 0 | 21.6 s |
| C2 | 21:04:06Z | 598 | 598 | 0 | 21.4 s |
| C3 | 21:05:26Z | 598 | 598 | 0 | 22.1 s |
| D1, darkwing-r251 | 21:06:44Z | 598 | 598 | 0 | 24.2 s |
| D2, darkwing-r251b, TMPDIR `r251-b` | 21:06:44Z | 598 | 598 | 0 | 24.2 s |
The script waited 33 s before C2, 34 s before C3, 33 s before D and 32 s before the sweep. Each time it
was waiting for that run's `sleep 60` orphans (two per suite, four after D), and nothing else. After the
wait, the sweep of my trees, the out dir and TMPDIR found 0 processes left (21:08:04Z). Sage confirmed
(sage-2159, sage-2160) that the short gitea-api and `node --check` runs they made during the slot weren't
stops. None of my busy checks refused on any of them.
These runs used the bus files as they were at 3b0aef995. Since then, origin/refactor (666173292, then
7247d80c7 and c3d88840c with row 196) has changed `broker.mjs`, `runtime.mjs` and `server.mjs`, which T27's
file imports, along with other bus files. None of those commits touch this row's two paths or
`host-run.mjs`, and `git merge-tree` of c3d88840c with 68f2dfd4b is clean. The gate rerun in
a clean worktree on Sage's slot runs T27 against them.
### Follow-up, not in this row
T27's start-sweep test leaves the `sleep 60` child of each planted shell running for up to 60 s after
the test. Killing the process group (`process.kill(-pid)`; both shells are spawned `detached`) or
`exec sleep 60` in the shell would end the child along with its parent.
-538
View File
@@ -1,538 +0,0 @@
# Row 210 (#424, Gitea #1707): agent host access with sudo through the credential store
Gizmoduck, 2026-10-20, r3. Design only, no code. Reviewer Duckworth, gate owner Sage. Due 2026-10-27 (Q184).
- Brief: `docs/plans/2026-10-20_host-credentials.md` at 9db578ff5.
- Base read: `origin/refactor` 9ba31b5b3. Row 199 read as DESIGN.md r6 (4d3007ec9) and its code and
BUILD.md at addee664a on `megavolt/row199`.
- PRD: REQ-CRED-3 in Honker's 1.7 draft (`agents/honker/work/queue-194/PRD-1.7-DRAFT.md:111-115`, 11cb0423b).
Jason approved PRD 1.7 (d52febb5 yes at 20:36:47Z, mos-1089), so REQ-CRED-3 is in.
- Scope (sage-1468): this row ships the `host.run` broker op, the grant check, the executor and the receipts,
all testable over `broker.sock`. The `host_run` harness tool is out, as 199 left its publish tool out
(199 BUILD.md deviation 12). Sage files one follow-up row for the agent-facing harness tools, publish and
`host_run`, in `packages/harness`. Duckworth's r2 N5 (the tool help says a gated call leaves stdin under
4 KiB) belongs to that row.
- Branch `gizmoduck/row210`. Row claimed in-progress (rev 2361) after Sage cleared the after (rev 2359). It
lands only after 199, rebased onto 199's landing (Q177).
Rounds:
- r3 (duckworth-0043, REVIEW-r2.md at 52b5fee63, approved with C1, and sage-1468, sage-1472): C1 Q-J0's revoke
line now matches section 6, the connection is cut and a started command may keep running. Q-S3 answered,
the harness tool is out (scope line above, D10). N1 the runner list takes Duckworth's whole list, and D6
says the per-call decision is the control. N2 numeric shorthand, unspecified and mapped addresses and
`*.localhost` refuse (D3, T1). N3 the pids file records each pid's start time (D7, T27). N4 the mutant
table is in order.
- r2 (sage-1450): no `"any"` grant. The command boundary is an argv allowlist per host grant, and load refuses
a wildcard (D6, T26, M6, M28). PRD 1.7 is frozen, so `host.sudo` stays a hard wait and Jason decides each
call. Section 5 now holds the proposed 1.8 routing requirement. D5 is confirmed against 199's deviation 11.
The seat network gap is a known limit with what closes it (section 6). Cites now point at 199's code.
- r2 (duckworth-0042, REVIEW-r1.md at 8140b7f39): B1 events name host, user and argv in clear (D5 step 6,
T22, M30). B2 a gated call's question shows stdin in full (D4, T9, M31). B3 the key and processes die with
the bus, and bus start sweeps leftovers (D7, T27, M32 to M34). B4 Q-J0 for Jason in section 10, worded to
stand alone (mos-1088). Notes taken: N1 (E12, runGit's kill shape), N2 (loopback refused, own-host limit),
N3 (process groups), N4 (stdin can make a prefix root), N5 (`-l` and a separate address), N6 (D8 framing),
N7 (section 7).
## 1. What the code does today
| # | Fact | Cite |
|---|---|---|
| E1 | The bus reads credentials for two kinds only, `gitea` and `vikunja` | `bus/src/credentials.mjs:22`, `business/src/vocabulary.mjs:103` |
| E2 | A credential value must match `^[-A-Za-z0-9._~]+\n?$`, at least 16 and at most 16384 bytes. An OpenSSH private key fails it at the first `-----BEGIN` space | `bus/src/credentials.mjs:84-87` |
| E3 | `assertClean` searches `JSON.stringify(result)` for each token. JSON escapes a newline, so a multi-line secret is never found: `JSON.stringify("a\nb").includes("a\nb")` is `false` (run on node here) | `bus/src/credentials.mjs:95-103` |
| E4 | `use()` turns any exception in the operation into `service-failed`, then runs `assertClean` | `bus/src/credentials.mjs:104-117` |
| E5 | Host to bus-child IPC is serial, one request at a time, with a 10 s timeout. A slow reply breaks the channel | `cli/src/host.mjs:31`, `:270` |
| E6 | The web verbs run in the bus child over `broker.sock`. `requestWeb` checks keys, runs `#secretCheck`, checks the grant `roles[s.role].web`, records `action.allowed` (requested), calls `onAdmit()` so the server raises the socket timeout, runs the service and records completion | `bus/src/broker.mjs:1492-1530`, `bus/src/server.mjs:87-95` |
| E7 | `roles[role].web` comes from the resolved tool limit. A role entry in the bus holds `authority`, `web` and `credentials` | `bus/src/business.mjs:17-20` |
| E8 | A business-file role entry takes `definition`, `holder`, `vars`, `tracker`, `credentials`. Anything else refuses at load | `business/src/business.mjs:89` |
| E9 | The chief's `organization.set` takes only `ORGANIZATION_KEYS`. The human-only `instances.set` takes `INSTANCE_KEYS` and refuses any definition that declares a credential (`organization-credentials-out-of-scope`). Holder, credentials and tracker stay hand edits | `bus/src/broker.mjs:145-147`, `:1248-1298`, `business/src/instance-settings.mjs:1-5`, `:22-31` |
| E10 | An action outside ROUTED is a hard wait and classifies `gated` for every role. No role file may grant one | `bus/src/broker.mjs:44-70`, `business/src/vocabulary.mjs:58-88`, PRD REQ-WAIT-2 and 3 |
| E11 | The seat sandbox does not confine the network, abstract Unix sockets or the host UID | `seat/src/sandbox.mjs:29-31` |
| E12 | On refactor the bus child spawns nothing. With 199 it spawns git: `publishRemote` (`bus/src/runtime.mjs:245` at addee664a) runs `runGit`, a detached group leader that gets SIGTERM at the limit, then SIGKILL to the group after a grace period | `seat/src/checkout.mjs:95-138` at addee664a |
| E13 | Role networks are `none`, `api-only`, `open`. The bundle carries the value, and the sandbox's own comment says the network is not confined (E11) | `business/src/vocabulary.mjs:101`, `harness/src/bundle.mjs:252` |
Row 199 gives, in code at addee664a: `useFor` (`bus/src/credentials.mjs:228`), `issue` (`:156`), `revoke`
(`:197`), `remove` (`:211`), the kind list (`:49`), the token pattern (`:19`, `:143`), the store dir's file
regex `^(gitea|vikunja)(\.tmp)?$` (`:222`) and `withGitea` (`bus/src/runtime.mjs:233`). From its design: the store at `<configDir>/secrets/agents/<business>/<agent>/<kind>` (D1), the `{store: true}`
reference (D2), the read path (D3, D3a), `useFor(session, kind, op)` with the call-site allowlist test (D5),
the `publish` grant shape, a list on the role entry plus a hard-wait action whose decision binds the commit
(D9), and the human-only lifecycle verbs with `changes.jsonl` receipts (section 6).
## 2. Probe (throwaway keys, scratch sshd on 127.0.0.1:22410, OpenSSH 10.5p1)
Run in `/mnt/storage/scratch/gizmo-210-probe` with keys made for the probe and deleted after. No real key or
host was touched.
| Probe | Result |
|---|---|
| `ssh-add -` with the private key on stdin, to an agent on a private socket | works, "Identity added: (stdin)" |
| `IdentityAgent=<sock>` with `IdentitiesOnly=yes` and `IdentityFile=none` | refused, `Permission denied (publickey)`. With `IdentitiesOnly`, ssh offers only agent keys that match a configured identity |
| same with `IdentityFile=<the public key file>` | works, rc 0 |
| `-i /dev/fd/3` with the key on fd 3 | fails, "Identity file /dev/fd/3 not accessible". ssh closes inherited fds, so the fd route is out |
| a known_hosts file pinning the wrong key under `HostKeyAlias` with `StrictHostKeyChecking=yes` | refused, "Host key verification failed", rc 255 |
| `printf '%s|'` with `'a b'` and `'it'\''s'` single-quoted, plus an unquoted `$((1+1))` as a control | prints `a b|it's|2|`. The quoted elements arrive whole, and only the unquoted control expanded |
| remote `exit 7` | ssh exits 7 |
| killing the agent | its socket file is gone |
## 3. Decisions
### D1. The kind and its naming
One key per (agent, host). The kind name is `host.<hostId>`, so the store file is
`<configDir>/secrets/agents/<business>/<agent>/host.<hostId>` (199 D1, unchanged). `hostId` matches
`^[a-z0-9][a-z0-9-]{0,31}$`.
The closed kind set becomes `gitea`, `vikunja` and the family `host.<hostId>`. One exported helper,
`credentialKind(name)` in `vocabulary.mjs`, answers whether a name is a kind, and each place that checks
kinds calls it: `SERVICES` users (`role.mjs:105-118`), 199's `bus/src/credentials.mjs:49` and the store
file regex at `:222`, the broker's service
check, the CLI `issue` date flag and 199's remove regex. A build test greps for literal `'gitea', 'vikunja'`
lists outside `vocabulary.mjs` and fails on a new one.
Why per host and not per agent. A key that leaks, or a host that is retired, is revoked for that host alone.
ops-01 installs one public key per host, and an `authorized_keys` line names one agent. The cost is more keys
for Jason to issue, one per grant.
A role definition declares the kind once as `{"service": "host"}` with no scopes. The business file binds the
concrete hosts (D3). A definition can't name a host, because hosts belong to one stack instance.
The date is `--rotate-by DATE`, as for gitea. An ssh key doesn't expire on its own, so `status()` reports
`rotation-due` or `valid`, the gitea branch.
### D2. The value check
`checkValue(kind, text)` replaces the one regex at `credentials.mjs:84`. For `gitea` and `vikunja` it is
today's check, unchanged. For `host.*` the value must be:
- PEM framed by `-----BEGIN OPENSSH PRIVATE KEY-----` and `-----END OPENSSH PRIVATE KEY-----`, LF line ends,
at most 16384 bytes (the existing cap).
- An `openssh-key-v1` blob with cipher `none`, kdf `none`, exactly one key, type `ssh-ed25519`. Parsed in
node with `Buffer`, no ssh-keygen call, so the check runs at bus start without a process.
- The two check integers equal, and the public key in the private section equals the one in the header.
Anything else refuses `credential-format`. An encrypted key refuses, because the bus has no way to ask for a
passphrase and `BatchMode=yes` would fail at use time instead of at load. The parsed public key is kept with
the entry so `status` and D7 can print its fingerprint without touching the private part.
### D3. The grant
Four layers. Each must hold, and the code checks all four on every call.
1. **Role definition, reviewed commit.** `roles/<def>.json` lists `{"service": "host"}` in `credentials`. That
is the "reviewed role change" in the brief. Once a PRD version routes `host.sudo` (section 5), the same
reviewed commit puts `host.sudo` in that definition's `authority.withinRole`.
2. **Host registry, business file, hand edit.** A new top-level key:
```
"hosts": { "<hostId>": { "address": "10.1.1.53", "port": 22, "user": "localadmin",
"hostKeys": ["ssh-ed25519 AAAA..."] } }
```
`address` is an IPv4 or IPv6 literal or a DNS name matching `^[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?$`.
An address of this machine refuses at load (r2 N2, r3 N2). For an IP literal, which `net.isIP` decides,
that is `127.0.0.0/8`, `0.0.0.0/8`, `::1`, `::`, and an IPv4-mapped `::ffff:` form of either IPv4 range.
For a name, it is `localhost` and any name ending in `.localhost`. A name made only of digits and dots
refuses unless `net.isIP` reads it as IPv4, so the shorthand `127.1` and the integer `2130706433`, which
pass the DNS pattern and which the resolver reads as 127.0.0.1, refuse. A label starting `0x` refuses for
the same reason. `port` is 1 to 65535. `user` matches `^[a-z_][a-z0-9_-]{0,31}$`. `hostKeys` is a non-empty list of
`ssh-ed25519`, `ecdsa-sha2-nistp256` or `ssh-rsa` public key lines, type and base64 only, no comment.
3. **Agent grant, business file, hand edit.** A new role-entry key beside 199's `publish`:
```
"hosts": { "<hostId>": { "commands": [["systemctl", "status"], ["journalctl", "-u"]] } }
```
Each `hostId` must be in the registry, and the role's definition must declare `host`. A role entry with
`hosts` whose definition lacks `host` refuses at load and names `roles.<agent>.hosts`. `commands` is the
command boundary (D6).
4. **The key.** `credentials["host.<hostId>"] = {"store": true, "rotateBy": DATE}` with the file present
(199 D2, D3a). A grant without an issued key refuses `credential-unavailable` at call time, as 199 does.
Who writes each layer is in section 4. Neither the chief nor any agent can reach layers 2 to 4: `hosts` is
outside `ORGANIZATION_KEYS` and `INSTANCE_KEYS` (E9), `instances.set` already refuses a definition that
declares credentials (E9), and 199's `issue` is human-only.
### D4. The action and the decision binding
`host.sudo` is a new action in ACTIONS and in neither ROUTED list. Until a PRD version routes it, it is a hard
wait and classifies `gated` for every role (E10). Each call then needs a decision the human resolved. PRD 1.7
is frozen (sage-1450, decision d52febb5 cites its sha256), so under 1.7 Jason decides every `host.run`.
The decision binds one call, the way 199 D9 binds `pushCommit`. When the action is `host.sudo`, the
`decision.raise` event body gets `hostRun = {host, argvSha256, stdinSha256}`, from the raise's own args. The
question shown to the human ends with the host id, the user, the argv with each element JSON-quoted, and the
stdin in full as one JSON string, so Jason sees all of what runs (B2). The broker appends that text itself, and
the agent can't shape it. The whole question keeps the 4096-character cap (`broker.mjs:126`, `:1654`), so on a
gated call stdin is as large as the cap allows and no larger. A raise that doesn't fit refuses
`invalid-request`. `argv ["bash"]` can't be granted at all (D6), and `["tee", "/etc/x"]` with a stdin Jason
can't see is ruled out by showing it. `#authorityFor` refuses `decision-mismatch` when the call's host, argv hash or stdin hash differs, as
`holderRun` does at `broker.mjs:796`. A decision is spent once (`decision-consumed`).
After routing, a definition that lists `host.sudo` within-role runs without a decision, and `decision` is
optional for that class only. The grant layers in D3 still apply.
### D5. The executor: a `broker.sock` verb in the bus child
```
host.run {"host", "argv": [..], "stdin"?, "timeout"?, "decision"?}
-> {"exit", "signal", "stdout", "stderr", "truncated": {"stdout", "stderr"}, "ms"} or a refusal
```
It runs in the bus child, shaped like `requestWeb` (E6), not as a host IPC op. A sudo command can take
minutes and the host channel times out at 10 s and serves one request at a time (E5). The key is in the bus
child's memory already (199 D3), so no ticket has to cross to the CLI host. Row 199's publish needs a ticket
because git and the working repo live in the CLI host. ssh needs neither. 199's BUILD.md deviation 11
(addee664a) keeps credential use in the bus child too: the push runs there under `withGitea`, and the host
socket only checks and polls. Sage confirmed D5 matches that shape (sage-1450). `host.run` needs no host
socket and no poll, because the agent calls `broker.sock` directly. The helper is `withHost(session, host,
fn)`, beside `withGitea`, calling `useFor(session, "host." + host, fn)`.
Order of checks. Each refusal goes through `broker.refuse()` and logs `action.refused`.
1. `keys(args)`, then `#secretCheck(args)`, then identity from the cap.
2. `host` is in the registry (`host-unknown`).
3. `host` is in `roles[s.role].hosts` (`host-not-granted`). This is the REQ-CRED-3 check "a grant exists for
that agent and that host".
4. `argv` shape (D6) and `commands` match (`command-not-granted`).
5. `#checkAuthority(s, "host.sudo", {decision, target: host, context})` (D4).
6. Record `action.allowed` phase `requested` with host, address, user and argv in clear (B1), and stdin as
size and sha256. Every arg already passed `#secretCheck` in step 1, as the web verbs log their url
(`broker.mjs:1521`). After routing, the event log is the record of what ran as root. Then `onAdmit()`, which
sets the socket timeout to the call's timeout plus 10 s, as the web branch does (`server.mjs:95`).
7. `credentials.useFor(s, "host." + host, run)`, where `run` is D7.
8. D8's leak check on the output, then `#secretCheck` on the result.
9. Record phase `completed` with host, argv, exit, signal, ms, and size and sha256 of stdout and stderr. The
output itself never goes into an event. A failure records `host-failed` with the same fields.
Only one `host.run` per (agent, host) runs at a time. A second refuses `host-busy`. That keeps a stuck call
from piling up ssh processes and makes revoke (D9) simple.
Spawns reuse `runGit`'s shape (E12): an argv array, never a shell, `detached: true` so each process leads its
own group, SIGTERM to the group at the limit and SIGKILL after a grace period. If the helper can be shared, it
is, rather than a second copy. `env` is `{PATH: "/usr/bin:/bin"}` only, plus `SSH_AUTH_SOCK` for `ssh-add`.
Each spawn is wrapped in `setpriv --pdeathsig KILL --`, the prefix the seat sandbox already uses
(`seat/src/sandbox.mjs:44`), so it dies when the bus child dies (B3).
### D6. The command boundary
`argv` is an array of 1 to 256 strings. Each is 1 to 4096 bytes of UTF-8 with no NUL, and the total is at most
32768 bytes. `argv[0]` must not contain `/` unless it starts with `/`. `stdin` is optional, a string of at most
65536 bytes, and goes through `#secretCheck` with the args.
The remote command is built by the bus, never taken as a string:
```
sudo -n -- 'argv0' 'argv1' ...
```
Each element is POSIX single-quoted: wrap in `'`, and replace each `'` with `'\''`. The probe shows the
remote shell gets each element back exactly (section 2). `sudo -n` fails instead of prompting, so a host
without NOPASSWD returns sudo's error, not a hang. The host's login shell for `user` must be POSIX. That is a
host precondition for ops-01, listed in section 7.
`commands` in the grant is an argv allowlist and nothing else. It is a non-empty list of argv prefixes. An
argv passes when its first N elements equal some prefix of length N, compared element by element as literal
strings, with no globbing. `[["systemctl", "status"]]` admits `systemctl status nginx` and refuses
`systemctl restart nginx`.
There is no wildcard grant (sage-1450). Business-file load refuses, naming `roles.<agent>.hosts.<id>.commands`:
- any value that isn't a list, including the strings `"any"` and `"*"`,
- an empty list, an empty prefix, a non-string element, or a duplicate prefix,
- an element containing `*`, `?` or `[`, so nobody writes a glob that the matcher would read literally,
- a prefix whose first element, by basename, is a shell, a command runner or an interpreter. Shells: `sh`,
`bash`, `dash`, `zsh`, `ksh`, `fish`, `busybox`. Runners: `su`, `sudo`, `doas`, `pkexec`, `runuser`, `env`,
`xargs`, `nohup`, `setsid`, `timeout`, `nice`, `systemd-run`, `chroot`, `nsenter`, `unshare`, `setpriv`,
`capsh`, `flock`, `stdbuf`, `ionice`, `chrt`, `taskset`, `strace`, `script`, `watch`. Interpreters:
`python3`, `perl`, `ruby`, `node`, `awk` (Sage's ruling on duckworth-0043 N1). Each would turn the prefix
back into "any". T26 has one case per name.
That list can't be complete, and it isn't the control. Under 1.7 the control is Jason's decision on each
call, bound to host, argv and stdin (D4, Q-S1). The list only refuses the obvious cases at load, early. Many commands can still start a shell or an editor as root (`systemctl edit`,
`find -exec`, interpreters). The allowlist limits mistakes and narrows what a grant means, but it is not a
sandbox. Stdin goes to the command, so some prefixes are root on their own: `[["tee"]]` or `[["dd"]]` writes
any file as root from stdin (N4). Section 6 says what remains.
Output: stdout and stderr each capped at 1 MiB. Past the cap the bus keeps reading and drops the rest so the
remote side doesn't block, and sets `truncated`. `timeout` defaults to 60 s, maximum 900 s. At the timeout
the bus sends SIGTERM to the ssh process group, then SIGKILL after 5 s, and answers `signal`. ssh's own exit
code 255 and a remote command exiting 255 can't be told apart. The result returns stderr so the agent can see
which, and the doc says so.
### D7. Handing the key to ssh
Per call, inside `useFor`'s operation:
1. `mkdtemp` a dir under `<configDir>/secrets/run/` with mode 0700. `<configDir>` is never bound into a sandbox
(199 D3). The socket path is checked against the 107-byte Unix socket limit before use, refusing
`host-failed` if over.
2. Write two files in it, mode 0600: `known_hosts` with one line per pinned key, `mosaic-host-<hostId> <type>
<base64>`, and `id.pub` with the entry's public key (D2).
3. Write `<dir>/pids` with the bus child's pid. Each line in it is a pid and that process's start time,
field 22 of `/proc/<pid>/stat`, so a reused pid never matches (duckworth-0043 N3). Start `ssh-agent -D -a <dir>/a.sock` as its own group leader,
append its pid, and wait for the socket. Run `ssh-add -t <timeout + 30> -` with `SSH_AUTH_SOCK` set and the
private key written to its stdin. The key never touches disk, argv or env. `-t` makes ssh-agent drop the key
at the call's timeout plus 30 s even if nothing else runs (B3). ssh is the second group leader, and its pid
is appended too. Timeout, revoke and the `finally` kill both groups, ssh-agent's and ssh's (N3).
4. Run ssh with exactly these options and nothing from any config file:
```
ssh -F /dev/null -T -p <port>
-o BatchMode=yes -o IdentitiesOnly=yes -o IdentityFile=<dir>/id.pub -o IdentityAgent=<dir>/a.sock
-o StrictHostKeyChecking=yes -o UserKnownHostsFile=<dir>/known_hosts -o GlobalKnownHostsFile=/dev/null
-o HostKeyAlias=mosaic-host-<hostId> -o UpdateHostKeys=no -o CheckHostIP=no -o VerifyHostKeyDNS=no
-o ForwardAgent=no -o ForwardX11=no -o ClearAllForwardings=yes -o PermitLocalCommand=no
-o ProxyCommand=none -o ProxyJump=none -o ControlMaster=no -o ControlPath=none
-o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3
-l <user> -- <address> <remote command from D6>
```
The user goes in `-l` and the address is its own argument, so nothing depends on how ssh splits `@` or on
an IPv6 literal (N5). `-F /dev/null` stops `~/.ssh/config` from adding a ProxyCommand, a different key or forwarding. The pin is
`StrictHostKeyChecking=yes` against a file the bus wrote from the registry, so there is no trust on first
use: an unknown or changed host key refuses with rc 255 (probe, section 2) and the bus answers
`host-key-mismatch` when stderr has "Host key verification failed".
5. In a `finally`: kill both groups, remove the dir. A test asserts the dir is gone after success, refusal,
timeout and an exception thrown inside the operation.
6. If the bus child is killed, the `finally` never runs. `--pdeathsig` kills ssh-agent and ssh with it, and
`-t` drops the key anyway. At bus start, before `broker.sock` listens, the bus sweeps
`<configDir>/secrets/run/`. A recorded pid counts as alive only when it exists and its start time matches.
For each dir whose bus pid is no longer alive, it sends SIGKILL to each recorded pid that is alive and
whose `/proc/<pid>/cmdline` names that dir, then removes the dir. A dir whose bus pid is alive is left
alone. Any other entry there refuses start, as 199 refuses a stray file
in the store (`credentials.mjs:222`). Revoke after a restart has nothing in flight to reach.
The 0600 temp key file route the brief allows is not used. The agent route keeps the private key off disk,
and the fd route doesn't work (probe).
### D8. The leak check
This is defense in depth (N6). The remote never holds the private key and forwarding is off, so output can
carry the key only if the agent already had it. `assertClean` misses a multi-line key (E3). A `host.*` entry
in the shared token map makes its loop compare against a value that never matches, which is harmless. For a `host.*` entry, `useFor` checks the result with
`assertCleanKey(result, entry)`, which searches the JSON text for:
- each base64 body line of the PEM of 24 characters or more,
- the whole body with line breaks removed,
- the 32-byte ed25519 private seed as lowercase hex and as base64 at each of the three byte offsets.
A match refuses `credential-leak`. The output is dropped, and the event records only sizes and hashes. The
existing `assertClean` still runs for the token kinds. The same function runs on `stdin` before the call, so
an agent can't round-trip a key it somehow has back through the host.
### D9. Lifecycle
199's verbs carry over with `KIND = host.<hostId>`. Two additions:
- `issue AGENT host.<id>` checks D2, then prints the public key line and a suggested `authorized_keys` line
for ops-01: `from="<stack host address>",restrict <type> <base64> mosaic-<business>-<agent>-<id>`. It
prints only public material.
- `revoke` also kills a `host.run` in flight for that agent and host (process group SIGKILL) before it drops
the key. The call answers `credential-unavailable`. Remote processes that the killed ssh started may keep
running (section 6).
`status` shows each host entry with its fingerprint (SHA256 of the public key), the rotate date and state.
The tester gets `status` and `revoke` only, as in 199.
### D10. Where the files change
| File | Change |
|---|---|
| `business/src/vocabulary.mjs` | `host.sudo` in ACTIONS, `credentialKind`, host id pattern |
| `business/src/business.mjs` | top-level `hosts` registry, role-entry `hosts`, load checks from D3 |
| `business/src/role.mjs` | `{"service": "host"}` in `checkCredentials` with no scopes |
| `business/src/credentials.mjs` | `host.*` refs take `rotateBy` |
| `bus/src/credentials.mjs` | kind check via `credentialKind`, `checkValue` (D2), `assertCleanKey` (D8), busy and kill map |
| `bus/src/host-run.mjs` (new) | D6 argv and quoting, D7 ssh-agent and ssh spawn, cleanup |
| `bus/src/broker.mjs` | `host.run` verb (D5), `host.sudo` in `#authorityFor` with `hostRun` (D4) |
| `bus/src/server.mjs` | `host.run` branch beside the web branch with `onAdmit` |
| `bus/src/business.mjs` | carry `hosts` onto the bus role entry |
| `cli` | 199's `credentials issue` accepts `host.<id>`, prints D9's lines |
No `packages/harness` file changes. The `host_run` tool is Sage's follow-up row (sage-1468).
Row 199's own kind and regex are not changed inside row 199. This row rebases onto 199's landing and changes
them here.
## 4. Who grants (brief item, "yours to propose with reasons")
Proposal: **Jason, at every layer that widens access. No agent, chief included, and not the tester.**
| Layer | Who | How | Why |
|---|---|---|---|
| Definition declares `host` (and later lists `host.sudo`) | Jason | reviewed commit to `roles/*.json`, as for any role authority (AGENTS.md invariant 7) | It decides which kinds of agent may ever hold a host key. Review puts a second seat on it |
| Host registry and pins | Jason, with ops-01's key fingerprints | hand edit of the business file | The pin is the host identity. A wrong pin is a man-in-the-middle on root. It must come from the host side, not from a first connection |
| Agent grant `roles.<agent>.hosts` | Jason | hand edit of the business file | Q183: "not all agents will need to have that access". The per-agent, per-host choice is his |
| Key `issue` | Jason | 199's human-only `issue` | The key passes through the caller. The tester is an agent (199 section 6, duckworth N1) |
| `authorized_keys` on the host | ops-01 | host side, from the line `issue` prints | ops-01 owns the hosts (brief) |
| Revoke | Jason, Mos, or the tester | `revoke` | Taking access away should be quick and needs no new trust |
Why not a reviewed commit for the agent grant too. The business file lives in `<configDir>`, outside the
repository (AGENTS.md invariant 2), and per-instance grants already stay hand edits there (E9, 199 D9 step 2).
Moving grants into the repo would put host addresses and the per-instance roster under review, which is a
bigger change than this row. The definition layer is the reviewed one.
Why not the chief. A chief with this power could hire an agent and grant it root. 199 already keeps hires
uncredentialed (199 D13). `instances.set` refuses credentialed definitions today (E9), and this row keeps it
that way.
REQ-CRED-3 says the who-grants answer is set by this design and approved by Jason. The table above is that
answer.
## 5. The routing gap and a proposed 1.8 requirement
PRD 1.7 routes `role.hire`, `role.fire` and `git.push.remote`, not `host.sudo`, and 1.7 is frozen (sage-1450).
This row builds for 1.7 as it stands. `host.sudo` is a hard wait under REQ-WAIT-3, and Jason decides each
`host.run`, bound to host, argv and stdin (D4). That is safe, and slower than Q183's "allow agents to perform
their jobs". The routing goes in 1.8. Sage takes the line below to Mos for Jason. I don't edit the PRD.
Proposed for PRD 1.8, beside REQ-CRED-3:
> **REQ-CRED-4.** The action `host.sudo` is routed. A role whose definition lists it within-role may run a
> command with sudo on a host without a decision for each call, but only when all of these hold: the
> definition declares the host credential kind, the business file grants that agent that host, the command
> matches the grant's argv allowlist, and the agent's key for that host is issued. A grant names exact
> argv prefixes. A wildcard grant is refused. A role without `host.sudo` within-role still needs a decision
> for each call. Jason adds `host.sudo` to a definition only through a reviewed role change.
Routing is then one reviewed commit: `host.sudo` into both ROUTED lists and into the chosen definitions'
`withinRole`, as 199 does for `git.push.remote`.
The brief also says this row lands only after the PRD version holding REQ-CRED-3 is approved. Jason approved
1.7 (d52febb5 yes at 20:36:47Z, mos-1089), so that holds. 1.7 doesn't route `host.sudo`, so Q-S1 stands.
## 6. Limits, said plainly
- **Known limit: the seat network isn't confined, so root can outlive the grant.** `seat/src/sandbox.mjs:29-31`
says the sandbox doesn't confine the network (E11). An agent whose allowlist admits any command that can
write a file as root, or start a shell, can add its own key to `authorized_keys` on the host. It can then
ssh straight from its sandbox, around the bus, the events and revoke. An `authorized_keys` `from=` option
doesn't help, because the sandbox shares the stack host's address. The allowlist (D6) narrows this but
doesn't close it.
What closes it: a seat sandbox with its own network namespace (`bwrap --unshare-net`), where the only way
out is the bus sockets and the model proxy, so role network `none` and `api-only` are enforced and not just
carried in the bundle (E13). With that in place, a planted key is useless from the sandbox. Until then,
ops-01's host-side audit (sudo log, `authorized_keys` changes) is the check after the fact. Sage is
reporting the gap to Mos as its own follow-up (sage-1450). It is not this row.
- **The stack's own machine.** A registry entry for the machine the stack runs on, or any machine holding the
stack's `<configDir>`, gives an agent with a root-writing command every agent's keys and tokens, not only
root there (N2). Load refuses loopback addresses, but it can't know every address of the stack's machine.
Jason doesn't register it.
- **Revoke stops the stack's path, not remote processes.** Killing ssh may leave a remote process running.
`-T` gives no tty, so there is no SIGHUP from a closing terminal.
- **Same OS user.** Anything running as the stack's OS user outside a sandbox can read the key file, as 199 D6
says for every kind.
- **Exit 255** is ssh's failure code and also a remote exit code (D6).
- **The pin trusts the registry.** Whoever writes the business file controls which host key is trusted. That
is Jason (section 4).
## 7. Host preconditions (ops-01, not this row)
- `user` has NOPASSWD sudo and a POSIX login shell. `sudo` has no `requiretty` for that user.
- The agent's key is in `authorized_keys` with `from="<stack host>",restrict`.
- The host's public keys are handed to Jason for the registry.
- Duckworth's #423 r5 condition (duckworth-0035) applies. If ops-01 changes #423 step 1 to fit this design,
for example per-agent `from=...,restrict` lines for localadmin, that change goes to Duckworth as a diff
before it runs. That is the live part, not this row (N7).
## 8. Tests
All tests use a scratch sshd on 127.0.0.1 started by the test, with keys made by the test, and a fake `sudo`
on the test user's PATH that prints its argv as JSON and runs nothing as root. The sshd config sets
`AuthorizedKeysFile` and `HostKey` in the test's `mkdtemp` dir. A helper skips the ssh tests with a visible
reason only when `/usr/bin/sshd` is missing, and the gate fails if any of them skipped. No test reaches
another host.
| # | Test | Refusal or result |
|---|---|---|
| T1 | business load: `hosts` registry field checks, bad address, each own-machine form from D3 (`127.0.0.2`, `0.0.0.0`, `::1`, `::`, `::ffff:127.0.0.1`, `::ffff:0.0.0.0`, `localhost`, `a.localhost`, `127.1`, `2130706433`, `0x7f.1`), with `10.1.1.53` and `host.example` as controls, port, user, empty `hostKeys`, a key line with a comment | load refuses, names the key path |
| T2 | role entry `hosts` naming an unknown host, or on a definition without `host` | load refuses `roles.<agent>.hosts` |
| T3 | `organization.set` and `instances.set` with `hosts` in args | `invalid-request`, and the file is unchanged |
| T4 | value check: a valid unencrypted ed25519 key loads. An encrypted key, an RSA key, two keys, CRLF, a gitea token in a `host.*` slot, and a PEM in a `gitea` slot each refuse | `credential-format` |
| T5 | `host.run` with an unknown host | `host-unknown` |
| T6 | `host.run` on a host not in the caller's grant, including a host granted to another agent | `host-not-granted` |
| T7 | argv shape: empty, a NUL, too long, too many, a relative path with `/`, non-string | `invalid-request` |
| T8 | allowlist: a matching argv runs, a longer argv runs, a shorter one and a differing element refuse | `command-not-granted` |
| T9 | before routing: no decision refuses, a decision for another argv, another host or another stdin refuses, a spent one refuses. The stored question carries host, user, argv and the full stdin, and a raise whose question would pass 4096 characters refuses | `gated`, `decision-mismatch`, `decision-consumed`, `invalid-request` |
| T10 | quoting: argv with spaces, quotes, `$()`, backticks, `;`, newline and glob characters reaches fake sudo unchanged | fake sudo's JSON equals argv |
| T11 | the remote command starts `sudo -n --` | fake sudo sees `-n` and `--` first |
| T12 | exit code and signal pass through, timeout kills the process group and answers `signal` | result fields |
| T13 | output cap: 2 MiB of stdout returns 1 MiB and `truncated.stdout: true`, and the call doesn't hang | result fields |
| T14 | host key pin: the sshd presents a key not in `hostKeys` | `host-key-mismatch`, and no command ran (fake sudo's marker file absent) |
| T15 | a registry with the right key passes on a fresh dir with no `~/.ssh/known_hosts` in HOME. A HOME with a `~/.ssh/config` setting a ProxyCommand doesn't change the call | runs, and the proxy marker is absent |
| T16 | leak: the fake sudo prints the private key file (multi-line), and in a second case prints only one body line, and in a third the seed in hex | `credential-leak`, the event has no output |
| T17 | `stdin` containing a body line of the key | `credential-leak` before any spawn |
| T18 | cleanup: after success, refusal, timeout and a thrown error, the run dir is gone and no `ssh-agent` or `ssh` with that dir in argv survives | dir absent, pgrep empty |
| T19 | the private key never appears in the argv or env of any spawned process (read from `/proc/<pid>/cmdline` and `environ` while the fake sudo sleeps) | absent |
| T20 | second concurrent call for the same agent and host | `host-busy` |
| T21 | revoke during a sleeping call kills it, and the next call refuses | `credential-unavailable` |
| T22 | events: `action.allowed` requested and completed carry host, address, user and argv in clear, and sizes and hashes of stdin, stdout and stderr. Never the output, the stdin or the key | event bodies |
| T23 | `issue` of a `host.*` kind prints the public line and the `authorized_keys` suggestion and never the private key | stdout and stderr checked |
| T24 | build grep: no literal `'gitea', 'vikunja'` kind list outside `vocabulary.mjs`, and 199's `credentials.use(` allowlist unchanged | grep |
| T25 | no role file in `roles/` lists `host.sudo` while it is a hard wait (REQ-WAIT-2) | grep |
| T27 | SIGKILL the bus child while fake sudo sleeps: the ssh and ssh-agent pids are gone within 2 s. A call with `timeout` 1 leaves an agent whose key is gone after 31 s (`ssh-add -l` on its socket, agent kept alive by the test). The next bus start removes the run dir, kills a planted live process whose cmdline names it, leaves a dir whose bus pid is alive, and leaves a live process whose pid matches but whose start time doesn't | pids, `ssh-add -l`, dir state |
| T26 | load refuses `commands` as `"any"`, `"*"`, `{}`, `[]`, `[[]]`, a non-string element, a duplicate prefix, an element with `*`, `?` or `[`, and a prefix starting with each listed shell or runner, `/bin/bash` included | load refuses, names `roles.<agent>.hosts.<id>.commands` |
## 9. Mutants
One per refusal, plus the two the brief names. Each must be killed.
| # | Mutant | Killed by |
|---|---|---|
| M1 | registry check skipped | T5 |
| M2 | agent grant check skipped | T6 |
| M3 | grant check reads any agent's `hosts` | T6 |
| M4 | each argv shape check removed, one mutant each (5) | T7 |
| M5 | prefix match compares only `argv[0]` | T8 |
| M6 | load accepts a string `commands` (wildcard grant) | T26 |
| M7 | `#checkAuthority` call removed | T9 |
| M8 | `hostRun` argv hash not compared | T9 |
| M9 | `hostRun` stdin hash not compared | T9 |
| M10 | quoting drops the `'\''` replacement | T10 |
| M11 | `sudo -n` loses `-n` | T11 |
| M12 | **host key pin skipped**: `StrictHostKeyChecking=accept-new` | T14 |
| M13 | **pin skipped another way**: `UserKnownHostsFile` set to the user's default | T14, T15 |
| M14 | `-F /dev/null` removed | T15 |
| M15 | **key leaks into output**: `assertCleanKey` not called | T16 |
| M16 | leak check uses only the joined body | T16 (one-line case) |
| M17 | stdin leak check removed | T17 |
| M18 | `finally` cleanup removed | T18 |
| M19 | key passed with `-i` from a temp file left on disk | T18, T19 |
| M20 | output cap removed | T13 |
| M21 | timeout kill removed | T12 |
| M22 | busy check removed | T20 |
| M23 | revoke doesn't kill the in-flight call | T21 |
| M24 | event records stdout | T22 |
| M25 | each value check removed, one mutant each (encrypted, type, count, CRLF) | T4 |
| M26 | `organization.set` accepts `hosts` | T3 |
| M27 | load check for a definition without `host` removed | T2 |
| M28 | shell and runner check removed, and a second mutant compares `argv[0]` without taking the basename | T26 |
| M29 | glob character check removed | T26 |
| M30 | `requested` event drops argv | T22 |
| M31 | the question omits stdin | T9 |
| M32 | `--pdeathsig` dropped from the spawns | T27 |
| M33 | `ssh-add` without `-t` | T27 |
| M34 | start sweep removed, a second mutant sweeps without checking the bus pid, and a third ignores the start time | T27 |
| M35 | loopback check removed | T1 |
The mutant script follows row 201's shape: copy the packages to scratch, anchor each mutant once, run each
test file in its own process group with a timeout, control first, exit 1 on any survivor. It runs on Sage's
slot.
## 10. Questions
- Q-S1, answered (sage-1450): 1.7 is frozen. Hard wait under 1.7, and section 5 holds the 1.8 proposal.
- Q-S1 still holds after 1.7's approval, which doesn't route `host.sudo` (sage-1468).
- Q-S2, answered (sage-1450): 199's deviation 11 keeps credential use in the bus child, so D5 stays.
- **Q-J0 (Jason, through Mos, before landing, B4, mos-1088).** Word for word:
> Q-J0 (#424). Who may give an agent sudo on another machine?
> Proposal: only you. No agent can, and that includes the chief of staff and the tester.
> You make a grant in four steps, all yours. A reviewed change to the agent's role allows machine keys at all.
> You list the machine and its fingerprint. You give that agent that machine with an exact list of allowed
> commands. You issue the key, and ops-01 installs it. Until a later PRD says otherwise, you also approve
> each command before it runs. You, Mos or the tester can revoke at once. The stack drops the key and cuts
> its connection. A command already started on the machine may keep running. A grant of "any" command is
> root on that machine, so the design refuses it. Some listed commands, such as a file writer, still reach
> root, so grant a machine only to an agent you'd trust as root there.
> Do you approve: only you grant machine access, as above? Yes or no. I recommend yes.
Mos records the answer in DECISIONS.md in jason-master-restart.
- **Q-J1 (Jason, through Mos, at the live window):** the first grants, each an argv allowlist per agent and
host. Not needed for the build.
- Q-S3, answered (sage-1468): the `host_run` harness tool is out of this row. Sage files a follow-up row for
publish and `host_run` in `packages/harness` (scope line at the top).
+310
View File
@@ -0,0 +1,310 @@
# Row 234 (#436a row a, Gitea #1740): host enrollment, store records and bus schema 3i to 3j
Gizmoduck, 2026-10-21. Reviewer Duckworth, gate owner Sage. Lands by 10-25, before the .53 and .54 installs
(mos-1134).
- Brief: `docs/plans/2026-10-20_host-credentials.md`, section "Machine grant row a: enrollment, store records
and migration 3i-3j", blob 64451be5 (queue rev 2617).
- Design: `agents/gizmoduck/work/machine-grant/DESIGN.md` r6, f9039682e on `gizmoduck/machine-grant-design`,
approved by Duckworth (duckworth-0064, REVIEW-r6.md at 8ea792256). Not reopened here. Cites "r6:" are lines
of that file.
- Grant event rule: #436b section 7, fa5658941 on `megavolt/row199`,
`agents/megavolt/work/436b/DESIGN.md:431-490`, file sha256
96510b83778e79a71b33d0d5f6fd0c806dfe74f6de315ff4a9f1fbe5620f6c69, approved by Duckworth (duckworth-0067).
- Base: origin/refactor 8fede5fc6, with rows 199 (cb5fdb064) and 210 (f5753c0e8) landed. Branch
`gizmoduck/row234`. Code cites are `path:line` at 8fede5fc6 unless they say otherwise.
## Names: r6 against section 7
r6's `grant.*` and `host.enroll.*` names are `body.action` values on `action.allowed` events, not event
kinds. The `events` kind CHECK isn't rebuilt. Four r6 names move under `grant.`:
| r6 | This row |
|---|---|
| `host.enroll.step` | `grant.host.enroll.step` |
| `host.enrolled` | `grant.host.enrolled` |
| `host.checked` | `grant.host.checked` |
| `host.removed` | `grant.host.removed` |
Only the consume record carries `decision`: `{operation: 'grant.host-enroll', decision}`. Every other
guarded body names the decision `grantDecision`, so r6's `decision` field in step, enrolled and failed bodies
is `grantDecision` here. The actor of every guarded event is `actor_role` NULL with `actor_run` 'grant',
and `body.by` names the caller the broker checked.
## Split (r6 section 12 Q-S4)
Row a builds:
1. **Schema 3i to 3j.** `migrations/3i-3j.sql` adds `hosts` and `host_grants` (r6 S1, primary keys
(business, host) and (business, agent, host)) and the guard trigger: an `action.allowed` whose
`body.action` or `body.operation` GLOBs `'grant.*'` aborts unless the actor is (NULL, 'grant').
`SCHEMA_VERSION` 3j, a migration test with a step that fails partway.
2. **Grant event writer.** A private session-less broker writer for guarded events. `#event` doesn't change.
`recordEvent` refuses `reserved-event` for the guarded namespace.
3. **The `grant` raise frame.** `decision.raise` takes `grant` only with `credential.mint`, only from the chief
(`grant-chief-only`), and in row a only with `method: 'host-enroll'`. Any other method refuses
`invalid-request` until row b adds `host`. E1's checks, the `ssh-keyscan` set compare before the raise
transaction, and the rendered block.
4. **Enrollment executor.** `#consumeGrant`, the start on the human's cli yes (X1), E2 steps 1 to 4, the
installer key at `<configDir>/secrets/hosts/<business>/<hostId>/installer`, the failure cleanup, and the
start reconcile for enrollments: the `registry` step undo, and a resolved-yes unspent enrollment ending
`grant.failed interrupted` (X3).
5. **`hosts.check` and `hosts.remove`** (E4), with `mosaic hosts status|check|remove`.
6. **`tools/host/`**: `mosaic-keys`, `mosaic-keys-write`, `mosaic-keys.sudoers`, `mosaic-keys.sshd.conf` (E3).
7. **`host.run` on the store** (X4 first and third bullets): it reads the `hosts` and `host_grants` records
and the Approve record check, and at start the runtime gives `Credentials` a reference per `host_grants`
record. The loader refuses `hosts` at the top level and in a role (S1). Row 210's tests move to store
fixtures.
8. **Reader filter** (duckworth-0067 N1): every grant-evidence reader in this row (status, the start
reconcile, `openGrantDecisions`) filters `kind = 'action.allowed' AND actor_role IS NULL AND actor_run =
'grant'`, with a test that an `action.refused` look-alike and a session-written one are ignored.
`hosts.remove`'s `hostLineLeft` reader is one of them (duckworth-0079 M4).
Row b keeps the `host` method's raise binding (R4), the grant executor (X2), the grant rows of X3, X5,
`Credentials.register`/`claim`/`abortClaim`, revoke (section 6) and `sweep`. So row a has no way to make a
`host_grants` record outside a test fixture, and from its landing to row b's no agent holds a host grant
(r6 N9, the run sheet strips row 210's keys).
Tests in row a (r6 section 10): G1 and G5 for the `host-enroll` raise, G6 and G10 for the enrollment
executor, G11's enrollment cases, G14, G15, G16, G17, G18, G20, G21 (its `host-line-left` case reads an
event fixture until row b writes `hostLineLeft`), G25, the migration test, the trigger cases, the reader
filter test, and row 210's T-tests on store fixtures. Row b: G2 to G4, G7 to G9, G11's grant cases, G12,
G13, G19, G22 to G24, G26 to G28.
## Early check items (duckworth-0079, REVIEW-build-split.md 10672772e on `duckworth/reviews`)
Moves into row a:
- M1. G4 for the `host-enroll` block (r6:240-242): a question can't forge the block, and the 4096-character
limit holds with the block appended. G4 for the `host` block stays in row b.
- M2. The tag function (r6:265-266), because E2 step 2 tags the installer line. G24's first sentence moves
here: names of 64 characters with `.` and `_` give distinct tags of the fixed length. The `sweep` half
stays in row b.
- M3. A store-fixture test that `host.run` refuses `host-not-granted` for no record, an unresolved decision,
a decision resolved by an arbiter, the tester or an agent or not via cli, a consume record with operation
`grant.host-enroll` in place of `grant.host`, and an unspent decision (X4 first bullet, r6:593-596). A
restart test that a `host_grants` fixture gives `Credentials` a reference (X4 third bullet).
G21's `host-line-left` case:
- F1. The fixture goes through this row's private grant writer in the real shape: `action.allowed`, actor
(NULL, 'grant'), `body.action` 'grant.failed', `body.host`, `body.hostLineLeft`. No raw SQL around the
trigger.
- F2. `hosts.remove` reads `body.hostLineLeft` from any guarded body, not only `grant.failed`.
- F3. Decided: a guarded body that carries `hostLineLeft` also carries `host`. Row b adds `host` to
`grant.revoke.closed`, so its body is `{tag, host, hostLineLeft}` (r6:716 has `{tag, hostLineLeft}`).
The reader matches on `body.host`, and doesn't map tags back to hosts through `grant.step`. A direct
field is one equality in SQL, and a tag lookup would add a second reader that the N1 filter also has to
cover.
- F4. Row b carry: "no later `sweep` cleared it" (r6:403) needs `grant.sweep`, which only row b writes. Row
b adds the sweep clear to `hosts.remove` with a G21 case, and its G8 or G13 shows `hosts.remove`
refusing after a real `hostLineLeft` write, not only after the fixture. Until then row a's check is "any
guarded `hostLineLeft` for this host refuses".
## Mutants (r6:838-883, numbered here)
Row a, killed on this row's tree:
| # | Mutant |
|---|---|
| A1 | drop the chief check |
| A2 | start the executor on a `credential.mint` with no `grant` |
| A3 | start it on an arbiter resolve |
| A3b | start it on a `cli` yes by someone other than the human (duckworth-0083 N8) |
| A4 | start it on `no` |
| A5 | skip the keyscan compare |
| A6 | compare only one key |
| A7 | write the private key into the event body |
| A8 | drop the "not yet enrolled" check in E2 step 1 |
| A9 | drop the `hosts` primary key |
| A10 | skip the start check for a resolved, unspent enrollment decision |
| A11 | `host.run` reads the business file instead of the grant record |
| A12 | build the tag without the business |
| A13 | drop `from=` in the writer |
| A14 | let the writer touch a line outside the tag pattern |
| A15 | drop `O_NOFOLLOW` on the user file |
| A16 | drop one group from the writer's refused list |
| A17 | drop the user pattern check before the path use |
| A18 | treat a `privileged` entry as enough without `users` |
| A19 | let the writer accept a sudo user not in `privileged` |
| A20 | trigger: drop the `body.operation` arm, so the consume record passes from a session |
| A21 | trigger: accept actor_role NULL with any actor_run |
| A22 | a grant reader drops the `kind = 'action.allowed'` or actor filter. The runner splits it: A22a drops the kind, and G21's `action.refused` look-alikes kill it. A22b drops the actor, which the trigger can't stand in for, because 3i-3j adds the trigger without checking old rows. Its killer is the grant-migration test "A22b, sage-1703": a session-written `grant.host.removed` with `hostLineLeft` and a `grant.host-enroll` row from 3i, migrated to 3j, where `hosts.remove` still removes the host and `reconcileGrants` ends nothing |
| A23 | `host.run` skips the Approve record check (any `host_grants` record is enough) |
Row b keeps the rest of r6's list: the `ready` check, the three hash compares, the definition check, the
install and record order, `remove` on a verify failure, both start sweeps, the reconcile's remote `remove`,
the `grant.step` before install, revoke leaving the record, `register` before `issue`, the abort check and
the `await` before `issue`, revoke and an unreferenced claim, `sweep` and any `mosaic-` line, revoke's host
step order, claim before consume, the resolved unspent grant decision, an async `commit`, `hosts.revoke`
through `credential-verbs.mjs`, the revoke marker reconcile, the upsert, the undo past `grant.revoked`, the
abort after step 5 and a missing `bus.tester` as dev.
## Broker functions this row touches (sage-1677)
For row 236 (Megavolt), which rebuilds on this landing. Old lines are `packages/bus/src/broker.mjs` at
8fede5fc6, new lines are at the last commit that changed this file (they moved by 10 after `runEnrollment`
went in). The planned list is kept in this file's history (827b89ea6).
Changed:
- `constructor` (:279): one more option, `hostGrant`, the object `host-grant.mjs` builds.
- `recordEvent` (old :1243-1251, new :1606): one more refusal. An `action.allowed` whose `body.action` or
`body.operation` is a `grant.*` name refuses `reserved-event`.
- `request()` (old :1362, new :1726): two routes ahead of the sync path, a `decision.raise` with `grant` to
`#raiseGrant` and the `hosts.*` verbs to `#requestHosts`. Both are async.
- `requestHost` (old :1712-1795, new :2079): the business-file lookups (old :1730-1736) become one
`#hostAccess` call, in the same transaction as `#agent`. `commands` and `entry` come from it.
- `#dispatch` (old :1796, new :2160) gets a fifth parameter, `enroll`, which only `#raiseGrant` passes.
- `decision.raise` (new :2244): `grant` joins the allowed keys, and `grant` without `enroll` (or the
reverse) refuses `invalid-request`. The `host.sudo` block reads `#hostAccess` in place of `b.hosts`
and the role's `hosts`, and an undefined `target` still refuses `host-unknown`. `validArgv` and
`validStdin` now run after the access check, so a malformed argv for an ungranted host refuses
`host-not-granted` first. The enrollment block is appended to the question before `string(question)`,
so the 4096 cap counts it (M1). The decision's raise body carries `grant: enroll`.
- `decision.resolve` (new :2409): when `op` is `resolved`, `#approvedGrant` reads the decision, and a
`host-enroll` grant starts `#runEnrollment` from `setImmediate`, after the transaction commits. The
result adds `grant: 'started'`.
New, all private but two:
- `#grantEvent` (:657), the writer of guarded events. It runs the secret check, then `writeGrantEvent`.
- `#approvedGrant` (:667), `#consumeGrant` (:678). `#consumeGrant` sits after `#approvedGrant`, not next
to `#consumeAuthority` as planned.
- `#hostRecord` (:685), `#hostAccess` (:692), `#grantNotice` (:708), `#enrollFailed` (:736),
`#runEnrollment` (:750), `#openGrantDecisions` (:823).
- `runEnrollment` (:745), public, a trusted entry to `#runEnrollment` for G10. No verb reaches it.
- `reconcileGrants` (:833), public, which `runtime.mjs` calls once at start.
- `#raiseGrant` (:866), `#hostsCaller` (:886), `#requestHosts` (:900), `#hostLinesLeft` (:964).
Module level, new (:184-230): `GRANT_RUN`, `grantName`, `GRANT_EVIDENCE` (the N1 filter as SQL),
`registrySha256`, `writeGrantEvent` (exported, the one writer of guarded events, which test fixtures use
for F1), `HOSTS_VERBS`, `fingerprintOf`, `INSTALLER_COMMENT`.
Unchanged: `#event`, `#spent`, `#consumeAuthority`.
Other `src` files:
- `bus/src/host-grant.mjs`, new: the keyscan, the installer key pair, and `hosts.check`'s `list`.
- `bus/src/runtime.mjs`: builds `hostGrant`, calls `reconcileGrants` before the server starts, and adds
the `host_grants` references before `new Credentials` (X4 third bullet).
- `bus/src/host-run.mjs`: `runHost` takes `command`, sent as is in place of the sudo argv, for the
installer's `list`.
- `bus/src/server.mjs`: the `hosts.*` verbs and a raise with `grant` go down the async path.
- `bus/src/store.mjs`: the 3i-3j step, `SCHEMA_VERSION` 3j. `bus/src/credentials.mjs` exports
`privateDir`.
- `business/src/business.mjs` and `business/src/hosts.mjs`: the loader refuses `hosts` (S1).
`checkHosts` and `checkHostGrants` are gone.
- `cli/src/hosts.mjs`, new, routed from `cli.mjs` and `scripts/mosaic`.
## Decisions taken in the build
- **`#hostAccess` binds the agent and the host.** X4 says the Approve record check passes when the
decision is an approved `host` grant with a `grant.host` consume. The check also requires the grant's
`agent` to equal the session's role and its `host` to equal the requested host. Without that, an
approved grant for one agent and host would vouch for any `host_grants` row that names its decision.
Cost: none for row b, whose executor writes the row from the same grant.
- **Store fixtures are written the real way.** `bus/tests/host-fixtures.mjs` inserts the decision rows by
SQL, but every guarded event goes through `writeGrantEvent`, so the trigger sees it (F1). An enrollment
fixture writes `grant.host.enrolled`, or the start reconcile reads it as interrupted.
- **The resolver test for host kinds is gone.** `business/tests/hosts.test.mjs` lost its D5 case, because
the loader now refuses `host.<id>` credential refs. The `isHostKind` filter in `business/src/resolve.mjs`
can't be reached now. I left it in, since row b may want it back, and it's one line.
- **Store before Credentials.** The runtime already opened the store before `Credentials`. The
`host_grants` references depend on that order, and the comment there says so.
- **CLI.** `mosaic hosts` prints tab lines, and `--json` prints the bus rows. Usage errors exit 4 and bus
refusals exit 2, as `mosaic credentials` does. There is no enroll command: enrollment is the chief's raise.
- **Host tools tests live in the bus suite.** `bus/tests/host-tools.test.mjs` loads the writer by path
through a Python harness under `python3 -B -I`. The harness passes its own key dir, config dir, owner
uid and user, group and sudo lookups, and runs `mosaic-keys` from a copy with stub `sudo` and `logger`.
The gate runs the bus suite, so G16 and G25 run there. The real `pwd`, `grp` and `sudo -l` paths are
checked on the #433 VM, not here. The test also runs `visudo -c` on the sudoers file and `sshd -T` on
the drop-in.
- **Writer details the design left open.** The `from` file must hold exactly one value matching
`[0-9A-Za-z.:/*?!_-]+(,[0-9A-Za-z.:/*?!_-]+)*`, so it can't close the quote. `users`, `privileged` and `from` must be
regular, owned by the owner uid and not group or world writable, and are opened without following a
symlink. Lines starting with `#` are comments. A refusal exits 1 with its code on stderr and in syslog.
`add` of a tag that is already there exits 0 and writes nothing.
- **`runEnrollment` is public for G10 (duckworth-0081 B1).** G10 needs a second start for a spent
decision, and the resolve path can't give one: a second resolve refuses `decision-closed`
(broker.mjs:2416) before the executor runs. So
`#runEnrollment` returns its code (`enrolled`, `host-enrolled` or the refusal), and a public
`runEnrollment` calls it, as `reconcileGrants` is public. The test checks a spent
decision gives `decision-consumed`, and an open or agent-resolved one gives `not-authorized`, with
nothing written. That second case also reaches the `via` and `by` checks in `#approvedGrant`, so A3 is
no longer vacuous (N1).
Who can call it (sage-1692): only code holding the `Broker` object, which is the bus process and the
tests. The one `new Broker` in `src` is `runtime.mjs:126`, and no `src` file calls `runEnrollment`;
the resolve path calls the private `#runEnrollment` from `setImmediate` (broker.mjs:2445). A seat
holds no `Broker`. It reaches the bus only over `broker.sock`, where `server.mjs` calls a fixed set of
methods by name (`request` at :134 and :140, `identity`, `disconnect`). `request()` routes on the verb
through the `#dispatch` switch, and neither file looks a method up by name (no `this[` in either), so no
verb string can reach `runEnrollment`.
- **A `sudo -l` that times out refuses `user-sudo` (N2).** `_sudo` returns an empty answer on
`TimeoutExpired`, which isn't the expected line, so `main` logs the refusal and exits 1.
- **Keyscan comment (N3).** A scan that can't start refuses `host-failed`. One that runs and finds no key
returns no lines, and the raise refuses `host-key-mismatch`. The comment in `host-grant.mjs` now says so.
- **No tag on in-run `grant.failed` (N4).** r6:578 tags only the `interrupted` one, so the code follows it.
- **Run sheet (N5).** The writer refuses `config-privileged` when the file is missing, so each host needs an
empty, root-owned `/etc/mosaic-keys/privileged` (0644) before the first enrollment.
## Mutant results (Sage's slot, 2026-10-21)
All 25 killed: A1 to A23, with A3b added for duckworth-0083 N8 and A22 split into A22a and A22b. The
runner is `mutants.py` outside the repo. Each mutant gets its own session, a 420 s limit with a group kill,
and a byte restore checked against git status. None hung. Run 1 (04:24Z, on 95f966b1c) killed A1 to A22a
and stopped at A22b, which survived. Run 2 (04:43Z, on 097892741) killed A22b and A23.
- Each kill comes from a named assertion in the mutant's own test files, except A9. Dropping the `hosts`
primary key leaves `host_grants`' foreign key with no parent key, so the store refuses to open
("foreign key mismatch") and 28 of 29 tests fail at setup. That's a structural kill, not the S1 test.
The schema can't exist without the key (sage-1709).
- A22b drops the actor filter from `GRANT_EVIDENCE`. The trigger can't stand in for it, because a 3i store
can hold a session-written grant row that the 3i-3j step keeps. The grant-migration test "A22b,
sage-1703" kills it (see the A22 row above).
Follow-up, not in row a: G18's control calls `runHost` directly, so nothing runs `hg.check` end to end
with a good pin (duckworth-0083 N9). The #433 VM run covers it.
## Status at 097892741
Done: every Split item, 1 to 8, duckworth-0081 B1 to B3, duckworth-0083 N8, and all 25 mutants killed.
Single files in the slot, all exit 0, 178 pass and 0 fail by an awk sum over 23 logs: bus grant-enroll
24, host-run 24, grant-migration 6, host-tools 6, account-migration 3, chief-of-staff 18,
comment-migration 5, hard-waits 13, name-migration 1, project-runtime 3, spec-migration 1,
thread-migration 3, usage-migration 1, project-files 8, spec-socket 6, spec-review 12, land 17, spec 4;
harness spec-tools 2, raise-domain 2; business hosts 4; cli hosts 5; seat sandbox 10.
Left: Sage runs the whole bus, business and cli suites and every `scripts/test-*.sh` at the landing gate,
after Duckworth approves.
## Round 3 (sage-1742): two tests that count the schema
The landing gate failed `packages/bus/tests/schema.test.mjs:522`. It counts the tables and triggers that
`schema.sql` creates, and row a adds two tables and a trigger, so the counts go from 16 to 18 tables and
from 70 to 71 triggers. My singles didn't run it because I took it for a test of other rows' tables.
I grepped `packages/*/tests/*` for `sqlite_master` and `schema.sql`, with `schema.test.mjs:517` as the
positive control. That turned up one more test that would fail: T46 in
`packages/cli/tests/stack-release.test.mjs:910`. It puts every schema table in exactly one of four lists, and
hosts and host_grants were in none. I'd left the stack-release tests out of the r2 singles on the claim that
they don't load the bus, but T46 reads `schema.sql`. Both tables go in its untimed list (Sage's ruling,
sage-1745: the message_threads pattern). Neither has an `at` column. Row a writes hosts at five lines,
and each runs in one store transaction with a `grant.*` events row, which the restore guard counts.
`#grantEvent` (broker.mjs:657) writes that row through `writeGrantEvent` (:201, INSERT INTO events at
:212).
- :737 DELETE, in `#enrollFailed`, with grant.failed at :738. Both callers wrap it in a transaction,
:816 and :849.
- :763 INSERT, in the step 1 transaction (:754), with grant.host.enroll.step at :761.
- :783 UPDATE installer_fingerprint, in the step 4 transaction (:780), with grant.host.enrolled at :784.
- :932 DELETE, in the hosts.remove transaction (:927), with grant.host.removed at :933.
- :950 UPDATE state ready, in the hosts.check transaction (:944), with grant.host.checked at :951.
Row a writes no host_grants rows. It only reads them (broker.mjs:695, :930, runtime.mjs:103). Row b adds
the writes, and each must share a transaction with a `grant.*` event the same way. That goes on row
235's carry list. The other hits pin an older schema
(account, comment, name, spec, thread and usage migrations, chief-of-staff 3c) or count only their own
tables (state.test.mjs:106 reads every table but pins no list, threads.test.mjs:129).
hosts and host_grants are mutable by design, so the refused-labels loop above `schema.test.mjs:517` gets no
entry for them. hosts.check updates a host to ready. hosts.remove and a failed enrollment delete the
record. Row b updates and deletes grant records. Neither table has append-only triggers. The one new
trigger, events_grant_actor, guards events. grant-migration.test.mjs tests it, and it killed mutants
A20 and A21.
+598
View File
@@ -0,0 +1,598 @@
# Row 235 build: machine grant row b (#436, Gitea #1741)
Brief: `docs/plans/2026-10-20_host-credentials.md`, "Machine grant row b: raise binding, executor,
reconcile and revoke". Design r6 f9039682e. Built on row 234's landing c771a36f4 (sage-1775), branch
gizmoduck/row235-b. Line numbers below were read on b0320d395, whose src files are byte-identical to the landing's.
## Squash onto 4ced5e63d (sage-1812, sage-1831)
Steps 1-6 were six local commits on c771a36f4 (gizmoduck/row235-b, tip f9fff4300). Sage named
4ced5e63d as the base, so this branch, gizmoduck/row235-c, holds one commit on it: the diff
c771a36f4..f9fff4300 applied with `git apply --index`. N10 is in that commit. N12 needs no change,
because row b doesn't touch schema.sql.
Rows 240, 228 and 238 landed between c771a36f4 and 4ced5e63d and changed 18 paths. One of them is
also mine:
- `packages/cli/README.md`. Row 228 (86249ac55) changed the session credential paragraphs at about
lines 432 and 706. My hunk adds three lines on `hosts revoke` and `hosts sweep` at about line 1052.
The two don't overlap, so the blob here is row 228's blob plus my three lines, with no landed line
reverted. cli tests/host.test.mjs:855 reads this README, and run 1's cli suite covers it.
The other 12 paths are byte-identical to f9fff4300. The line numbers in the step notes below were read
on the old base.
## Run 1 (sage-1831, sage-1833, sage-1838)
Run 1 on 4d4bb4af6, 07:45-07:54Z, failed: 1351 tests, 1285 pass, 66 fail. The 66 are 33 tests counted
twice, once in the files run and once in the bus suite. The cli suite, the cli hosts test, seat
sandbox.test and bus project-runtime.test passed. Two defects, both mine:
- grant-executor.test.mjs, 32 tests. setup() never made `dir/data`, and `new Store` lstats its root
(store.mjs:137), so every test stopped at setup with ENOENT. setup() now makes it, mode 0700.
- credential-claim.test.mjs:76 (X4), 1 test. A register after a revoke left the key in `#revoked`, so
status() said `revoked` for a new grant's reference that has no token yet. register now clears
`#revoked`, as unregister does. `#revoked` feeds only status(), and issue clears it as well.
Both fixes are amended into the one commit. The rerun covers the five bus files, the bus suite, and
cli credentials.test.mjs and verbs.test.mjs, which read credential state strings.
## Run 1 rerun (sage-1842, sage-1843)
The rerun on ac7c45975, 08:06-08:07Z, failed: 701 tests, 695 pass, 6 fail, which are 3 grant-executor
tests counted in both the files run and the bus suite. All three were test defects that run 1's setup
failure had hidden. No src file changed for them:
- G10. The two fixture decisions were built after `before`, and grantDecision (host-fixtures.mjs)
INSERTs one events row each, so the count read 13 for 11. The test now builds them first, asserts
they added exactly 2 rows, and then pins the executor's 0 against that count.
- G27. setup's t.after removes the scratch dir before a hook the test adds later runs, so the test's
chmod hook hit ENOENT after the body had passed. The body now restores the mode in a try/finally.
- G22. The slow run's promise had no handler until `refusesWith`, four lines on. The revoke rejected it
first, and node failed the test on the unhandled rejection. The handler now goes on at creation.
The fix is one more amend of the single commit. The rerun is grant-executor.test.mjs whole plus the bus
suite.
Rerun 1c on 5df4b52c5, 08:15Z, was green: 623 of 623, grant-executor 34 of 34 and the bus suite 589 of 589.
## Mutants B1-B37 (sage-1894, sage-1899, sage-1904)
Three chunks ran on 5df4b52c5, 09:22-09:37Z. Restores matched HEAD after each. 33 were killed and four
survived. Two of the misses were mine, in the file lists:
- B2 and B23 listed only grant-executor, and that file never makes the raise either mutant changes.
grant-enroll.test.mjs:227 (A2, A4) resolves a plain `credential.mint` yes and a host grant no, and
asserts no grant events for either. mutants.py now lists grant-enroll and grant-executor for both.
B2 then ran alone and A2 killed it. B23 runs again with this round's tests.
The other two survivors needed tests, and Sage ruled on all four (sage-1904):
- B16 drops the abort check after `verify`. The check after `step('commit')` still ends the grant
revoked, so G23 X5's asserts held. The run then logs a commit step for a grant that never committed.
G23 X5 now asserts that its only `grant.step` is `install`.
- B17 adds `await null` after the commit check. Sage accepted it as equivalent. `await null` yields
only to microtasks. Every revoke starts in a macrotask of its own (a socket request, or a test's
call), so none can land in that gap. There is no test for it.
- B27 makes issue's commit callback async. issue() doesn't await it, so the executor returns `applied`
before the INSERT runs. The two differ only when the commit transaction throws, and a real store can
throw there with a full disk, an I/O error or a busy timeout. Under the mutant that throw becomes an
unhandled rejection after the key file is written, and the catch's undo never runs. The new G8 test
makes the store's `run` throw SQLITE_FULL once on the host_grants INSERT. It asserts host-failed, no
record, key file, reference or line, and the steps install, commit, failed. The store object comes
from the test's setup, so src gains no seam.
These two test changes went in as a new commit on gizmoduck/row235-d (8b13d0ca0). That branch then
moved onto row 213's landing as gizmoduck/row235-e, below. B16, B23 and B27 run there, after the touched
test files run whole.
## Rebase onto 62ddc96cc (sage-1922)
Row 213 landed as 361ba2387, and refactor's tip is 62ddc96cc, with only docs past it. gizmoduck/row235-e
starts at 62ddc96cc and cherry-picks 8b13d0ca0's one commit. The patch-id is unchanged (c062b73fa9f1).
row235-c and row235-d stay as they were.
Between 4ced5e63d and 62ddc96cc, 361ba2387 is the only commit that touches a file of mine. The one
drifted path is `packages/bus/src/broker.mjs`:
- 213 adds `CREATIVE_DEFINITIONS` to the vocabulary import at :6, and refuses `decision.raise` from a
creative role with `decision-not-allowed` at :2176. Both hunks stay as landed.
- My 31 removed broker lines read the same before and after the cherry-pick, so no landed line is
reverted.
`packages/cli/README.md` isn't in 361ba2387. Every other path of mine is byte-identical to 8b13d0ca0.
Line numbers in the section below were read on the row235-e working tree; earlier sections keep theirs.
## Creative roles get no host grant (mos-1163, sage-1905, sage-1908, sage-1916)
Brief blob b16e236188f6 (4364dddb0, rev 2784) requires two refusals: the writer, at the raise and at the
step 1 recheck, and host.run at spend time, with the record kept. Webby's N2 on row 213 r4 asks whether
a creative role can spend a decision another role raised.
The spend answer. Mos confirmed it read-only (mos-1164, sage-1916) on 8b13d0ca0 and 01e1fe512; the
row235-e line follows each:
- Only the chief raises a host grant. `#raiseGrant` refuses `grant-chief-only` for any other
definition: :1232 on 8b13d0ca0, :870 on 01e1fe512, :1238 here. The hosts verbs check the same at
:1307 on 8b13d0ca0, :896 on 01e1fe512, :1313 here. `chief-of-staff` isn't in `CREATIVE_DEFINITIONS`.
- No agent spends one. `#consumeGrant` (:697) is called for a host grant only from `#runHostGrant`
(:902) and from the restart reconcile (:1106), which ends it interrupted. `#runHostGrant` starts from
the resolve hook (:2881) only when `#approvedGrant` returns a grant, and that needs a close by the
business's human, via cli, with the raise's approval choice (:691). An agent's resolve records via
`broker`, so it starts nothing (grant-enroll A3). The public `runHostGrant` (:891) is a trusted API
that no verb reaches.
So a creative role can't raise, approve or spend a host grant. The gap was a different one: the chief
could raise a grant whose `agent` is a creative role, and the human could approve it. The only check on
that agent was `#hostDefinition`, which refuses `host-not-declared` (:864) unless the definition's
`credentials` names `host`. Row 213's `checkCreative` (business/src/role.mjs:143) limits a creative
role's tools, authority and messaging, not its `credentials`, so a creative role file naming `host`
passed both the raise and the step 1 recheck.
As built:
- Writer. `#hostDefinition` refuses `host-creative-role` (:862) before the `host-not-declared` check.
The raise binding (:1266) and the step 1 recheck (:879) both call it, so a grant raised before its
agent's instance moved to a creative definition ends at step 1, spent, with no remote step.
- Spend. `#hostAccess` refuses `host-creative-role` (:714) when the agent's current definition is
creative, before it reads the record. Both callers go through it: host.run (:2543) and the host.sudo
raise (:2778). The record stays, because revoke still owns the cleanup (sage-1908). This goes past
the brief's writer line in the same direction; Sage tells Mos.
Tests:
- grant-host.test.mjs:115. A `muse` role on a `research` definition that declares host. The raise
refuses `host-creative-role` and adds no decision.
- grant-executor.test.mjs:947. The restart moves `ops` to a creative definition that declares host,
then a fixture grant runs. It ends `host-creative-role` at step 1, the decision is spent, and there is
no remote request and no record. The fixture comes after the restart, because the restart's reconcile
ends any resolved, unspent host grant as interrupted.
- grant-executor.test.mjs:958. An applied grant, then a restart with `ops` creative, then host.run
refuses `host-creative-role`. No remote request is added, sudo.log has no `systemctl`, and the record
is still there.
The broker clones `businesses` when it's built, so a definition change needs a new Broker. The test's
`restart()` gains a `business` parameter, default unchanged, for that.
Mutants: B38 removes the `#hostDefinition` line, run against grant-host and grant-executor. B39 removes
the `#hostAccess` line, run against grant-executor.
## Run 2 (sage-1928)
Run 2 on 534f2254a, 10:12:52-10:14:51Z, was green. grant-host.test.mjs 9 of 9, grant-executor.test.mjs
37 of 37 and grant-enroll.test.mjs 24 of 24, each run whole. grant-enroll is unchanged, but it drives the
broker, and broker.mjs changed. Then the five mutants, all killed, with the restore matching HEAD:
| Id | Killed by |
|---|---|
| B16 | G23 (X5), the revoke as verify returns |
| B23 | A2, A4 |
| B27 | G8, the store fault in the commit transaction |
| B38 | both mos-1163 tests |
| B39 | the sage-1908 test |
## Mutant results, B1-B39
| Run | Head | Killed | Survived |
|---|---|---|---|
| B1-B13, 09:22Z | 5df4b52c5 | 12 | B2 (file list) |
| B2 alone, B14-B37, 09:28-09:37Z | 5df4b52c5 | 21 | B16, B17, B23, B27 |
| B16 B23 B27 B38 B39, 10:13Z | 534f2254a | 5 | none |
38 of 39 are killed. B17 is equivalent (sage-1904), for the macrotask reason in the B1-B37 section.
B1-B37 ran on 5df4b52c5, before the move onto row 213's landing, and weren't rerun on 534f2254a.
`--check` matches all 39 anchors once on 534f2254a.
## Carry list
- N10 (duckworth-0084, row 234 r1): `cli/src/hosts.mjs:30-31` reads `shape[verb]` off a plain object,
so `mosaic hosts constructor` passes usage and the bus refuses it with exit 2 instead of 4. Row b adds
`hosts revoke` and `hosts sweep` to the same table (design r6 :562, :680), so the fix goes in with
them: `Object.hasOwn(shape, verb)` and a usage test for an `Object.prototype` name (sage-1726).
- Untimed tables (sage-1745, row 234 r3): stack-release T46 lists hosts and host_grants as untimed
because every write to them shares one store transaction with a `grant.*` events row. Row a writes only
hosts. Every host_grants INSERT, UPDATE and DELETE row b adds must do the same, and BUILD.md lists each
with its event line. A write without one is a stop to raise with Sage.
- N12 (duckworth-0086, row 234 r3, doc-only): row 234's round 3 list of other schema hits leaves out
`packages/cli/tests/stack-release-fixture.mjs:214` and `scripts/test-land-work.sh:254`. Both look up
one table by name and need no change. If row b changes the schema, its grep list names them.
## What row a already gives row b (b0320d395)
- `#approvedGrant`, `#consumeGrant(business, decision, method)` and `GRANT_EVIDENCE` (broker.mjs:667-690).
Row b calls `#consumeGrant` with `'host'`.
- `#openGrantDecisions(business, method)` and `reconcileGrants()` (broker.mjs:823-860), which today handle
`host-enroll` only. Row b adds the `host` method, the `grant.step` undo table (r6 X3) and the
revoke-marker reconcile (r6 section 6, N4).
- The resolve hook at broker.mjs:2444-2445 starts `#runEnrollment` for `host-enroll`. Row b adds the same
start for `host`, under the same human, via cli and choice checks.
- `host.run` already reads the store grant record and checks the Approve record (broker.mjs:691-703).
- The raise check `grant-chief-only` (broker.mjs:870, :896). `host-grant.mjs:55` refuses every method but
`host-enroll`, and row b opens it to `host`.
- `Credentials` has `#inflight` with the `host-busy` claim inside `runHost` (credentials.mjs:118, :355).
It has no `claim`, `abortClaim`, `register` or `unregister`. Row b adds those (r6 X4, X5).
## Plan, in build order
1. Raise binding (r6 R1-R4): the `grant` argument for `method: 'host'`, `host-not-declared`,
`host-not-ready`, the 210 D6 argv rules, `rotateBy`, the rendered block and the 4096 cap, and the raise
body hashes. Tests G1-G5, G19.
2. `Credentials.claim`, `release`, `abortClaim`, `register`, `unregister`, with the shared reference check.
3. The executor in `bus/src/host-grants.mjs`, steps 1-6 (r6 X2), with failure undo (X3) and the abort rule
(X5, C1). Tests G7-G10, G15, G23, G24.
4. Restart reconcile for `grant.step`, unspent `host` decisions, and the start sweep of `host.<id>` key
files with no record (r6 X3, N1). Tests G11, G27, G28.
5. `hosts.revoke` and `hosts.sweep` verbs, plus the CLI `revoke` and `sweep` with N10. Tests G12, G13,
G26, G27.
6. G22 end to end on one bus. Then the mutant list from r6 section 10 for row b, numbered B1 onward in
this file, with the upsert mutant and the abort-after-commit mutant included.
There is no schema step. If one turns out to be needed, I stop and ask Sage to re-brief first.
## Step 1 notes (raise binding, read on b0320d395)
Where the code goes:
- `#raiseGrant` (broker.mjs:866-885) runs the checks and keyscan for `host-enroll` today, and every raise
with `grant` reaches it (:1738-1739). Row b branches on `a.grant?.method`. `host` takes no keyscan, so
its checks can all run inside the one raise transaction, as R4 asks.
- `enrollCheck` (host-grant.mjs:50-69) refuses every method but `host-enroll` (:55). Row b adds a sibling
`hostGrantCheck` for the R4 shape `{method, agent, host, commands, rotateBy}` and keeps `enrollCheck`
as it is, so the 234 tests don't move.
- The raise body is at broker.mjs:2270, :2345-2353 and :2391. `enroll` becomes a generic `grant`
binding. The question block for `host` follows the `host.sudo` block (:2337-2341): agent, host id,
address, user, sudo, each prefix JSON-quoted, rotate date, and new key or changed commands. The
4096 cap comes from `string(question)` (:2354).
- The argv rules come from `checkCommands` (business/src/hosts.mjs:67-87): no wildcard, no empty prefix,
no glob characters, a bare name or an absolute path, and `refusedCommand` for shells and runners. It
throws `refuse(...)` with a message, so the bus maps any throw to `invalid-request`, as `enrollCheck`
maps `checkAddress` (host-grant.mjs:57-61).
- `host-not-declared`: the agent's definition must list `{"service": "host"}`. business.mjs:126-129 reads
`definition.credentials` and filters out `HOST_SERVICE` (vocabulary.mjs:113). The bus needs the
definition's service list. Read how the running business exposes the definition before choosing.
- Both lookups are answered. `loadBusiness` returns `definitions`, each role file's validated document
(business/src/business.mjs:114-124, :159, returned at :267). `busBusiness` clones the whole business
(bus/src/business.mjs:7), so the broker has `b.definitions`. `#definitions` already reads it
(broker.mjs:1027-1029), and the comment there says the broker doesn't read `roles/` (sage-0590).
- `host-not-declared` checks `b.definitions[b.roles[agent].definition].credentials` for
`{service: 'host'}`. role.mjs:118 normalizes a host entry to exactly that.
- Deviation from R4: `definitionSha256` is sha256 over `canonicalJson` of that validated definition,
not over the file's bytes. The broker doesn't read `roles/`. A whitespace-only edit to the file no
longer voids an approval, but any change to a field the loader keeps still does. The bus sees a
definition change only after a business reload or restart, the same as every other role field.
This goes in the round 1 note.
Assumption, accepted for round 1 by Sage (sage-1782), for Duckworth to check in review: design R4 says `rotateBy` is "a date inside 210's rotation window", but 210
defines no window in code. It checks only that the value is a real calendar day (bus
credentials.mjs:16-20, issue at :245). The business load calls a past `rotateBy` a problem
(business/src/credentials.mjs:114). So the raise takes a real day strictly after today, UTC, and refuses
`invalid-request` otherwise. There is no upper bound, because no requirement states one and 210
checks none.
## Step 1 as built (not run yet; no slot)
- `hostGrantCheck(grant, today)` in host-grant.mjs takes exactly `method`, `agent`, `host`, `commands` and
`rotateBy`. `agent` must match ID_PATTERN and `host` HOST_ID. `commands` goes through `checkCommands`,
and any throw becomes `invalid-request`. `rotateBy` must pass 210's `day` (now exported from bus
credentials.mjs) and be later than the bus clock's UTC day.
- `#raiseGrant` branches on `a.grant?.method === 'host'` after the chief check and before `enrollCheck`.
So a host-enroll shape with `method: 'host'` still refuses `invalid-request`, and the 234 G1 list holds.
- `#hostGrantBinding` runs inside the raise transaction. It refuses `unknown-role` for an agent that
isn't a role instance (the code bindLaunch uses, broker.mjs:522). It refuses `host-not-declared`, and
`host-unknown` for a host with no record, as `#hostAccess` does. A record in `pending-install` refuses
`host-not-ready`. R4 names only `host-not-ready`. I picked `host-unknown` for a missing record so
"not enrolled" and "enrolled but not checked" stay apart.
- `commandsSha256` is sha256 over `JSON.stringify(commands)`. For arrays of strings this equals
canonicalJson, and step 3's executor recomputes it the same way.
- `#dispatch` takes `binding = {body, block}` in place of `enroll`. The enrollment block moved into
`enrollBinding` at module level with its text unchanged, so the 234 G4 test reads the same question.
- No new import in broker.mjs, so RUNNER_FILES doesn't change. host-grant.mjs now imports
business/src/util.mjs for canonicalJson. Nothing in the sandbox loads host-grant.mjs. Before in-review
I still run seat sandbox.test.mjs and bus project-runtime.test.mjs whole.
- The new-key line reads host_grants (a SELECT). Step 1 writes no host_grants row.
- Tests: bus/tests/grant-host.test.mjs, G1 (host), G2, G3, rotateBy, G19, R4/G5 body hashes, G4 block.
The G5 tester-thread case: a thread run is always THREAD_DEFINITION, never chief-of-staff, so
`grant-chief-only` refuses it first. The test covers the tester's own cap (`tester-refused`).
## Step 2 as built (not run yet)
- The constructor's per-reference checks moved into `#checkRef`, in the same order, and `register` calls
it (r6 X4). `register` reads no file. A key file that a failed revoke unlink left at the path stays
unused until the start sweep (step 4) removes it. `register` refuses `credential-reference` while the
entry holds a live token. An entry with no token, revoked or never issued, is replaced, so a new grant
after a revoke registers again.
- `unregister` drops an entry with no token and clears its revoked mark. A live token refuses.
- `claim` refuses `host-busy` when the slot is held, by a claim or a `host.run`, and returns the signal.
It needs no reference, because the executor claims at step 1 and registers at step 5.
`release(instance, service, signal)` frees the slot only for its own signal. I added the signal
argument so a late release can't free a later claim.
- `abortClaim` aborts whatever holds the slot and returns `{pending}`. An aborted claim keeps the slot
until the executor releases it in its `finally` (X2).
- Tests: bus/tests/credential-claim.test.mjs. The D5 `use(` allowlist is unchanged.
## Step 3 as built (not run yet)
Where the code goes. The remote steps are in host-grant.mjs (`grantKey`, `install`, `verify`, `remove`)
and the orchestration is `#runHostGrant` in broker.mjs, next to `#runEnrollment`. BUILD.md's plan said a
new `bus/src/host-grants.mjs`. I kept row 234's split instead: the broker holds every store write and
host-grant.mjs holds the network and key files. A new broker import would also change what the sandbox
must mount (RUNNER_FILES), and this way broker.mjs gains none.
The broker gets `hostKeys`, made by `grantKeys({credentials, storeRoot})` in host-grant.mjs. It wraps
only `claim`, `release`, `abortClaim`, `register`, `unregister`, `issue` and `revoke`, keyed by
(business, agent, hostId). The broker still never gets `use()`. runtime.mjs builds it and passes it in.
`hostReference` gives the store reference, and the runtime's start load uses the same function.
Every host_grants write in step 3, with its event (sage-1745):
| Write | Where | Event in the same transaction |
|---|---|---|
| INSERT, a new key | the `commit` callback that `issue` calls | `grant.applied` |
| UPDATE commands and decision, a commands change | step 5's transaction | `grant.applied` |
No other step 3 code writes host_grants.
Deviations from r6, for the round 1 note:
- `grant.applied` commits with the record, inside `commit`, not after `issue` at step 6. The untimed-table
rule needs an event in the record's transaction. `grant.step commit` can't be that event, because X2
writes it before the key file. So X3's "5 after commit" row has nothing to undo in the executor: once the
record commits, `grant.applied` has too. What's left in `issue` after `commit` is the memory fill, which
writes no file and no store row. The executor sets `committed` after the transaction returns and from
then ignores an abort (C1). The restart reconcile (step 4) keeps the commit row, because a crash can fall
between the key file write and `commit`.
- Event bodies name the decision as `grantDecision`, as row 234's enrollment events do. The consume record
is the only grant body with `decision`, so `#spent` counts it and nothing else.
- For a commands change, `grant.applied` has `fingerprint: null`. The executor holds no key on that path.
The record's `rotate_by` stays. X2 names only commands and the decision id.
- The keygen dir goes as soon as `grantKey` has read the key back into memory, not in step 5's `finally`.
- Step 4 runs `true` as the remote command as is, not through the sudo argv, since a user with no sudo
must pass it too.
- `hostGrant()` takes `installerUser`, `mosaic-installer` by default. Only the tests change it, because
the scratch sshd logs in only as the test's own user.
- A commands-change UPDATE that changes no row ends `host-changed`. Only a revoke deletes a live record,
and a revoke aborts the claim first, so this is a guard.
Step 1 codes, after the spend and in its transaction: `host-not-configured`, `unknown-role`,
`host-not-declared`, `decision-mismatch` (definition, registry or commands hash), `host-unknown`,
`host-not-ready`, `revoked` (a `grant.revoked` of the agent and host after the raise). The definition
check moved into `#hostDefinition`, which the raise binding uses too.
Tests: bus/tests/grant-executor.test.mjs, a scratch sshd on 127.0.0.1 with stub `list`, `add` and
`remove` that edit a second authorized-keys file. G7 (new key, and a commands change), G8 (install, verify,
a failed remove, step 5 before commit, a failed step 1 check), G9, G10, G15, G23 (a revoke during install
through `abortClaim`, a `grant.revoked` before start, and a held slot), G24's tag part. G23's held slot
uses `claim` in place of a running `host.run`. Both hold the same `#inflight` entry. The `revoke` verb
comes in step 5, so G23 here writes `grant.revoked` through the fixture writer and aborts through
`abortClaim`. Step 5 adds the verb's own path.
## Step 4 as built (not run yet)
The restart reconcile has three parts, in start order:
1. `sweepHostKeys` (host-grant.mjs), in runtime.mjs right before `Credentials` loads. In each loaded
business's agent store it unlinks every `host.<id>` file that has no `host_grants` record, and logs
one `hosts: {"event":"host-key-swept",...}` line per file to stderr. A store dir that isn't private
refuses `credential-file`, and so does the start. A name that isn't `host.` plus a valid host id
stays, so ssh-keygen's `.pub` stays too. A business the bus didn't load is left alone.
2. `reconcileGrants`, where row 234 already calls it, after `Credentials` loads and before the socket
opens. For a `host` grant it ends a spent decision that has no `grant.applied` and no `grant.failed`.
It also spends and ends an unspent one the human resolved yes. Each ends `grant.failed interrupted`,
with the tag and a notice. When the last step is `install`, or `commit` with `previous: null`, a line
may be left. For those, it claims the (agent, `host.<id>`) slot and queues the decision for part 3.
3. `reconcileHosts`, which runtime.mjs calls once `serve` returns, with no await, so a start doesn't
wait on the network. Each queued decision runs `remove <user> <tag>` through the installer, as the
step's `user`. Then it ends `grant.failed interrupted`, with `hostLineLeft` if the remove failed or
there's no host record, and releases the claim. The claim makes a new grant for the same agent and
host refuse `host-busy` while the remove runs. Otherwise its new line, with the same tag, could be
removed.
Deviations from r6's restart table, for the round 1 note:
- No record write on restart. Step 3 commits the record with `grant.applied` in one transaction. So a
decision with no `grant.applied` has no record. For a new key, the record undo has nothing to delete.
For a commands change, the "back to `previous`" undo has nothing to put back. What's left of the
`commit, new key` row is the key file, written before `commit`, and the start sweep unlinks it.
The `commit, commands change` row ends interrupted with nothing undone. G28's "a revoke before
`grant.applied`" can't happen, because they commit together. Step 5 tests G28 as a change, then a
revoke, then a restart.
- The local undo of row 234's enrollments stays after `Credentials` loads, where row 234 put it. That
undo touches only the installer key and the host record, and `Credentials` reads neither.
- The host grant part writes no `host_grants` row, so the untimed-table rule (sage-1745) adds nothing
here.
Fixture change: `hostGrantRecord` (tests/host-fixtures.mjs) writes `grant.applied` after a `grant.host`
spend, as the executor always does. Without it, a start over a fixture record ends that record's
decision `interrupted`. Row 234's M3 start test (host-run.test.mjs:602) does that start. No test counts
the fixture's grant events (checked by grep across the bus tests).
Tests in grant-executor.test.mjs, through a `restart` helper that follows runtime.mjs's order on the
same store:
- G11 after `install`: the line goes, the slot is held between the two parts, and the grant ends
interrupted. A second start adds nothing, and `runHostGrant` refuses `decision-consumed`.
- G11 after a new key's `commit` with the key file written: no key file, record, reference or line.
- G11 after a change's `commit`: the record, key and line are unchanged, and the host gets no request.
- G11 (B2) before step 1, spent and unspent: each ends interrupted with a notice, and the unspent one
is spent exactly once.
- A failed remote undo, and a missing host record: each ends with `hostLineLeft` and the sweep hint.
- A finished grant: the reconcile adds nothing.
- N1 (G27's sweep part): only files with no record in loaded businesses go, and a dir that isn't private
refuses.
The revoke-marker part of the reconcile (section 6, N4) comes with the revoke verb in step 5.
## Step 5 as built (not run yet)
Two new bus verbs, `hosts.revoke` and `hosts.sweep`, join `HOSTS_VERBS`. The CLI gets
`mosaic hosts revoke AGENT HOST --reason R` and `mosaic hosts sweep HOST`.
`hosts.revoke {agent, host, reason}` follows r6 section 6:
1. One transaction deletes the `host_grants` record and writes `grant.revoked` with a new `revoke` id
(a UUID), the computed tag, `hostLine: "pending"`, `granted`, `reason` and `by`. With no record it
still writes the marker, with `granted: false`, so a stale line can be named (N4). A host with no
record refuses `host-unknown` and writes nothing.
2. With no await between, `Credentials.revoke` drops the key from memory, aborts a `host.run` in flight
and unlinks the file. With no reference (`credential-unknown`), an executor holds only a claim, so
`abortClaim` stops it. Any other throw, such as an unlink that fails, comes back as `keyFile: "left"`.
The record is already gone, so the next start's sweep unlinks that file.
3. `remove <user> <tag>` through the installer, as the host record's user (the executor's user too).
Then `grant.revoke.closed` with the same `revoke` id and `hostLine: "removed"`, or `hostLineLeft`.
Callers (G26): the human, and the assistant of the session's business, checked with `#threaded`. The
tester can call it when `bus.tester` is exactly `true`, read again at the call. Any other agent gets
`revoke-refused`, a code new in this row. A tester session can't hold a business whose `bus.tester`
isn't `true`, since `bindTester` refuses `tester-off`, and the broker's businesses don't change while
it runs. So `tester-dev-only` can't be reached from a test. The check stays for N8, in case a later
row reloads businesses.
`hosts.sweep {host}` is the human's or the tester's; an agent gets `human-required`. It runs
`list <user>`, then removes each listed tag that this business recorded for the host in a `grant.step`
or `grant.revoked` and that no live grant holds. Another business's line and an unrecorded `mosaic-`
line stay (G24's sweep part). It writes `grant.host.swept {host, removed, left, by}`, where `left` is
each earlier left tag still listed and not removed. `#hostLinesLeft` now starts from the last
`grant.host.swept` and adds only later `hostLineLeft` bodies (F4).
The restart reconcile adds the marker part: `reconcileGrants` collects each `grant.revoked` with
`hostLine: "pending"`, a text `revoke` id and no `grant.revoke.closed` of that id. `reconcileHosts`
runs step 3 for each. A `grant.revoked` with no `revoke` id, as G23's fixtures write, isn't reopened.
`hosts.status` adds `revoking`, the tags of the open markers on the host.
Other changes:
- N10: the CLI checks the verb with `Object.hasOwn`, so `constructor`, `toString` or `__proto__` is a
usage error.
- `--reason` is required with `revoke` and refused with any other verb.
- The reason goes through the broker's `string(x, 500)`, as `credentials.revoke`'s does, so a reason of
only spaces is accepted there too.
- Correction to step 4: the first G11 test expected `list` then `remove`. `reconcileHosts` sends only
`remove`, so the test now expects that.
Residuals for the round 1 note:
- A new grant whose line goes in while a revoke's step 3 or a sweep runs can lose that line, if it has
the revoked tag. Revoke holds no claim during step 3. If the remove lands after the new grant's
verify, the grant is recorded but its line is gone, so its logins fail. I didn't add a claim, because
a revoke must not wait on a slot.
- The enrollment's installer tag is never in the sweep's set, so a `hostLineLeft` naming it stays
until the human removes it by hand.
Tests in grant-executor.test.mjs:
- G12: the human's revoke of a live grant, and the bodies of both markers. A restart adds nothing.
- G12 (N4): a revoke with no record removes the computed tag's line. Bad args refuse
`invalid-request` and an unknown host `host-unknown`.
- G13: a revoke during `install` aborts the executor, which ends `revoked`. A raise before a revoke
and started after it ends `revoked` at step 1.
- G26: who may revoke and who may sweep.
- G27: a restart after step 1 (the sweep takes the key file, then step 3 runs), after step 2 with the
host down (`hostLineLeft`, then a sweep clears it), and a file a revoke couldn't unlink.
- G28: a commands change, then a revoke, then a restart.
- G24 (B4): the sweep's tag set, a failing `list`, and the left list it records.
The CLI tests in packages/cli/tests/hosts.test.mjs cover revoke and sweep, the N10 usage cases and the
`revoking` field in a status line.
## Step 6 as built (not run yet)
New tests in grant-executor.test.mjs:
- G22 (B1), one bus with no restart. The chief enrolls web1, the human approves, and `hosts.check`
marks it ready. A grant of `echo` follows, then a `host.run` of `echo hi` that prints `hi`. A
`sleep` raise refuses `command-not-granted`, and after a commands change to `echo` and `sleep` the
next `host.run` of `sleep 30` starts. A revoke kills that run with `credential-unavailable`. The next
call refuses `host-not-granted`.
- G12: the local steps finish before the host step. With the host's `remove` held, the record and key
file are already gone and `hosts.status` shows the tag under `revoking`.
- G13: a revoke whose host `remove` fails returns `hostLineLeft`, drops the record and key, and closes
the marker, so a restart adds nothing.
- G23 (X5): a revoke right after the executor's spend reaches its claim and the grant ends `revoked`.
- G23 (X5): a revoke that lands as `verify` returns ends `revoked` with no record, key file or line.
Only the abort check after `verify` catches it.
The fixture gains a `sudo` stub (logs to sudo.log, then runs the command as the test's user), a
`hold-remove` flag, `opts.enroll` (no host record and no installer key) and `opts.wrap` (changes the
host API the broker gets). `hostGrant` gets a fake `scan` that returns the scratch sshd's host key.
G22 deviations, for the round 1 note:
- `checkAddress` (business/src/hosts.mjs:62) refuses this machine's address, so the test enrolls
192.0.2.10 with the fake keyscan. It then points the record at the scratch sshd with an SQL update
and adds the enrolled installer key to the sshd's authorized keys. Enrollment, check, grant, run and
revoke all go through the real code after that.
- r6 B1 says the next `host.run` after a revoke refuses `credential-unavailable`. Here it refuses
`host-not-granted`, because step 1 deletes the record and `requestHost` checks the record
(`#hostAccess`) before it reaches the key. The call in flight does refuse `credential-unavailable`.
### Mutants
`/mnt/storage/scratch/gizmo-235-out/mutants.py` holds each mutant as anchored edits. `--check` applies
them in memory and runs no test. All 39 anchors match once. Each mutant runs its named test files.
From r6 section 10, as they apply to row b:
| Id | Mutant | Test file |
|---|---|---|
| B1 | drop the chief check on a host grant raise | grant-host (G1) |
| B2 | start the executor on a `credential.mint` with no `grant` | grant-enroll (A2), grant-executor |
| B3 | skip the `ready` check at the raise | grant-host (G19) |
| B4 | skip the `ready` check at step 1 | grant-executor |
| B5 | skip the definition hash compare | grant-executor (G9) |
| B6 | drop the definition check (`host-not-declared`) | grant-host (G2), grant-executor (G9) |
| B7 | skip the commands hash compare | grant-executor (G9) |
| B8 | skip the host record hash compare | grant-executor (G9) |
| B9 | write the grant record before install | grant-executor (G8) |
| B10 | skip `remove` on a verify failure | grant-executor (G8) |
| B11 | skip the start sweep of key files with no record | grant-executor (N1, G27) |
| B12 | skip the reconcile's remote `remove` | grant-executor (G11) |
| B13 | skip the `grant.step` before install | grant-executor (G11) |
| B14 | revoke leaves the grant record | grant-executor (G12) |
| B15 | skip `register` before `issue` | grant-executor (G7) |
| B16 | drop the abort check after `verify`, the last one before `issue` | grant-executor (G23 X5) |
| B17 | add an `await` between the abort check and `issue` | grant-executor |
| B18 | revoke ignores a claim with no reference | grant-executor (G13, G23 X5) |
| B19 | build the tag without the business | grant-executor (G24) |
| B20 | `sweep` removes any `mosaic-` line | grant-executor (G24 B4) |
| B21 | run revoke's host step before the local steps | grant-executor (G12) |
| B22 | start the executor on an arbiter resolve | grant-executor (G10) |
| B23 | start it on `no` | grant-enroll (A2, A4), grant-executor |
| B24 | write the private key into the event body | grant-executor (G15) |
| B25 | put an `await` between the spend and the claim | grant-executor (G23 X5) |
| B26 | skip the start check for a resolved, unspent grant decision | grant-executor (G11 B2) |
| B27 | make `commit` async | grant-executor (G8) |
| B28 | route `hosts.revoke` to `Credentials.revoke` alone, as `credential-verbs.mjs` would | grant-executor (G12, G27) |
| B29 | skip the revoke marker reconcile | grant-executor (G27) |
Row 235's own:
| Id | Mutant | Test file |
|---|---|---|
| B30 | skip the `grant.revoked` check at step 1 | grant-executor (G13, G23) |
| B31 | the sweep set keeps a live grant's tag | grant-executor (G24 B4) |
| B32 | reopen a `grant.revoked` with no revoke id | grant-executor (G27) |
| B33 | step 3 reports `removed` when the host remove fails | grant-executor (G13, G27) |
| B34 | any agent revokes, not just the assistant | grant-executor (G26) |
| B35 | `hostLineLeft` ignores the last sweep | grant-executor (G24 B4, G27) |
| B36 | a stale `release` frees a later claim | credential-claim (X5), grant-executor |
| B37 | `register` replaces an entry with a live token | credential-claim (X4) |
| B38 | drop the creative check in `#hostDefinition` | grant-host (mos-1163), grant-executor (mos-1163) |
| B39 | drop the creative check in `#hostAccess` | grant-executor (sage-1908) |
B17 may survive. `await null` yields only to microtasks, and every revoke starts in a macrotask of its
own (a socket request, or a test's call), so none can land in that gap. If it survives I'll report it
as equivalent under this build, not add a test that fakes a microtask revoke.
Section 10 items not run as row b mutants, and why:
- The upsert mutant ("let the undo to `previous` insert a record") and "run past a newer
`grant.revoked`". Row 235 has no undo to `previous`. A commands change commits the UPDATE and
`grant.applied` in one transaction (sage-1745), so a restart after it leaves the record as it is
(G11). The UPDATE can't meet a missing record either: step 1 reads the live record, and nothing
awaits between there and the UPDATE. An upsert there would be equivalent.
- "let the executor act on an abort after step 5 commits". The record and `grant.applied` commit
together, and no abort check follows the commit, so no code path reaches the `if (committed)` guard
with an abort. Removing the guard is equivalent. C1's case is covered by G28 with a later revoke.
- "treat a missing `bus.tester` as dev". The tester check can't be reached from a test (step 5 notes).
- Row a's: the keyscan compare, the writer items, the "not yet enrolled" check, the `hosts` primary key
and `host.run` reading the business file. Row 234 ran those as A5, A6, A8, A9, A11 and A13-A19.
+98
View File
@@ -0,0 +1,98 @@
# Row 249 build notes (Gizmoduck)
Gitea #1751, Vikunja P19 445 under #436. Brief: docs/plans/2026-10-20_host-credentials.md, section
"Machine grant row b follow-up: the undo can't prove the remote add has ended" (6de1ebc62). Reviewer
Duckworth, gate owner Sage. Branch gizmoduck/row249-on-8a04, on row 248's landing 8a04f4aa4 (sage-2012).
No file under packages/bus/src changed between 6d5545d44 and 8a04f4aa4, so the brief's src line numbers
hold on the base. Line numbers below are on this branch.
## Correction to the brief
The brief, copying gizmoduck-0139, puts the sweep's host-unknown refusal at broker.mjs:1350. On
6d5545d44 it is :1349. On this branch it is :1354.
## The change
`#runHostGrant` in packages/bus/src/broker.mjs:
- :943 adds `installed = false` next to `installing`.
- :956 sets `installed = true` on the line after `await this.#hostGrant.install(...)`, before the
`stop()` that follows it. An abort that lands after install returned still ends revoked, with no hint,
because install returning means every request it sent has exited.
- :1025, in the catch, inside `if (installing)` and after the remove: `if (!installed) extra =
{ hostLineLeft: tag };`. The remove still runs first and its result doesn't matter. A remove that
throws sets the same hint as before (:1021).
The rule keys on install not returning, not on `signal.aborted`. That covers an abort (host-run.mjs:136,
SIGKILL to the local group), a timeout (host-run.mjs:131-134, SIGTERM, then SIGKILL after `graceMs`) and
a dropped connection, which fails the request with no signal at all. `#hostGrantFailed` (:845) already
adds "A line tagged X may be left on the host. Run: mosaic hosts sweep <host>" when `hostLineLeft` is
set (:852), so the message needed no change.
False hints. install is one call that runs `list`, maybe `remove`, then `add` (host-grant.mjs:329-333).
Any failure inside it now records the hint, including a `list` that exits 1 before any add was sent, and
an abort that lands before the first ssh starts (the G23 X5 case at "a revoke right after the spend").
The brief accepts this for an add that exits nonzero. The other cases are the same kind: the line isn't
on the host, and a sweep that doesn't list the tag drops the hint (G24, "a left tag no longer on the
host", broker.mjs:1358). Narrowing it would need install to report which request was in flight, which is
a change to host-grant.mjs's API that the brief doesn't ask for.
## Row a's enroll
The race can't happen there, because enrollment sends nothing to the host. `#runEnrollment`
(broker.mjs:772-842) writes the record, calls `this.#hostGrant.enroll(business, host)` (:800) and
commits the fingerprint. `enroll` (host-grant.mjs:247-286) runs ssh-keygen locally and writes the
private half to its file. It opens no ssh session. The installer line goes on the host by hand: the
notice tells ops-01 to write it, then to run `mosaic hosts check`. The undo at :835 is `unenroll`, which
deletes the local key file. There is no remote add to outlive a kill, and no remote undo to race with.
## Tests
packages/bus/tests/grant-executor.test.mjs:
- `lateAdd` (:451) is the shared tail. After the grant ends it asserts the code, `hostLineLeft` on
grant.failed, the sweep line in the notice, and that the undo's remove was the last request. Then it
writes `release`, so the held add writes its line and exits 0, and waits for the line in `granted`. It
checks there's no record, `hosts.status` lists the tag in `hostLineLeft`, `hosts.remove` refuses
host-line-left, `hosts.sweep` returns `removed: [tag]` and the line is gone, then `hosts.remove`
succeeds.
- :468, the abort case. `hold-add`, the human approves, the add holds, `abortClaim` aborts the
executor's claim. Ends revoked.
- :480, the timeout case. A 30 s wait is too long for the file, and making the timeout an option would
add a src seam, so the test wraps `install` through `opts.wrap` instead. The wrapper passes install its
own AbortController signal, waits for `holding`, aborts it, which SIGKILLs the local ssh group as the
timer does, then throws as a timed-out request does. The executor's signal isn't aborted, so it ends
host-failed. Nothing under src changes for it.
- G8 (:283) changes. fail-list and fail-add now carry `hostLineLeft` and the sweep line. noop-add fails at
verify, after install returned, so it keeps no hint and the old message.
- The comment at :436 (:432 at 6d5545d44) now says `skip` makes the held add exit without writing and that a
killed session's add can still write. G13 has no such comment.
Both new tests write `release`, so their held stub exits before `t.after` removes the dir. Row 248's dir
check (:67, :72) is the backstop.
## Mutant
/mnt/storage/scratch/gizmo-235-out/mutants-249.py, on row 235's runner. C1 removes the :1025 line and
runs grant-executor.test.mjs. Both new tests and G8 should fail under it. `--check` matches its anchor
once.
## Runs
On Sage's grant sage-2015, at 8457af076 (7cee1a1ab plus this file), TMPDIR /mnt/storage/scratch/g249t:
| Run | Window | Result |
|---|---|---|
| grant-executor.test.mjs whole | 13:29:01-13:29:21Z | 39 of 39 pass |
| C1 | 13:29:21-13:29:40Z | killed: both row 249 tests and G8 fail on `hostLineLeft` undefined |
After C1 the worktree was clean and broker.mjs matched HEAD. After the runs, no node, sshd or sh process had a
cwd under the worktree or g249t, and no `fake/add` or `fake/remove` stub was running. The bus suite runs on
Sage's gate.
## Rebase onto 8a04f4aa4 (sage-2012)
The first commit, 02bfd184b, sat on bc579f7a8 as gizmoduck/row249, which was never pushed. Row 248 landed
as 8a04f4aa4, changing grant-executor.test.mjs at :54, :67 and :72. gizmoduck/row249-on-8a04 starts at
8a04f4aa4 and cherry-picks 02bfd184b with no conflict. 248's lines stay as landed. The patch-id is the
same on both (c33823941e61). Test line numbers moved down by one, and this file was updated for that.
+100
View File
@@ -0,0 +1,100 @@
# Row 250 build notes (Gizmoduck)
Vikunja #450, Mos ruling mos-1182 item 3. Brief: docs/plans/2026-10-21_k12-cohort-events.md. Reviewer
Filbert, gate owner Sage. Branch gizmoduck/row250 from origin/refactor 3b0aef995. Line numbers below are on
this branch unless a commit is named.
## Step 1
cbba6ff07 changes only the K12 assertion (packages/conversation/tests/cohort.test.mjs:292 on 3b0aef995).
On failure it prints the shim's answer, with `ok` and `unavailable`, and `h.ctrl.evidence.releases`.
## Step 2: the cause
K12 reads the shim after the controller has started to release it.
- The force stop sets the binding `stopped` (packages/conversation/src/controller.mjs:1615). Then it pauses at
`scope-release` (:1623) and calls `#releaseScope` (:1624).
- `#releaseScope` calls `releaseCohort` (:1639). That sends `hello`, then `release`
(packages/conversation/src/cohort.mjs:261-265).
- The shim answers `release` and exits 10 ms later. It unlinks its socket first (shim.mjs:165-176). systemd
then collects the empty scope, `engine` included.
- K12's `forceStop` helper returns once it sees `stopped` (cohort.test.mjs:148, polling every 10 ms). The
test then reads `engine/cgroup.freeze` (:281 on 3b0aef995) and asks the shim for `events` (:291).
Nothing in K12 orders those reads before the release. Unloaded, the test usually sends its `events` request
within the 10 ms before the shim exits, so it passes. Under load the release wins. `shimRequest` then
returns `{ ok: false, unavailable: "the shim socket is gone" }` (cohort.mjs:46), or "unreachable" or
"closed the connection" (:62-63), and `ev.populated` is undefined. Both recorded failures fit this
pattern:
- Row 197 proof 4, pair1-b: K12 failed in 339 ms on `undefined !== 0` at :292. 339 ms is far below the
3 s `shimRequest` timeout (cohort.mjs:56), so the request didn't time out. The `cgroup.freeze` read at
:281 passed, so the scope still existed then.
- BUILD-LOG.md:4092, row 56's gate: "`populated` undefined with load around 12".
Row 50 (a9cc522a0, #1536) added the release after a proven stop. K12 is older. Its comment still says
"the shim holds the scope", and that stopped being true in row 50. E1 (cohort.test.mjs:923-925) already
holds `scope-release` so it can read the shim while the shim still answers. K12 never got the hold. No
other test reads the shim after a proven stop and expects an answer. R1 expects the shim gone, R3 is an
uncertain stop, and K15 reads before its stop.
The cgroup read itself has no race. The kill's `waitFor` (shim.mjs:117-126) had already read `populated 0`
for the stop to be proven. The read is the shim's call to `events()` (shim.mjs:83-96), and it reads one
kernel file. So the brief's allowance for a bounded retry doesn't apply, and there is none.
The fix goes in the test's setup, which the brief allows ("Use the shim, the cohort controller or the
test's setup and teardown, whichever the evidence points to"). The controller's release is correct and
specified (README "Scope release", R1). Delaying it for a reader would change row 50's behavior.
## Step 2: the change
packages/conversation/tests/cohort.test.mjs, K12:
- It runs with `gate()`, holds `scope-release` before the stop, and waits until the hold is reached. Then it
does every read that needs the scope: `cgroup.freeze`, `events` and `members`.
- After the reads it lets the release go and asserts one entry, `["force-stop", "released"]`. This is new
coverage: K12 now also shows the forking cohort's scope released.
- The assertions are unchanged. `populated` must be 0 and the member list must be empty. The step 1 message
stays.
packages/conversation/src/shim.mjs:167-168, `release` (:167 on 3b0aef995). It answered a failed `events()` with "the engine
cgroup is not empty", so it reported unavailable as not empty. The refusal was already safe, but the reason
was false. Now an `events()` answer that isn't `ok` is returned as it is, with the read's own reason. Only
`populated` 1 gives "not empty". Nothing else matched that string (grep of packages/).
## Tests
- K12 as above.
- R13, the brief's new case, placed after R3. After a proven stop, with `scope-release` held, the test makes
`engine/cgroup.events` mode 000. Then:
- `events` returns `ok: false`, has no `populated`, and the reason is "engine/cgroup.events unreadable
(EACCES)".
- On release, the controller records one entry `["force-stop", "unavailable", <that reason>]`.
- The unit stays active, and the shim still answers `hello`.
Then `close()` reuses the stop's release, so there is still one entry. The `finally` closes the
controller again (a second close is a no-op), restores mode 444, the kernel's mode for cgroup.events,
and releases the scope. On
3b0aef995's shim.mjs, R13 fails on the reason, because it reads "the engine cgroup is not empty".
- K15 already covers a stop, as opposed to a release, with `events` unavailable: the shim gone, the file
mode 000, or `engine` missing, each ending `uncertain` with its reason.
## File added beyond the brief
packages/conversation/README.md:599 listed "R1–R3, R5–R12" for cohort.test.mjs. With R13 it now reads
"R1–R3, R5–R13". The brief's files don't include the README. Sage added it to this row in sage-2082.
## Runs
None yet. They need Sage's slot. Sage's sage-2082 says the run is on the pushed candidate in a clean
worktree, and that it adds the control-board suite whole, so it counts as the gate run.
## Round 2
Filbert's C1 (#1755 comment 30125): R13 never closed its in-process controller, so the controller's
socket server (controller.mjs:548) stayed open, and `reap` only kills units and engines. Only `close()`
closes it (:1789). R13 now calls `h.ctrl.close()` after its checks and asserts one release entry, as R2
does. Its `finally` also closes the controller, for a failure partway through. The second close is safe:
`server` is null by then (:1791), and `#releaseScope` returns the cached release (:1634-1648). Branch
gizmoduck/row250-r2, on e9cfd92f8.
+11
View File
@@ -0,0 +1,11 @@
# Gosalyn
Worker seat for Mosaic Stack, MVP track T2 (Case 2, the creative writing hub, Vikunja #416).
- T3 thread: 7604438a-0ded-41c8-bd3f-26f57fdb5cd2
- Model: Opus 5.5 (Claude Code, T3)
- Launched by Mos (Jason's main agent, T3 38bc4c81-6270-4021-b00c-6a15c43b9b0e), who briefs the seat
- Repository lead and queue: Sage (T3 1ef1e4f8-3ead-4208-beca-38f9f1add079)
- Branch: gosalyn/case2-design, pushed only by this seat, landed on refactor by Sage
Work lives in `work/`. The Case 2 design is `work/case2/DESIGN.md`.
+53
View File
@@ -0,0 +1,53 @@
# Row 212 build notes, C2-01 packages/library (#1713, Vikunja #416)
## Rounds
- Round 1: bdf18272a on gosalyn/c2-01, base 32b54ac3b. Webby approved it in Gitea comment 29704.
- Round 2: d3c7bbf839d9c3bf50f154b18e7e4472fbd005ed on gosalyn/c2-01-r2, rebased onto 9b5362018 after row 232 drifted three paths (sage-1714). Webby approved it at merge scope, rev 2666 against comment 29921 (webby-0052).
- Round 3: this commit on gosalyn/c2-01-r3, one commit on the row 234 landing c771a36f4 (sage-1736, sage-1779, mos-1160). Row 234 changed scripts/mosaic, so round 3 review covers that one file.
## Round 2 merge
range-diff 32b54ac3b..bdf18272a against 9b5362018..d3c7bbf83 shows one change, the context line where upstream added `credentials` to the scripts/mosaic case line. Patch-id moved from 8f80dfa3 to 175f45c6 for that reason only. All three drifted hunks keep both sides:
- scripts/stack-copy.sh COPY_PATHS reads `packages/queue/src packages/library/src`. Row 232's bundle, --create, --units and instance code is unchanged.
- scripts/mosaic runs the `library survey` exec block before the case line, and the case line keeps `credentials`.
- packages/cli/tests/stack-copy.test.mjs adds `packages/library/src/index.mjs` to the copy file list.
## Round 2 singles, evidence for the 19 paths other than scripts/mosaic
Run on d3c7bbf83 in Sage's slot (sage-1723) from 2026-10-21T05:00:09Z to 05:11:29Z. The HEAD and a clean tree were checked first. The counts are an awk sum over the 23 logs: 481 passed, 0 failed, 0 skipped, 0 cancelled, and every file exited rc=0.
| Package | File | Pass |
|---|---|---|
| library | links | 6 |
| library | mapping | 7 |
| library | reader | 11 |
| library | render | 11 |
| library | survey | 4 |
| cli | app | 30 |
| cli | case2-unit | 30 |
| cli | credentials | 5 |
| cli | host | 42 |
| cli | instances | 5 |
| cli | project | 4 |
| cli | stack-copy | 16 |
| cli | stack-instances | 23 |
| cli | stack-release-external | 25 |
| cli | stack-release | 192 |
| cli | tester-reads | 6 |
| cli | tester-threads | 5 |
| cli | usage | 2 |
| queue | dispatch | 4 |
| runs | task-cli | 9 |
| webui | pinned-copy | 10 |
| webui | service | 31 |
| webui | views | 3 |
The file list comes from the library tests plus every test that names scripts/mosaic or stack-copy.sh, by basename and by join() path. No 64-hex pin in the tests names a merged path.
## Round 3 scripts/mosaic
The only path both row 234 and this row change is scripts/mosaic. Row 234 added `hosts` to the cli case line and changed nothing else in the file. Round 3 takes the landed blob cce1fdee4 and adds this row's six-line `library survey` block before the case line by hand, so `hosts` stays. The mode stays 100755. The other 19 paths are byte-identical to d3c7bbf83. Against d3c7bbf83 the only difference is the `hosts` context line.
Round 3 runs only the tests that read scripts/mosaic, in Sage's slot. Their counts go to Sage in T3 and onto #1713, since this file can't hold them and stay one commit. Those tests are library survey, cli app, case2-unit, credentials, hosts, host, instances, project, stack-copy, stack-release, stack-release-external, stack-upgrade, tester-reads, tester-threads and usage, queue dispatch, runs task-cli, webui pinned-copy, service and views, plus scripts/test-queue.sh. hosts.test.mjs is new with row 234.
+158
View File
@@ -0,0 +1,158 @@
# Row 221 build notes, C2-20 Bean and Chet persona import (#1722, Vikunja #416)
Brief: docs/plans/2026-10-20_case2-rows.md "C2-20", blob 16847e94ae17 (sage-1834). Claimed rev 2736.
Rulings: sage-1836 (fence, pairing) and sage-1840 (YAML parser, handoff).
## Base
Built on Morgana's row 213 r4 tip 2b3e67e36 (origin/morgana/row213-r4), since the brief says "After:
C2-08" and 221 exports 213's fence check. Before review it goes onto 213's landing under a new branch
name, and its tests run again there.
## What changed
- `scripts/case2-persona-import.mjs`, new. Verbs `stage` and `install`, usage in its header.
- `packages/cli/tests/case2-persona-import.test.mjs`, new, and `packages/cli/tests/fixtures/case2-persona-import/`,
a fixture Hermes home plus `trace.mjs`, the read trace.
- `packages/harness/src/bundle.mjs`: the three text rules in `readPersona` move into an exported
`personaTextFault(text)`, which returns null or the reason, and `readPersona` calls it. Nothing it
checks changes and the messages are byte for byte the same, so 213's creative tests hold. There is
one copy of the rules and the import calls it (sage-1836).
- `package.json` and `package-lock.json`: `yaml` 2.9.0 becomes an exact direct dependency (sage-1840 Q3).
## Rulings recorded
Profile to name (sage-1836, answers DESIGN.md:1322 as an implementation ruling, not a PRD change).
Hermes at df5700ebe has no field holding a profile's own name. `profile.yaml` has only `description`
and `description_auto` (hermes_cli/profiles.py:798-850), and `agent_name` at cli.py:3494 is skin
branding with the default "Hermes Agent". So the human passes the pairing as `--pair default=Bean
--pair chet=Chet`. The script checks both profile directories exist, each name is exactly Bean or Chet
and the two differ, then prints the pairing with each SOUL.md's byte count and sha256 before it writes.
The brief's "confirm profile to name" is met by the human supplying and confirming the pairing.
The persona is `<profile dir>/SOUL.md`, where Hermes loads it (agent/prompt_builder.py:1829-1855).
The default profile's directory is the Hermes root, and others are `profiles/<name>`, with names
checked against hermes_cli/profiles.py:37. A missing or blank SOUL.md is "no persona", as in Hermes.
The file is staged byte for byte. Hermes strips it and runs its own scan before use, but this import
copies her text unchanged.
Handoff between the steps (sage-1840 Q4). `stage` writes 0600 files owned by whoever ran it. `install`
runs as `<user>` and refuses a staging file that isn't its own or has a group or other bit. The script
changes no owner. It prints the root step, `chown -R <user>: <staging>`, as an instruction. **Item for
Jason's go:** who runs stage on `<host>`, and the root chown between the two steps.
## Word list
69 tool names: every literal `name="..."` in a `registry.register(` call under hermes-agent's
`tools/*.py` at df5700ebe, plus Hermes and Discord. Two more registrations, in tools/mcp_tool.py:4997
and :5034, and plugin tools (hermes_cli/plugins.py:430) get their names at run time, so a fixed list
can't hold them. Matching is whole word, any case. `memory`, `terminal`, `todo`, `process`, `patch` and
`clarify` are ordinary words, so a persona line using them in prose counts, and the count leans high.
It is a hint for Jason and changes nothing in the import.
## Channel prompts
Hermes takes a platform's block from `<platform>` at the top level of config.yaml, else from
`platforms.<platform>` or `gateway.platforms.<platform>` (gateway/config.py:1159-1178), and its
prompts from `channel_prompts` (:1233). The script lists all three by platform, channel id and byte
count, and says "not text" for a value that isn't a string. The dry run doesn't read config.yaml.
yaml parses with `intAsBigInt`, so an unquoted 18 or 19 digit Discord id prints exactly instead of
rounding as a double, and with `uniqueKeys: false`, so a repeated key keeps the last value as Python's
`safe_load` does. `logLevel: "silent"` stops yaml's warnings, which quote config text. yaml reads YAML
1.2 and Hermes's PyYAML reads 1.1. They differ on scalars such as `yes` and `0123`, but neither
changes a channel prompt's key or its byte count for a quoted or decimal id.
## yaml dependency (sage-1840 Q3)
Made with `npm install --save-exact [email protected] --package-lock-only --ignore-scripts` at the worktree
root. `git config core.hooksPath` was unset before and after. npm moved nothing else:
```
package-lock.json | 3 ++-
package.json | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
```
```
@@ -10,7 +10,8 @@
"license": "UNLICENSED",
"dependencies": {
"@earendil-works/pi-coding-agent": "0.85.1",
- "oauth4webapi": "3.8.8"
+ "oauth4webapi": "3.8.8",
+ "yaml": "2.9.0"
}
},
```
node_modules/yaml is 2.9.0, from the pinned pi packages (package-lock.json:502 and :556).
Where it runs. `scripts/case2-persona-import.mjs` isn't in stack-copy's COPY_PATHS, and a copy carries
no node_modules of its own beyond its `npm ci`. So the script runs from a checkout at the reviewed
commit after `npm ci`, where `import "yaml"` resolves through the root package.json and
`../packages/harness/src/bundle.mjs` is beside it. Whoever runs `stage` needs read access to that
checkout and to her Hermes home, which is part of the item for Jason's go above.
## Tests
Round 1, on 213 r4 2b3e67e36, slot grants sage-1851 and sage-1863. One `node --test` per file.
Run 1 started at 08:19:44Z. A 600 s tool timeout on my wrapper killed it at about 08:29:45Z, inside
stack-release-external, after stack-release took 494 s. One test in this row's file failed in run 1.
Its filter `includes("per-channel prompt")` also matched the "profile chet: no per-channel prompts"
line that the next assert checks on its own. That was a test defect, and the script's output was
right. The filter is now `includes(": per-channel prompt ")`. The kill orphaned one fixture
holder.cjs, which I killed, and I removed its tmp dirs. Sage counts this as one failure under mos-0823.
The rerun, 08:31:11Z to 08:33:31Z, covered this row's file, stack-release-external, stack-upgrade
and the fixture dry run. The 15 files that run 1 finished green stand on code the fix didn't touch. Their counts come from run 1.
| File | pass | fail |
|---|---|---|
| packages/business/tests/launch-settings.test.mjs | 16 | 0 |
| packages/cli/tests/stack-copy.test.mjs | 16 | 0 |
| packages/cli/tests/stack-release.test.mjs | 195 | 0 |
| packages/conversation/tests/cohort.test.mjs | 37 | 0 |
| packages/harness/tests/bundle.test.mjs | 22 | 0 |
| packages/harness/tests/claude-gate.test.mjs | 3 | 0 |
| packages/harness/tests/claude-session.test.mjs | 8 | 0 |
| packages/harness/tests/creative.test.mjs | 7 | 0 |
| packages/harness/tests/mcp-server.test.mjs | 5 | 0 |
| packages/harness/tests/pi-session.test.mjs | 8 | 0 |
| packages/harness/tests/skills.test.mjs | 10 | 0 |
| packages/plugins/tests/harness.test.mjs | 9 | 0 |
| packages/seat/tests/sandbox.test.mjs | 10 | 0 |
| packages/webui/tests/editor.test.mjs | 5 | 0 |
| packages/webui/tests/pinned-copy.test.mjs | 10 | 0 |
| packages/cli/tests/case2-persona-import.test.mjs | 11 | 0 |
| packages/cli/tests/stack-release-external.test.mjs | 25 | 0 |
| packages/cli/tests/stack-upgrade.test.mjs | 20 | 0 |
Total over the 18 files: 417 pass, 0 fail, 0 cancelled, summed by awk.
The dry run on the fixture, `stage --hermes-root packages/cli/tests/fixtures/case2-persona-import/hermes
--pair default=Bean --pair chet=Chet --dry-run`, exited 0 and printed:
```
files 2
profile default: lines 3, lines starting "# " or "## " 0, lines naming a Hermes tool or channel 0
profile chet: lines 2, lines starting "# " or "## " 0, lines naming a Hermes tool or channel 0
```
## Rebase onto 213's landing (sage-1925)
213 landed as 361ba2387, with the same `git patch-id --stable` as r4 2b3e67e36, f313e69b6f7e. The
refactor tip was 62ddc96cc. I cherry-picked 2d7a99aee onto it under the new branch gosalyn/c2-20-r2 as
48f044ccc. The patch-id is eb094ae783e9 before and after. None of the 22 paths changed between 2b3e67e36
and 62ddc96cc. The fence export from sage-1836 Q1 was already in the commit: `personaTextFault` from
packages/harness/src/bundle.mjs, called by the script. I checked the test list again on the new base by
grepping tests and scripts/test-*.sh for each non-test path's basename. The new hits name other packages'
package.json files or write lockfiles in temp dirs, so the 18 files stand. 231 landed after that as
29ce1e360. It touches only packages/library and the queue-231 packet, none of these 22 paths.
Rerun on 48f044ccc, slot grant sage-1932, 10:18:09Z to 10:30:14Z, after a busy check. All 18 files ran
whole, one `node --test` each, every rc 0: 419 pass, 0 fail, 0 cancelled, summed by awk. That's two more
than the 417 above, both from the base. stack-release went from 195 to 196 and stack-upgrade from 20 to 21,
and rows 238 and 240 changed those test files. Every other file has the count in the table above. The dry
run printed the same three lines, rc 0.
+104
View File
@@ -0,0 +1,104 @@
# Row 230: C2-13a unit render and host precheck
Gitea #1732. Vikunja #416 (track T2). Brief: docs/plans/2026-10-20_case2-rows.md, section "C2-13a",
blob f6b4933f0f47 (01b40d357, sage-1562). Base: 00d4cc1f6 (row 190). Reviewer: Webby. Gate: the cli
suite plus every scripts/test-*.sh.
## What ships
`scripts/case2-unit.sh` renders `[email protected]` and `mosaic-webui.service` from the two existing
templates into the system unit directory, for the default instance only. It writes each unit through
a temp file at mode 0644, runs `daemon-reload`, enables both units, starts the bus and then the
Console. If the bus fails to start, the script refuses and leaves the Console unstarted. Both units
get `WantedBy=multi-user.target`, and neither gets `After=` or `Wants=`. The script never links a unit
file into place. A re-install refuses unless each installed unit passes the same checks, both units
name one copy, and each equals the render apart from the copy id.
`--profile library` (the default) refuses `--pm`, `--claude`, `--tester-port` and `--sso`, and needs
the preview proxy flags plus the proof pair. `--profile factory` refuses the six proxy flags and takes
`--pm` and `--claude`. Both refuse `--checkout` and `--board`.
`scripts/case2-precheck.sh --business ID [--profile factory|library]` runs as `<user>`. It prints one
`ok` or `REFUSED` line per check and exits 1 if any check refused. It prints variable names only,
never values.
`packages/cli/README.md:713` now says nothing creates the session env file, only the Case 2 precheck
reads it, and the precheck prints only variable names (webby-0034 N2, webby-0035).
## Deviations for review
Sage accepted all six for the build (sage-1570). Webby rules on each.
1. The precheck takes `--business ID`. The brief names no other way to find the business file.
2. A run root `<dataRoot>/launches/<business>` that doesn't exist passes, with a note that the first
launch creates it at 0700 (session.mjs:47 and newRun). A symlink refuses, and so does an existing
entry that isn't a 0700 directory.
3. The claude marker check passes when the reply holds the marker and input plus cache tokens are at
most 2000. **The 2000 ceiling is unmeasured.** The tests use fakes at 40 and 9000 tokens. Measuring
it on a real claude belongs to row 195's install run, not this row.
4. A non-root test can't create a root-owned proof file. For the "proof not owned by the user" case,
the fake getent reports another uid for the user, so the user's own file reads as someone else's.
5. The unit script refuses `%h` left in any unit, rendered or installed. A system unit expands `%h`
to root's home. The render replaces every `%h`, so the test edits an installed unit to reach it.
6. With `--auth-header X-Authentik-Uid`, the allowed user must be 64 lower-case hex characters (lead
decision 101). The test covers both the refusal and the pass.
## Tests (packages/cli/tests/case2-unit.test.mjs, 30 cases after round 2)
The tests use a scratch unit directory and a scratch home, plus fake systemctl, getent, sudo, id,
node, bwrap and claude. They never touch /etc/systemd, ~/.mosaic-dev, ~/.config/mosaic-dev or a
live unit. Every precheck case asserts its output doesn't hold the fixture key.
- Unit: the clean library render, PATH for a node outside the default list and for /usr/bin/node, the
systemctl order, a failed bus start, no `systemctl link`, factory `--pm` and `--claude` rendered and
absent, re-install to another copy, edits to an installed unit, template restrictions,
EnvironmentFile placement, unknown tokens, PermissionsStartOnly and `+`/`-+` Exec prefixes,
symlinked unit paths, bad `--node`, re-install drift, a split copy pair, bad copy ids, every library
and factory flag refusal, and bad `--claude`.
- Precheck: a good host, node older than 24, more than one group, sudo succeeding, XDG_DATA_HOME,
HOME mismatch, a run root mode, a bad config, bwrap failing, the port floor at or below the Console
port, a missing Console unit, bus.tester per profile, profile-library-business, the claude check
(no route, replaces, appends, missing, the unit's MOSAIC_CLAUDE_BIN beating PATH), and the session
env file (missing, 0644, symlink, empty, emptied by a later line, quoted).
- Bind (mos-1100): inside `binds()` plus `sandboxArgv()`, a bind on port 980 gets EACCES, and a bind
on a free port of 1024 or above succeeds. The test fails if the host's floor is at or below 980.
## Verification
Round 1 singles, run before the gate: case2-unit.test.mjs passes 28/28 with no skips, and host.test.mjs (it
reads the cli README) passes 42/42. No test pins a hash of the README. No other test reads the two
scripts.
## Round 2 (webby-0046, sage-1589, sage-1590, sage-1591)
Base: origin/refactor (rebased from 00d4cc1f6; the 15 commits between touch none of the five paths
or the two unit templates). Branch gosalyn/c2-13a-r2.
B1. The precheck now has a case for each refusal Webby named: the user missing from passwd (:77),
a business file that is unparsable, missing or fails the schema (:137), a run root that is a symlink
to a 0700 directory (:151), a Console ExecStart with no --port (:179), an unreadable port floor
(:180), a claude reply without the marker (:223) and a claude reply that isn't JSON (:224). The fake
claude gained `nomarker` and `text` kinds. The no-claude-code case and the missing-claude case share
a PATH without `/usr/bin/claude` (`pathWithoutClaude`), so the no-claude-code case now shows a host
with no claude passes.
N1 (ruled in for this round). Before writing, `case2-unit.sh` asks `systemctl is-active` for both
units. For each unit that was running, it prints that the unit is still on its old copy, never
"started", and then that the bus and Console must restart together to reach the new copy. A unit
that wasn't running gets "started ... on copy". A fresh install prints the round 1 line. The script
still restarts nothing. The new test covers both units running and only the Console running.
N4. The README line at :713 is rewrapped.
Mutants, run in Sage's slot on a scratch worktree, one branch each: :77, :137, :151, :179, :180,
:223, :224, a precheck that refuses with no claude on PATH (for the no-claude-code case), and an
install that prints "started" for a running unit (sage-1591).
## Carry items for row 195
- N1's restart. Row 195 restarts the bus and Console together after a re-install over running
units, as the install line now asks.
- N2. A system drop-in check like `bus-service.sh:122-145`, so a drop-in under
`[email protected]/` or `mosaic-webui.service.d/` can't change what the checked unit runs.
- N3. The precheck finds claude on the shell PATH. The bus unit's `PATH=` is what a session uses
when `MOSAIC_CLAUDE_BIN` is unset, so the check should search that PATH.
+96
View File
@@ -0,0 +1,96 @@
# Row 231 build notes, C2-01b survey escapes, list item lines and attachment embeds (#1733)
Brief: docs/plans/2026-10-20_case2-rows.md "C2-01b survey escapes, list item lines and attachment embeds",
blob 16847e94ae17. These are Webby's row 212 notes N1 to N3 (webby-0041), split out by sage-1517.
## Base
First built as 777f7358a on 212's first round bdf18272a. 212 landed as ee7b2f137 (sage-1886), so the
commit was cherry-picked onto refactor fefd54e13 under the new branch gosalyn/c2-01b-r2 as 3647a5a93.
`git patch-id --stable` is 45911915a9ee before and after. packages/library is the same in bdf18272a
and in 212's landed round a2c72ac95, so nothing the commit touches moved under it.
## What changed
- N1, `packages/library/src/survey.mjs`: JSON strings escape C0 controls but leave DEL, C1 and bidi
controls raw. The new `quote()` escapes those too, and folder names print through it.
- N2, `packages/library/src/reader.mjs`: front matter parsing records each block list item's line, and
a link in a block list carries its item's line instead of its key's.
- N3, `links.mjs` exports `isAttachment` through `index.mjs`. A wikilink or embed whose target's last
segment ends in an extension other than `.md` is an attachment. The reader neither resolves nor
indexes it, and it raises no problem. The survey prints a count of body links to attachments.
- `packages/library/README.md` describes all three, and the links, reader and survey tests cover them.
## Outside readers
`packages/cli/tests/stack-copy.test.mjs:38` checks only that `packages/library/src/index.mjs` exists, and
`scripts/mosaic:41` execs `survey.mjs`. No test pins a hash of a library file.
## Tests
On 3647a5a93, slot grant sage-1889, 09:20:01Z to 09:20:14Z, after a busy check. One `node --test` per
file, every rc 0.
| File | pass | fail |
|---|---|---|
| packages/library/tests/links.test.mjs | 7 | 0 |
| packages/library/tests/mapping.test.mjs | 7 | 0 |
| packages/library/tests/reader.test.mjs | 13 | 0 |
| packages/library/tests/render.test.mjs | 11 | 0 |
| packages/library/tests/survey.test.mjs | 6 | 0 |
| packages/cli/tests/stack-copy.test.mjs | 16 | 0 |
Total: 60 pass, 0 fail, 0 cancelled, summed by awk.
## Round 2, after webby-0070 (CHANGES) and sage-1900
Webby's round 1 review is agents/webby/work/queue-231/REVIEW-r1.md at 387f04dfd, rev 2774.
Ruling sage-1900, recorded here as asked. B2: the brief stays as approved (sage-1517, case2-rows.md
C2-01b N3). Only a `![[x.ext]]` embed can be an attachment. Plain wikilinks and front matter values keep
resolving, and an unresolved one is R4. README's line "A link to a `.canvas` file or an attachment is
R4" is back as it was at fefd54e13. B1: resolve first. An embed is an attachment only when it resolves
to nothing and its target has an extension other than `.md`. So `![[Dr.Quinn]]` with Dr.Quinn.md
indexed resolves, `![[cover.png]]` gives no problem, `![[missing-note]]` is R4 and `![[note.md]]`
resolves. My own design ruling after r11 (28213ac27) also made plain links attachments. The brief
narrowed that, and the brief governs this row.
Changes:
- `links.mjs`: `isAttachment` is true only for a body embed, `link.embed === true && !link.field`,
with the extension test as before. Its comment says it goes by name, so the reader asks it only
after resolving.
- `reader.mjs`: every link resolves first. An unresolved one is R4 unless `isAttachment` holds.
- `survey.mjs`: `quote()` also escapes U+061C (Webby N1). The attachment count now covers body embeds
only, since it uses `isAttachment`.
- Webby N2, my choice of Sage's two options: the survey keeps a count by name and the README says so,
"the count of body embeds whose target has an extension other than `.md`", and that `![[Dr.Quinn]]`
counts there even when Dr.Quinn.md exists. Checking against the survey's path list would mean a second
resolver in the survey, for a count that only guides the mapping.
- Tests. links: embeds only, with `[[cover.png]]`, `[[Board.canvas]]`, `[[Dr.Quinn]]`, a front matter
value and a markdown link all false, and `![[Dr.Quinn]]` true by name. reader: the Shore case now
gives R4 for the front matter `"![[portrait.JPG]]"` (line 3), for `[[Board.canvas]]` (line 5) and for
`![[missing-note]]` (line 6). The new Dr.Quinn case has Dr.Quinn.md indexed and resolves `[[Dr.Quinn]]` in
a block list and in the body, plus `![[Dr.Quinn]]`, with no problem. survey: a U+061C folder name
prints escaped, and the attachment count is 1, since `[[map.PDF]]` is a plain link.
Round 2 tests, one `node --test` per file, each run after a busy check.
Run 1, slot grant sage-1903, 09:39:32Z to 09:39:46Z: 60 pass, 1 fail. The new Dr.Quinn reader test failed
on order alone. All three links resolved to Dr.Quinn.md with the right field, line and embed values, and
there was no problem. The reader lists body links before front matter links, and my expected array put
the `relationships` link first. I stopped and reported it (gosalyn-0098).
Run 2, after the test-only fix sage-1907 approved, which moves that entry to the end of the expected array,
09:43:42Z to 09:43:56Z. Every rc 0.
| File | pass | fail |
|---|---|---|
| packages/library/tests/links.test.mjs | 7 | 0 |
| packages/library/tests/mapping.test.mjs | 7 | 0 |
| packages/library/tests/reader.test.mjs | 14 | 0 |
| packages/library/tests/render.test.mjs | 11 | 0 |
| packages/library/tests/survey.test.mjs | 6 | 0 |
| packages/cli/tests/stack-copy.test.mjs | 16 | 0 |
Total: 61 pass, 0 fail, 0 cancelled, summed by awk.
+87
View File
@@ -0,0 +1,87 @@
# Row 247 build notes, C2-20b persona import follow-up (#1749)
Brief: docs/plans/2026-10-20_case2-rows.md "C2-20b persona import follow-up: printed keys, the dry run
fence check and the mkdir race", landed in 335bb3e30. These are Webby's row 221 notes N1, N2 and N4
(webby-0073, REVIEW-r1 a0a00630e on webby/reviews). N3 is in DEFERRED (sage-1941).
## Base
Branch gosalyn/c2-20b from 335bb3e30, which is row 221's landing 300766450 plus the brief.
## What changed
- N1, `scripts/case2-persona-import.mjs`: the per-channel prompt line prints the platform key, with its
fixed `platforms.` or `gateway.platforms.` prefix, and the channel key through `JSON.stringify`. A key
holding a newline prints as `\n` inside one quoted line. The profile name on that line is not config
text: `pairing` already holds it to `PROFILE_ID`.
- N2: the dry run calls `personaTextFault` on each persona after its count line. A fault prints as
`profile P: persona <reason>, so C2-08's fence would refuse it`, where the reason is the fixed string
`personaTextFault` returns, never persona text or a line number. With any fault the run refuses, exit
1, and writes nothing, as stage would. The header comment no longer says a fault is counted, not
refused, and now says a SOUL.md over four times the fence in bytes refuses before any count, which is
what `soul()` already did (Webby's second point under N2).
- N4: `privateDir` catches an EEXIST from its mkdir and refuses with `<path> was made by something else
between the check and mkdir`. Any other mkdir error is thrown as before, since the brief names only
EEXIST.
- Tests, `packages/cli/tests/case2-persona-import.test.mjs`:
- The per-channel prompt test expects the quoted keys.
- New: a chet config.yaml with a newline in the platform key and in the channel key prints one quoted
line, and no output line starts with the forged text.
- The dry run heading test now expects the fence fault line, exit 1 and the refusal, still with
nothing written. Its persona has two heading lines, which the fence refuses.
- New: a persona of 8001 two-byte characters, 16002 bytes and under the 32000 byte cap, makes the dry
run print the over 8000 characters fault, write nothing and exit 1. At exactly 8000 it exits 0.
- New: the mkdir race refuses by name at stage, for the staging directory, and at install, for
`personas/<business>`. Each refused directory is left empty.
- New fixture `packages/cli/tests/fixtures/case2-persona-import/mkdir-race.mjs`, an `--import` preload
like `trace.mjs`. It makes the directory named by `$PERSONA_MKDIR_RACE` just before the script's own
`mkdirSync` of that path, so the script meets a real EEXIST from the kernel, not a thrown stub.
## Known limit of N1
`JSON.stringify` escapes C0 controls, `"` and `\`, but leaves DEL, C1, U+2028, U+2029 and the bidi
controls raw. None of those ends a line on a terminal, so the forged line N1 names can't happen. A bidi
control could still reorder how the rest of that one line displays. The brief asks for `JSON.stringify`,
and Sage kept it in sage-1964 because the keys come from Jason's own Hermes config. Note for Webby: row
231's `quote()` in packages/library covers the wider set, and this row doesn't import it. If you want it,
ask for it in review.
## Mutants
One per fix, plus a second for N1 so each of its two keys has its own. Each replaces one exact string
in the script, runs the test file, then restores the script from a copy and checks its sha256.
| Mutant | Fix | Change |
|---|---|---|
| M1 | N1 | the channel key prints raw |
| M2 | N1 | the platform key prints raw |
| M3 | N2 | the dry run's refusal after a fault is skipped |
| M4 | N4 | privateDir's EEXIST branch never matches, so EEXIST is thrown raw |
## Outside readers
`git grep` over packages/*/tests/* and scripts/test-*.sh finds `case2-persona-import` only in its own
test file and in two files of its fixture tree, README.md and trace.mjs, which name the test file in a
comment. No test pins a hash of the script.
## Tests
Two slot runs, `busy.sh && run.sh` in /mnt/storage/scratch/gosalyn-r247, on the test file alone.
Run 1, grant sage-1969, 11:47:45Z to 11:48:00Z, was red: 14 tests, 12 pass, 2 fail. The fault was in the
script. Adding `JSON.stringify` to the per-channel line dropped the space after the comma, so it printed
`"-1001",29 bytes`. The two per-channel tests caught it. run.sh also went on to the mutants with a red
base, so their counts meant nothing. Sage approved both fixes in sage-1971: the space goes back, and run.sh
now exits 3 before the mutants unless the base is rc=0 with fail 0.
Run 2, grant sage-1971, 11:49:04Z to 11:49:22Z, was green: 14 tests, 14 pass, 0 fail, rc=0.
| Mutant | Line | rc | Failed | Killed by |
|---|---|---|---|---|
| M1 | 237 | 1 | 2 | the per-channel prompt test and the newline key test |
| M2 | 237 | 1 | 2 | the same two |
| M3 | 238 | 1 | 2 | the dry run fence test and the dry run heading test |
| M4 | 192 | 1 | 1 | the mkdir race test |
After the run the script's sha256 was 7031d2c0dc9bd22bc748b364a35b461fc01cb98b795032a2e0ec436f3188d4e4, the
same as the copy taken before it. The /proc scan found no process in the worktree or the scratch dir.
+432
View File
@@ -0,0 +1,432 @@
# Row 197 execution ledger
Plan: MANIFEST.md r2, approved by Webby ca6b32d66.
Build grant: sage-1390. Suites, image builds and paired proof are not granted.
Rebased five documentation commits onto origin/refactor aa1bd209e before code.
Existing worktree herb/row197 and plain node_modules symlink retained.
- T1 preflight/steps: in progress.
- T2 locks/status/recovery: pending.
- T3 image/Docker: pending.
- T4 fixtures/timing: pending.
- T5 evidence/acceptance: historical timing done; implementation pending.
Preflight interfaces: T1 RunPaths supplies T2 ownership and T3 image checks.
T2 run receipt supplies T4 lane/port state and T5 attribution. T3 image identity
is checked before Docker-bearing steps. T4 timings supply T5 pair summaries.
No interface conflict found. No product API or assertion changes are authorized.
Ruling: use this approved BUILD.md as the execution ledger, rather than adding
skill workspace paths outside the manifest. This preserves the reviewed path
boundary. Missing ledger detail would cost recovery time, not product behavior.
## Implementation checkpoint 2026-10-20T20:11:58Z
T1-T5 source is present within the approved manifest. Full acceptance is pending.
Fresh single-file checks: preflight/supervisor 6/6; state/recovery 11/11; image and
Docker guard 4/4; port leases/attribution/proof summary 3/3. No skips or failures.
Existing files checked singly: board serve; WebUI serve, preview, proof-cli,
tester-cli and service; harness pi and Claude. Stack-release R1 selected single
passed 1/1. WebUI service also passed 31/31 with lane-a leases and a 30-byte root.
All existing assert lines are unchanged. Shell syntax and diff checks pass.
Observed defects fixed during the single-file cycles: cancellation incorrectly
returned zero when a TERM handler exited zero; recovery did not supersede an
unclean terminal; status omitted the lock-before-receipt window. Evidence logs
retain the failing and passing observations. Source still has no commit because
applicable suites have not run. No image build, proof, push or live operation.
Ruling: use an instrumented single-lane author gate on a frozen dirty-source
clone before committing source. The new wrapper requires a clean candidate and
proven image receipt, while repository rules require green suites before source
commits. The author gate retains every suite and assertion, caps concurrency at
two, uses own port/state/temp/project roots, and guards Docker against builds.
Cost if wrong: the author gate fails; clean-candidate wrapper baselines and the
paired proof remain mandatory after source is verified and committed.
Ruling: publish the initial run directory atomically after the start receipt is
complete. This avoids a second lane observing a partially written start. A
crash before publication has started no test or mutable Docker operation.
Cost if wrong: a lane refuses or quarantines; no protected state is bypassed.
## Implementation review corrections, 2026-10-20T20:33Z
Webby-0029 reviewed the first implementation packet at f942bd7d4.
B1 identified missing manifest tests. B2 identified a test that leased both real
bands through a private lock namespace. The old author-gate clear was withdrawn
in herb-0019. SOURCE.json, BUILD.patch and gate.sh still describe the old frozen
clone. They are not a clear for the edited working tree. No author gate ran.
B2 correction: the helper test uses only the active lane and MOSAIC_GATE_STATE.
It compares the other band arithmetically and has no unasserted listener. The
occupied-port probe first obtains its own real lease, binds that port, and
asserts that its sentinel listener survives refusal. No test reserves another
lane's band. Exhaustion narrows only the test process's band to one owned port.
B1 coverage added within the existing approved paths. No case is dropped:
| Manifest case | Added check |
| --- | --- |
| T1 canonical and builder checkout, existing output | gate-lane.test: direct validateRun refusals |
| T1 92-byte socket and over-107-byte socket | exact design example with a distinct gt- test root; 108-byte refusal |
| T1 both candidates carry tooling | validateTooling compares each launcher file, including missing and changed cases |
| T1 duplicates and NODE_TEST_CONTEXT | repeated package/script fixture is retained; sanitized environment is exercised by both supervised children |
| T2 one-winner race and inherited FDs | two actual shell wrappers race; a controlled supervisor verifies held FDs, and its child checks that no FD points to either lock file |
| T2 same checkout and legacy claim refusal | activeConflicts uses synthetic process observations with a live lane and a sibling landing gate |
| T2 surviving child and interrupted recovery | locked recovery kills a child proven by the private root, preserves a foreign child, is killed during fake Docker cleanup, leaves quarantine, and resumes under a new fixture grant |
| T3 missing receipt and missing image | verifyImage refuses absent provenance and failing inspection |
| T3 tag changed mid-gate and at gate end | runGate now uses the same executePinnedSteps boundary exercised by the tests; the next step never runs after a changed ID |
| T3 mutable-tag rebuild | fake preparation returns a new image ID with identical input digest; the old receipt refuses it |
| T3 cold and warm counters | preparation records one build and its time; warm execution records zero builds; repeated verification never builds |
| T4 parallel test-file leases | two generated Node test files hold ports in the same lane/state; locks are checked during child execution and shutdown |
| T4 exhausted band, occupied port and foreign listener | a one-port process-local band refuses both a held lease and an existing sentinel listener; the listener survives |
| T4 owner death | a killed allocator releases only its own holder; a foreign lease remains held |
| T4 lease through child shutdown | parent probes the lock while the fixture test runs and after its child has stopped |
| T4 fixtureCacheEnv | replacement child environment writes only under separate tmp, jiti and cache directories |
| T5 unrelated session append | a separate process waits for a running-step barrier, appends under a fixture HOME, and remains unrelated in the resulting metadata attribution |
N1 removes the unused closed binding. All leases reserve an unbound endpoint;
existing closed options need no call-site change. N2 makes child TMPDIR a tmp
subdirectory. N3 normalizes ss interface scopes before checking port 9. N4 uses
gt- for unit run roots. N5 documents the canonical node_modules dependency.
N6 is recorded: row 200 also edits stack-release.test.mjs; the second landing
must rebase and rerun that file singly. Sage was told in herb-0019.
Before Sage's hold, expanded preflight tests passed 11/11, state tests 14/14,
image tests 7/7, and the pi/Claude singles passed 8/8 each with separate tmp
subdirectories. Later source/test edits still need verification. In particular,
the new parallel test-file probe had a cleanup-order defect and is unverified.
Its root cleanup ran before child shutdown. Cleanup now stops children before
removing their root. Output is retained for a useful failure report. The initial
FD probe also assumed Node did not reopen FD numbers; it now checks the actual
lock targets. The early passing fixture file covered child allocators, before
they were changed to actual generated test files. It is not acceptance of the
latest file.
Sage-1448 stopped all node --test runs at 20:30Z for Rocko's browser slot.
The remaining owned process tree was stopped at 20:30:32Z. Inspection found no
remaining owned test processes afterward. Edits and static inspection continue.
20 module syntax checks, shell syntax and git diff checks pass. Existing-file
assertion lines remain identical to HEAD. No test is run during the hold.
Next: after Sage passes Rocko's clear, verify the corrected files singly,
including a lane-env helper-file check. Then refresh the frozen clone and all
three packet artifacts, send their hashes, and request a new author-gate clear.
Suites, image builds and paired proofs still need their own grants. No source
commit, push, suite, real Docker build or paired proof has run in this phase.
## Refrozen review packet, 2026-10-20T20:41Z
Sage-1454 lifted the single-file hold. Fresh runs passed without failures or
skips: review-t1-resume.log 11/11, review-t2-resume.log 14/14,
review-t3-resume.log 8/8, review-t4-resume.log 8/8. The helper file also passed
8/8 with lane-a environment in review-t4-lane-a.log. Its retained state had only
an a port-lock namespace, no b directory, and every lock was released. Generated
fixture Node test entries explicitly use --test-concurrency=2. The child-test
probe now finishes and cleans up before removing its root. All 41 new tests pass.
B1 and B2 fixes are ready for Webby's review. No manifest case was dropped.
All 25 source paths were copied to a new frozen clone and compared by mode and
SHA-256. This supersedes the earlier withdrawn source packet:
- SOURCE.json: 91ba149f2fc039a8bed0e6d1ad5da44b80fb8cbec84d0fa4e38616040138596b
- gate.sh: 48e06e23e45d3484755f8cd79ad5b6dfde5ef4f960081e7c455a2540e707fed1
- BUILD.patch: 0b3871bd9848ca223c7cda50266debc04643e5d986eb7f0fece096aaf25ff43e
- Frozen checkout: /mnt/storage/scratch/mg197-a-d22dacd92735/wt
- Output: /mnt/storage/scratch/mg197-a-d22dacd92735/out
- Temporary root: /mnt/storage/scratch/ga-VPXhG7 (30 bytes)
- Compose project: mg197-a-d22dacd92735
After an explicit author-gate grant, run this command with Sage's actual ID:
```sh
env MOSAIC_GATE_GRANT=ACTUAL_SAGE_GRANT_ID bash /home/jwoltje/.t3/worktrees/mosaic-stack/herb-row197/agents/herb/work/queue-197/gate.sh > /mnt/storage/scratch/mg197-a-d22dacd92735/out/gate.log 2>&1
```
No gate or suite has started. Queue order from sage-1454 is 192, 201, 204,
200, Gander, then 197. No branch deletion is authorized or attempted. Any later
rebase uses a new branch name, and pr-merge.sh -d stays off under #426(a).
Whichever of rows 197 and 200 lands second must rerun stack-release singly.
Clean-candidate wrapper baselines, image preparation and paired proofs remain
separately gated acceptance work. There is still no source commit or push.
## Implementation approval and final notes, 2026-10-20T20:47Z
Webby-0031 approved r2 in REVIEW-impl-r2.md at 1f404c973. Their static review
verified every source hash/mode and patch result, and all B1/B2 cases. No queue
review request comment exists yet; Sage owns creating it if needed.
Folded all three optional notes into the same approved paths. The helper file's
standalone fallback now shares /mnt/storage/scratch/mosaic-gate-lanes with real
lane-a leases and leaves its stable lock files in place. Band assertions use the
imported BANDS. Both T5 polling children fail after five seconds if their barrier
is missed. README records the shared standalone namespace and deadlines.
Fresh single-file logs review-r3-fixtures.log and review-r3-lane-a.log both pass
8/8 with zero skips. The lane-a run has no b lock directory, and every lock was
released. Syntax and diff checks pass. No existing test assertions or timeouts
changed. Remaining source matches the 11/14/8-file checks from r2.
Refreshed all 25 source rows in the existing frozen checkout. New digests replace
the preceding r2 packet. gate.sh and its command are unchanged:
- SOURCE.json: 3584763642506e81a532c839bdd07b111d25daa87e35dbe441d8e8c01a2c2aab
- gate.sh: 48e06e23e45d3484755f8cd79ad5b6dfde5ef4f960081e7c455a2540e707fed1
- BUILD.patch: 55fe390da17ab926a0593d39e79561abe52756a1b1a2c756ec57f45d90f10fde
Author gate remains unstarted and queued for Sage's explicit grant. No source
commit, suite, image build, paired proof, push or branch deletion ran.
## R3 approval receipt
Webby-0033 approved r3 in REVIEW-impl-r3.md at a5643f9bd. Static review verified
the three packet hashes, all 25 source rows, and the patch result. Their review
accepts N1-N3; no additional hang-guard test is requested. No queue review request
comment exists yet, so Webby has not recorded a queue review.
Read-only verification after receipt confirms that the approved packet hashes
and all 25 worktree/frozen source hashes and modes still match. Source and gate
script stay frozen. The author gate has not started and still needs Sage's grant.
## Queue review round 1
Sage-1476 authorized moving row 197 to in-review before the author gate.
Created CANDIDATE.sha256 from the same 25 reviewed SOURCE.json rows. Its digest
is 6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3.
Ran the queue move from the canonical checkout with op herb-197-review-r3.
The move succeeded at revision 2395, opening round 1 on #1695. The request post
failed before send because MOSAIC_GITEA_CREDENTIAL_FILE is not set; revision 2396
records the failed outcome. Exit code 1 describes the failed post, not a failed
row transition. Read-back confirms state in-review and the correct candidate.
Sage will post as jarvis and give Webby the request comment number. No credential
lookup, alternate post or retry was attempted here.
SOURCE.json, BUILD.patch, gate.sh and all 25 frozen source rows remain unchanged.
Latest slot order is row 190 r2, 201, 204, 200, Gander, then Herb. No gate starts
before Sage sends an explicit grant ID.
## Follow-on assignment
Sage-1503 assigns row 215, Case 2 C2-04, to Herb after row 197 (queue rev 2455,
Gosalyn design r11). Read docs/plans/2026-10-20_case2-rows.md, C2-04 section,
at 5b8def2f1 when starting that row. Its required base is origin/refactor
305857c8f. This assignment does not replace row 197 or authorize its gate.
Keep row 197's approved source frozen while awaiting Sage's grant. Start row
215 only after row 197, using a new branch name under the existing fleet rule.
## Round 2 merge preparation on row 233
Sage-1757 replaces history replay with one candidate commit on 57f59e0c3.
The attempted history replay on herb/row197-r2-233 had already finished when
that instruction arrived. Its only conflict was the append-only BUILD-LOG tail.
That branch remains for history; none of its replayed commits is in this round.
The current branch is herb/row197-r3-233, directly from
57f59e0c399ce29b1bb85ca6af686fdc10eb195d.
Stash correction: shared refs/stash was used incorrectly. The exact entry
2767b88549dfd8c7e810908b5416876fa50cb571 is now kept on
herb/row197-r3-wip. After verifying stash@{0} still named it, only that entry was
dropped. The two other entries remain. No further stash creation, force push,
branch deletion or refs/stash mutation is planned. A separate working-copy
backup is /mnt/storage/scratch/herb197-merge-W4oiLg.
Sage-1760 requires preserving every landed hunk on every changed manifest path.
Checked each of the 25 r1 paths with git diff aa1bd209e 57f59e0c3 -- PATH.
Exactly three changed upstream. Their r2 diff against 57f59e0c3 is captured in
r2-merge-only.patch:
| Merged path | Only row 197 changes against landed base | Landed content retained |
| --- | --- | --- |
| packages/cli/tests/stack-release.test.mjs | leasePort and laneAfter imports; freePort now calls the lease helper | All landed tests, including row 233's tests after R15 and BACKUP_STORE checks |
| packages/webui/tests/service.test.mjs | leasePort and laneAfter imports; closedPort now calls the lease helper | SEND_REFUSAL permits --sso; default unit rendering removes the @CONFIG_ENV@ line |
| scripts/test-discord.sh | fixture PI_CODING_AGENT_DIR setup; optional two-worker nested Node cap | Row 190's absent scratch MOSAIC_CONFIG selection and caller-config restoration |
The other 22 paths equal the r1 reviewed source bytes and modes. r2-MERGE.json
lists every path, its r1/r2 digest, mode, upstream-change flag and equality result.
Source copies came from the hash-verified r1 frozen checkout. Original r1 digest:
6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3.
No existing assertion or timeout line differs from the landed base. Static
checks only: 20 module syntax checks, four shell syntax checks and git diff check
pass. BUILD-LOG.md equals 57f59e0c3 byte for byte and is outside this commit.
Sage explicitly requested the local candidate SHA before granting tests. This
commit is an untested preparation checkpoint, not gate evidence. No tests or
push are authorized yet. r2-TEST-FILES.txt lists the requested grant scope:
16 Node test files singly and scripts/test-discord.sh, test-foundation.sh and
test-queue.sh. The four stack-release consumers are stack-release.test.mjs,
stack-release-external.test.mjs, stack-upgrade.test.mjs and stack-instances.test.mjs.
The shared stack-release-fixture.mjs is exercised by its importing test files,
not run as a standalone test. This covers all 13 touched Node test files plus
three additional stack-release consumers and all three touched shell suites.
Current slot order from sage-1760: Gizmoduck, Launchpad, Darkwing, then Herb,
around 06:30Z. Wait for an explicit grant. After granted checks and authorized
push, build the new candidate manifest from that pushed tip and open the
merge-scoped round 2. Old SOURCE.json, CANDIDATE.sha256 and gate.sh remain r1
historical artifacts and are not a clear to run the merged candidate.
## Round 2 rebuilt on row 234
Sage-1776 names landing c771a36f4bd0b6c359282425c668996a90072f93.
Built a new branch, herb/row197-r4-234, directly from that landing. No history
replay, stash, force push, branch deletion, test run or push is involved.
The prior candidate a3b7cbde8 remains untested and is not the queued candidate.
Checked git diff --name-only 57f59e0c3 c771a36f4 against all 25 source paths.
Only packages/cli/tests/stack-release.test.mjs overlaps; the other 24 do not.
The full comparison is in r2-on234-OVERLAP.txt. The stack-release file starts
from the landing and keeps both T46 untimed entries, host_grants and hosts.
Its only changes against the landing are the leasePort and laneAfter imports
and freePort's call to the lease helper. r2-on234-merge-only.patch records those
hunks. Existing assertions and timeouts match the landing exactly.
Every other source path equals a3b7cbde8 byte for byte and retains its mode.
This includes the previously merged service and test-discord files, preserving
all landed --sso, CONFIG_ENV and scratch-config handling. The earlier evidence
r2-MERGE.json still records those three pre-234 merge cases. The new
r2-on234-MERGE.json records each current digest, prior digest, mode and equality
result. BUILD-LOG.md equals the new landing byte for byte and is excluded.
Static check of the changed Node file and the candidate diff pass. No tests
were run. The scope stays r2-TEST-FILES.txt: 16 Node files singly plus the three
shell suites. No manifest or queue round is rebuilt until granted checks and
authorized push. Old SOURCE.json, CANDIDATE.sha256 and gate.sh remain historical
r1 evidence. The test slot follows Darkwing 240 and needs Sage's explicit grant.
## Round 6 preparation (sage-1883, mos-1162)
New branch herb/row197-r6 starts at fefd54e13, after the standalone row
212 landing ee7b2f137. No r5 manifest path changed between e7078e6a8 and
this base; all 32 r5 blobs and modes were copied intact before the two
umask changes. No history replay, stash, force push or branch deletion.
executeSteps now launches each argv through /bin/bash with umask 022
and exec, preserving the process group and argv boundaries. The supervisor
and gate-lane.sh retain 077, including state, locks, private logs and roots.
New gate-lane tests assert the actual child umask and created file mode,
the unchanged parent mask, private log mode, and actual wrapper-created
state/lock modes. No shell, image preparation, credentials test or other
existing assertions changed. process.test.mjs equals the landed base;
lane-image.mjs equals r5. N7 remains DEFERRED at fefd54e13; rows 243 and
244 own the explicit chmod and PI_AUTH_FILE fixtures respectively.
Read Sage's permitted failed pkg-bus log: process.test startup token refusal
saw ready.ok true, consistent with inherited 077 narrowing its fixture mode.
No further diagnostic read of the retained ga-8I3iZc root is needed.
No r6 tests or probes have run. Source commit, push and new manifest await
the explicit granted checks. r6-OVERLAP.json records the empty overlap.
Sage-1892 focused grant: gate-lane.test whole 13/0, process.test whole
through executeSteps 8/0, zero skips/cancellations. Syntax checks passed.
All new umask/mode assertions passed. No Docker, image, lane or proof run.
Empty /proc cwd/argv scan and removal recorded in r6-granted/cleanup.json;
clear herb-0068 at 09:21Z. The source matches r5 on 29 of its 32 paths,
including modes. Only BUILD.md, gate-lane.test.mjs and gate-lane.mjs differ.
The new r6-OVERLAP.json records the base comparison. No process test edit.
Sage-1895 authorizes one source commit and seat-only push after range check.
## Round 7 preparation (sage-1951, sage-1955)
The r6 proof passed all package suites with the original process.test,
then failed script-release. The guard injected --no-build into compose run;
Compose 5.5.1 rejects it before container creation. The old unit test only
asserted the same invalid argv, so it could not detect this compatibility
error. This supersedes no-build-flag claims in the historical packet.
herb/row197-r7 starts at b1fece6f0. No r6 manifest path changed between
fefd54e13 and this base; all 33 r6 blobs/modes were copied intact before
r7 tests were authored. No history replay, stash, force push or deletion.
Sage-1955 accepts the private tempRoot/compose-no-build.yaml override,
mode0600, with build: !reset null and image pinned to the receipt's imageId.
The guard owns the -f arguments and forces run --pull never --no-deps.
Caller-supplied Compose config/build/pull options remain refused.
The existing proof removes successful temp roots and retains failed roots;
the override stays within that same cleanup ownership.
Tests first: installed-binary run --help before SERVICE checks parser
acceptance without starting a container. A separate compose config --format
json check uses the actual candidate compose.yaml, made-up env/auth paths,
and an empty Docker config directory, asserting no build key and the exact
pinned image. The no-build-restoration mutant must fail that parser test.
No test, probe or Docker command has run on r7; waiting for Sage's grant.
Primary references: Docker's merge reference documents !reset null:
https://docs.docker.com/reference/compose-file/merge/#reset-value
Docker's run reference documents --pull and --no-deps, with no --no-build:
https://docs.docker.com/reference/cli/docker/compose/run/
Installed 5.5.1 acceptance and merged configuration are still unverified.
Sage-1956 focused grant, 2026-10-21 11:19-11:21Z: busy check CLEAR.
Whole gate-lane-image.test on the r6 guard: 9 pass, 2 fail. The installed
Compose parser emitted unknown flag: --no-build; the other expected failure
was the missing override helper. No cancellation or skip. After the fix,
whole file 11/0. The committed r7-no-build.mutant.patch restores the invalid
flag. Applied only for the installed parser regression: 0/1 with the same
unknown-flag error, killed. Restored source and whole file passed 11/0 again.
Both approved node --check commands passed. Empty /proc cwd/argv scan;
clear herb-0073 at 11:21:04Z. No container start, image build, lane, proof or
full suite ran. Logs/counts/scan are in r7-granted/ (local run evidence).
The guard supplies both absolute -f paths, suppressing automatic override
selection, pins receipt imageId, removes build via !reset null and adds
--pull never --no-deps. Its main entry publishes the override before Docker
runs. A unique private staging file plus hard-link publication prevents
partial writes or replacement under concurrent guard calls; existing bytes,
mode0600, file type and ownership must match, with O_NOFOLLOW on reads.
The file stays under the run's short private tempRoot for existing proof
cleanup. Caller Compose options remain allowlisted and config/build/pull
options refuse. The actual installed Compose config test reports no build
key and the exact image ID; parser help is before SERVICE, so no container
operation can run. Fixtures supply made-up env/auth paths and empty Docker
configuration. Existing image provenance, cleanup and umask behavior remain.
Range check: one source commit on b1fece6f0, same 33 r6 paths plus the
mutant patch and r7 overlap receipt (35 paths). Thirty r6 paths retain the
same blob/mode. Only lane-docker.mjs, gate-lane-image.test.mjs and BUILD.md
differ from r6. process.test and BUILD-LOG equal the landing. No force push,
branch deletion, history replay or other source edits. Sage-1956 authorizes
the source commit and seat-only push; fresh proof remains Sage's action.
## Round 8 preparation (sage-1979)
The r7 D5 proof passed business and bus, then failed the two installed
Compose probes in pkg-cli. Their subprocess fixture retained the lane's
shim-leading PATH but omitted MOSAIC_GATE_DOCKER_RECORD, so the shim read
an undefined path. This is a test context error; the production guard stays
byte-identical to r7.
New herb/row197-r8 starts at 5c6f73ed7. None of r7's 35 manifest paths
changed from b1fece6f0 to this base. Tests choose the record's real absolute
Docker path when a lane record is set; outside lanes they keep using docker.
Only the two read-only help/config probes use this helper. The subprocess
env still holds fixture-only configuration; the record need not propagate
to it. No run or policy allowlist is widened.
Sage-1979 grants 215's 16 singles first, then r8's normal and lane-shaped
shim-leading PATH whole-file checks and the record-lookup-removal mutant.
The shim fixture invokes the actual unchanged guard, with a private made-up
record naming the real Docker binary. No image build, container start, lane
or proof is part of r8 checks. No r8 checks have started yet.
Sage-1979 r8 checks followed 215's early singles, then clear herb-0078 at
2026-10-21 12:09:28Z. Whole gate-lane-image.test normal PATH11/0; whole
actual guard-shim-leading PATH plus private record11/0. The committed
r8-record-lookup.mutant.patch removes the lookup: installed-Compose argv
regression0/1, killed with the original Received undefined path error.
Restored code, repeated whole shim-PATH file11/0. All zero skips/cancellations.
Logs/counts/fixture metadata/empty /proc cwd+argv scan are in r8-granted/.
Both 215 and r8 private roots were removed after an empty process scan.
No image build, container start, suite, lane or proof ran for r8.
Range: one commit on5c6f73ed7. Of r7's35 paths,33 keep the same blob/mode;
only gate-lane-image.test and this BUILD.md change. The r8 mutant and overlap
receipt are added. The production guard and all other tooling match r7.
BUILD-LOG and process.test equal the landing. No history replay, force push,
branch deletion, live state change or policy widening. Fresh proof remains
Sage's action after Webby's merge review.
+207
View File
@@ -0,0 +1,207 @@
{
"r1Base": "aa1bd209e",
"r1Candidate": "6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3",
"r2Base": "57f59e0c399ce29b1bb85ca6af686fdc10eb195d",
"rows": [
{
"path": "packages/cli/tests/fixtures/gate-lane-child.mjs",
"r1Sha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
"r2Sha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/cli/tests/gate-lane-fixtures.test.mjs",
"r1Sha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
"r2Sha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/cli/tests/gate-lane-image.test.mjs",
"r1Sha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
"r2Sha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/cli/tests/gate-lane-state.test.mjs",
"r1Sha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
"r2Sha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/cli/tests/gate-lane.test.mjs",
"r1Sha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
"r2Sha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/cli/tests/stack-release.test.mjs",
"r1Sha256": "8ca733a600d5f5534d7e14d8533e1b16d710ebd43080ef8421238b5dd145fade",
"r2Sha256": "b95c0c125b6a283961e24adc07d43150bcbd05d2f11005180b836043015c52a6",
"mode": "0o644",
"landedPathChanged": true,
"r1BlobEqual": false
},
{
"path": "packages/control-board/tests/serve.test.mjs",
"r1Sha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
"r2Sha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/harness/tests/claude-session.test.mjs",
"r1Sha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
"r2Sha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/harness/tests/pi-session.test.mjs",
"r1Sha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
"r2Sha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/webui/tests/preview.test.mjs",
"r1Sha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
"r2Sha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/webui/tests/proof-cli.test.mjs",
"r1Sha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
"r2Sha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/webui/tests/serve.test.mjs",
"r1Sha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
"r2Sha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "packages/webui/tests/service.test.mjs",
"r1Sha256": "c915239f0c223209baf84c96ec9f5ae2845fce7dd98ad46b073a96033aa7d6df",
"r2Sha256": "845313239547ddbc478090b29f96385f0b8853e5087c572393be328e7075f578",
"mode": "0o644",
"landedPathChanged": true,
"r1BlobEqual": false
},
{
"path": "packages/webui/tests/tester-cli.test.mjs",
"r1Sha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
"r2Sha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/test-discord.sh",
"r1Sha256": "c10af53b0717344026cb22462964f253c7401c5082ac6ef3722a89d9dfa5c661",
"r2Sha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
"mode": "0o755",
"landedPathChanged": true,
"r1BlobEqual": false
},
{
"path": "scripts/test-foundation.sh",
"r1Sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"r2Sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"mode": "0o755",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/test-queue.sh",
"r1Sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"r2Sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"mode": "0o755",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/README.md",
"r1Sha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
"r2Sha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/gate-lane.mjs",
"r1Sha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
"r2Sha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/gate-lane.sh",
"r1Sha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
"r2Sha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
"mode": "0o755",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/lane-docker.mjs",
"r1Sha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
"r2Sha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/lane-evidence.mjs",
"r1Sha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
"r2Sha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/lane-fixtures.mjs",
"r1Sha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
"r2Sha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/lane-image.mjs",
"r1Sha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
"r2Sha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
},
{
"path": "scripts/testing/lane-state.mjs",
"r1Sha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
"r2Sha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
"mode": "0o644",
"landedPathChanged": false,
"r1BlobEqual": true
}
]
}
@@ -0,0 +1,19 @@
packages/cli/tests/gate-lane-fixtures.test.mjs
packages/cli/tests/gate-lane-image.test.mjs
packages/cli/tests/gate-lane-state.test.mjs
packages/cli/tests/gate-lane.test.mjs
packages/cli/tests/stack-instances.test.mjs
packages/cli/tests/stack-release-external.test.mjs
packages/cli/tests/stack-release.test.mjs
packages/cli/tests/stack-upgrade.test.mjs
packages/control-board/tests/serve.test.mjs
packages/harness/tests/claude-session.test.mjs
packages/harness/tests/pi-session.test.mjs
packages/webui/tests/preview.test.mjs
packages/webui/tests/proof-cli.test.mjs
packages/webui/tests/serve.test.mjs
packages/webui/tests/service.test.mjs
packages/webui/tests/tester-cli.test.mjs
scripts/test-discord.sh
scripts/test-foundation.sh
scripts/test-queue.sh
@@ -0,0 +1,45 @@
diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs
index 8788158c3..a78f52062 100644
--- a/packages/cli/tests/stack-release.test.mjs
+++ b/packages/cli/tests/stack-release.test.mjs
@@ -0,0 +1,2 @@
+import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
+import { after as laneAfter } from 'node:test';
@@ -1777 +1779 @@ const TESTER='7499';
-const freePort=async()=>{const s=createServer();s.listen(0,'127.0.0.1');await once(s,'listening');const p=String(s.address().port);await new Promise((r)=>s.close(r));return p};
+const freePort=async()=>String(await leasePort({after:laneAfter},{closed:true}));
diff --git a/packages/webui/tests/service.test.mjs b/packages/webui/tests/service.test.mjs
index a6e66608a..adb460d07 100644
--- a/packages/webui/tests/service.test.mjs
+++ b/packages/webui/tests/service.test.mjs
@@ -0,0 +1,2 @@
+import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
+import { after as laneAfter } from 'node:test';
@@ -48,9 +50 @@ function filled(flags, root = REPO) {
-async function closedPort() {
- const server = createServer();
- server.listen(0, '127.0.0.1');
- await once(server, 'listening');
- const { port } = server.address();
- server.close();
- await once(server, 'close');
- return port;
-}
+async function closedPort() { return leasePort({after:laneAfter}, {closed:true}); }
diff --git a/scripts/test-discord.sh b/scripts/test-discord.sh
index 114994936..4daf0d143 100755
--- a/scripts/test-discord.sh
+++ b/scripts/test-discord.sh
@@ -73,0 +74,2 @@ TOOLS_JSON="{\"roots\":[{\"name\":\"docs\",\"path\":\"$SANDBOX/toolroot\"}],\"ma
+export PI_CODING_AGENT_DIR="$SANDBOX/pi-agent"
+mkdir -p "$PI_CODING_AGENT_DIR"
@@ -145 +147,8 @@ fi
-node_tests() { env -u NODE_TEST_CONTEXT node --test "$@"; }
+node_tests() {
+ local lane_concurrency=()
+ if [ -n "${MOSAIC_TEST_CONCURRENCY:-}" ]; then
+ [ "$MOSAIC_TEST_CONCURRENCY" = 2 ] || return 2
+ lane_concurrency=(--test-concurrency=2)
+ fi
+ env -u NODE_TEST_CONTEXT node --test "${lane_concurrency[@]}" "$@"
+}
@@ -0,0 +1,235 @@
{
"base": "c771a36f4bd0b6c359282425c668996a90072f93",
"priorCandidate": "a3b7cbde8ef51c206a83769a7c272e4f9a1de5bc",
"originalCandidate": "6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3",
"overlap": [
"packages/cli/tests/stack-release.test.mjs"
],
"rows": [
{
"path": "packages/cli/tests/fixtures/gate-lane-child.mjs",
"r1Sha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
"priorCandidateSha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
"newSha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/cli/tests/gate-lane-fixtures.test.mjs",
"r1Sha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
"priorCandidateSha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
"newSha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/cli/tests/gate-lane-image.test.mjs",
"r1Sha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
"priorCandidateSha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
"newSha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/cli/tests/gate-lane-state.test.mjs",
"r1Sha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
"priorCandidateSha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
"newSha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/cli/tests/gate-lane.test.mjs",
"r1Sha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
"priorCandidateSha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
"newSha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/cli/tests/stack-release.test.mjs",
"r1Sha256": "8ca733a600d5f5534d7e14d8533e1b16d710ebd43080ef8421238b5dd145fade",
"priorCandidateSha256": "b95c0c125b6a283961e24adc07d43150bcbd05d2f11005180b836043015c52a6",
"newSha256": "071ea2803b8c7e0450bb4c86ac9b49ae7c180af1599dca52bfce6d0274ca205b",
"unchangedFromPrior": false,
"overlaps234": true,
"mode": "0o644"
},
{
"path": "packages/control-board/tests/serve.test.mjs",
"r1Sha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
"priorCandidateSha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
"newSha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/harness/tests/claude-session.test.mjs",
"r1Sha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
"priorCandidateSha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
"newSha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/harness/tests/pi-session.test.mjs",
"r1Sha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
"priorCandidateSha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
"newSha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/webui/tests/preview.test.mjs",
"r1Sha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
"priorCandidateSha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
"newSha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/webui/tests/proof-cli.test.mjs",
"r1Sha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
"priorCandidateSha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
"newSha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/webui/tests/serve.test.mjs",
"r1Sha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
"priorCandidateSha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
"newSha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/webui/tests/service.test.mjs",
"r1Sha256": "c915239f0c223209baf84c96ec9f5ae2845fce7dd98ad46b073a96033aa7d6df",
"priorCandidateSha256": "845313239547ddbc478090b29f96385f0b8853e5087c572393be328e7075f578",
"newSha256": "845313239547ddbc478090b29f96385f0b8853e5087c572393be328e7075f578",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "packages/webui/tests/tester-cli.test.mjs",
"r1Sha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
"priorCandidateSha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
"newSha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/test-discord.sh",
"r1Sha256": "c10af53b0717344026cb22462964f253c7401c5082ac6ef3722a89d9dfa5c661",
"priorCandidateSha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
"newSha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o755"
},
{
"path": "scripts/test-foundation.sh",
"r1Sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"priorCandidateSha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"newSha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o755"
},
{
"path": "scripts/test-queue.sh",
"r1Sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"priorCandidateSha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"newSha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o755"
},
{
"path": "scripts/testing/README.md",
"r1Sha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
"priorCandidateSha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
"newSha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/gate-lane.mjs",
"r1Sha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
"priorCandidateSha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
"newSha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/gate-lane.sh",
"r1Sha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
"priorCandidateSha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
"newSha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o755"
},
{
"path": "scripts/testing/lane-docker.mjs",
"r1Sha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
"priorCandidateSha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
"newSha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/lane-evidence.mjs",
"r1Sha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
"priorCandidateSha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
"newSha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/lane-fixtures.mjs",
"r1Sha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
"priorCandidateSha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
"newSha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/lane-image.mjs",
"r1Sha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
"priorCandidateSha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
"newSha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
},
{
"path": "scripts/testing/lane-state.mjs",
"r1Sha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
"priorCandidateSha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
"newSha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
"unchangedFromPrior": true,
"overlaps234": false,
"mode": "0o644"
}
]
}
@@ -0,0 +1,47 @@
Compared 57f59e0c3 to c771a36f4bd0b6c359282425c668996a90072f93
agents/gizmoduck/work/queue-234/BUILD.md
docs/SESSIONS.md
packages/bus/README.md
packages/bus/schema.sql
packages/bus/src/broker.mjs
packages/bus/src/credentials.mjs
packages/bus/src/host-grant.mjs
packages/bus/src/host-run.mjs
packages/bus/src/migrations/3i-3j.sql
packages/bus/src/runtime.mjs
packages/bus/src/server.mjs
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/fixtures/schema-3i.sql
packages/bus/tests/grant-enroll.test.mjs
packages/bus/tests/grant-migration.test.mjs
packages/bus/tests/hard-waits.test.mjs
packages/bus/tests/host-fixtures.mjs
packages/bus/tests/host-run.test.mjs
packages/bus/tests/host-tools.test.mjs
packages/bus/tests/name-migration.test.mjs
packages/bus/tests/project-runtime.test.mjs
packages/bus/tests/schema.test.mjs
packages/bus/tests/spec-fixtures.mjs
packages/bus/tests/spec-migration.test.mjs
packages/bus/tests/thread-migration.test.mjs
packages/bus/tests/usage-migration.test.mjs
packages/business/src/business.mjs
packages/business/src/hosts.mjs
packages/business/tests/hosts.test.mjs
packages/cli/README.md
packages/cli/src/cli.mjs
packages/cli/src/hosts.mjs
packages/cli/tests/hosts.test.mjs
packages/cli/tests/stack-release.test.mjs
scripts/mosaic
tools/host/mosaic-keys
tools/host/mosaic-keys-write
tools/host/mosaic-keys.sshd.conf
tools/host/mosaic-keys.sudoers
Overlap with row 197 source manifest:
packages/cli/tests/stack-release.test.mjs
Other 24 source paths: no overlap.
@@ -0,0 +1,10 @@
diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs
index daae6efb7..910785bc4 100644
--- a/packages/cli/tests/stack-release.test.mjs
+++ b/packages/cli/tests/stack-release.test.mjs
@@ -0,0 +1,2 @@
+import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
+import { after as laneAfter } from 'node:test';
@@ -1779 +1781 @@ const TESTER='7499';
-const freePort=async()=>{const s=createServer();s.listen(0,'127.0.0.1');await once(s,'listening');const p=String(s.address().port);await new Promise((r)=>s.close(r));return p};
+const freePort=async()=>String(await leasePort({after:laneAfter},{closed:true}));
@@ -0,0 +1,5 @@
{
"base": "fefd54e13",
"prior": "9aa6d52d9",
"overlaps": []
}
@@ -0,0 +1,6 @@
{
"base": "b1fece6f0",
"r6": "df0c59608",
"r6Base": "fefd54e13",
"overlaps": []
}
@@ -0,0 +1,11 @@
--- a/scripts/testing/lane-docker.mjs
+++ b/scripts/testing/lane-docker.mjs
@@ -34,7 +34,7 @@
const service=argv.indexOf('mosaic-agent',2);
if(service<0||argv.slice(2,service).some(a=>!['--rm','-T','--no-deps','--interactive','-i'].includes(a)))refuse('Docker Compose run options not allowed');
const [base,override]=composeFiles(run);
- return ['compose','-f',base,'-f',override,'run','--pull','never','--no-deps',...argv.slice(2)];
+ return ['compose','-f',base,'-f',override,'run','--no-build','--pull','never','--no-deps',...argv.slice(2)];
}
refuse('Docker mutation or unowned command');
}
@@ -0,0 +1,6 @@
{
"base": "5c6f73ed7",
"r7": "9c3166915",
"r7Base": "b1fece6f0",
"overlaps": []
}
@@ -0,0 +1,11 @@
--- a/packages/cli/tests/gate-lane-image.test.mjs
+++ b/packages/cli/tests/gate-lane-image.test.mjs
@@ -85,7 +85,7 @@
// including when the lane's guard is first on PATH. Child fixture env stays
// isolated; passing the real absolute path avoids re-entering that guard.
function installedDocker(){
- const file=process.env.MOSAIC_GATE_DOCKER_RECORD;
+ const file=undefined; // mutant: lose the lane's real-binary lookup
if(!file)return 'docker';
const binary=JSON.parse(readFileSync(file,'utf8')).docker;
assert.ok(typeof binary==='string'&&isAbsolute(binary),'lane record must name the real absolute Docker binary');
+399
View File
@@ -0,0 +1,399 @@
# Row 215 build record
Owner: Herb. Reviewer: Filbert. Claim: herb-215-start, rev 2753.
Brief: docs/plans/2026-10-20_case2-rows.md, C2-04, pin
16847e94ae1768881711318fe81cbb5e6c66b971. Design: Gosalyn r11,
sections 1.1, 1.3, 2.1, M4, M9 and 7.2.
Base: a2c72ac952df94ecab7e391d5042f49b7f88b816, row 212's reviewed
candidate, under Q177. Branch: herb/row215. Rebuild on the combined 212+197
landing when Sage names it. Do not replay or revert landed changes.
## Scope rulings (sage-1848)
1. Add packages/bus/src/runtime.mjs for library adapter injection and the
library skip in protectedRoots. The skip has its own consumer test. Change
nothing else there.
2. Add packages/library/src/mirror.mjs, named mirror rather than store. It
creates a bare mirror without a remote, reads at the served commit and
reads sync state. Export one state filename constant, and export this
module through packages/library/src/index.mjs. Row 222 imports the constant.
3. Add packages/bus/src/server.mjs only to await library.file and
library.status, with socket tests proving their replies are results, not {}.
4. HTTP exposure is deferred. Row 225 owns the library.status route;
C2-11b owns library pages. Leave serve.mjs alone. webui/src/cli.mjs gets
only callback wiring next to the project-file callbacks on both listeners.
5. Reuse existing refusal codes where possible.
6. Build on a2c72ac95 until Sage supplies the 212 landing SHA.
Additional ruling, sage-1852: library-state-invalid is approved for malformed,
unsafe or mismatched state, including a missing file while the served ref
exists. The version-1 shared contract below is approved. Row 222 also owns
runtime.mjs for its sync and timer wiring, recorded in that row's manifest.
Additional rulings, sage-1860, after Filbert's CHANGES on 6a508804f:
- B1 adds packages/cli/src/config.mjs and cli/tests/trackers-boot.test.mjs.
trackerFor skips library entries. A real bus boot with tracker.baseUrl and
a library entry succeeds and gets no tracker from that entry.
- B2 refuses a role publish grant naming a library project in the loader.
- B3 readers use the sync lock. mirror.mjs exports withLibraryLock, with a
5000 ms wait bound and library-sync-busy on timeout. Sync locks only the
served-ref move and state publication, never the fetch.
- N3, git-socket doPush/recordPush accepting a library ID, is a later row.
Sage records it in DEFERRED.md when 215 lands. No fix here.
## Shared contract with row 222
Agreed in herb-0048, megavolt-0081/0082 and sage-1852. This section is the
contract Filbert reviews and Megavolt cites for Dewey. It is not a sync writer.
Module: packages/library/src/mirror.mjs. Export
`SYNC_STATE_FILE = 'mosaic-library-sync.json'`. The full path is
`join(mirror, SYNC_STATE_FILE)`, inside the bare repository. Only sync.mjs
writes it, using a sibling temp file, fsync, rename, and directory fsync.
Version 1 has exactly these keys:
```json
{
"version": 1,
"branch": "main",
"served": null,
"lastFetch": null,
"lastError": null,
"diverged": null,
"problems": []
}
```
- branch is the configured mapping branch and must match the reader's branch.
- served is null or a full lowercase 40-hex commit ID. refs/heads/served is
authority. The state value must equal that ref, which must name a commit.
- lastFetch is null or a canonical ISO UTC timestamp.
- lastError is null or exactly {code, at}. code matches [a-z-]{1,64}; at is
canonical ISO UTC. It never holds stderr, prose or a credential value.
- diverged is null or exactly {commit, at}, with the same commit/time rules.
- problems uses C2-01's exact {code, path, message, line?}. code is R1-R7,
message is that code's fixed RULES text, line is a positive safe integer
when present. No file body or arbitrary message is accepted.
- An absent file and absent served ref return the empty object above with
the configured branch. An absent file with an existing served ref refuses.
- Unknown keys, bad types, unsafe filesystem metadata, invalid JSON and a
mismatch refuse library-state-invalid. Reads use a bounded regular owned
private file without following links. The read never repairs state.
- refs/remotes/library/<branch> is not duplicated in this file. Row 222 may
read it when it needs the fetched tip.
Exports consumed by row 222:
| Export | Contract |
| --- | --- |
| mirrorPath({business, project, dataRoot}) | Require a library entry; return projectRepo's checked path |
| ensureMirror(context) | Promise of the mirror path; race-safe idempotent bare init; no remote or fetch |
| servedCommit(mirror) | Promise of full commit ID or null; refs/heads/served only |
| readSyncState(mirror, {branch}) | Promise of checked version-1 state; compare with served ref |
| withLibraryLock(mirror, operation) | Await the same exclusive lock as readers, then await operation({readSyncState}); always release |
The lock is a stable private regular file, mosaic-library-sync.lock inside
the mirror. It is never unlinked. A kernel flock is held by the caller's
file descriptor. Acquisition waits at most 5000 ms and refuses
library-sync-busy on timeout. Unsafe lock metadata refuses library-state-invalid.
There is no fs.flock API. The exact protocol is: open the stable file with
O_NOFOLLOW, check regular/private/owned/single-link metadata, pass its fd as
child FD 3 to `flock --exclusive --wait 5 --conflict-exit-code 75 3` using
asynchronous spawn, await
that child's exit 0, then run the callback while the parent still holds its
original descriptor. Both descriptors share the open file description, so
the lock survives acquisition-child exit. Finally close the parent's fd.
Process death also closes it. No holder child stays alive in the callback,
no stale file is deleted, and unrelated operation children inherit only 0-2.
Acquisition never uses spawnSync: the bus must serve other calls while a
library reader waits (megavolt-0087).
A scratch-only probe on node v26.8.1 at 2026-10-21T08:34:00Z confirmed this:
acquisition child exited 0; a second flock -n returned 1 while the parent fd
stayed open; after closeSync it returned 0. The probe removed its own root.
This was a design probe, not a package test, suite, Docker command or proof.
readSyncState takes that lock around reading the served ref and state file.
The callback-scoped readSyncState({branch}) supplied by withLibraryLock reads
under the already-held lock and does not reacquire it. The helper awaits
asynchronous callbacks and releases in finally. A callback-scoped reader
used after its callback ends refuses; it cannot escape its lock lifetime.
Row 222 fetches without this lock, then calls withLibraryLock across the
served-ref move and atomic state write. Its tests hold publication between
those steps and prove a concurrent reader waits, then sees the new consistent
pair, including first sync. Row 215 tests that publication shape and timeout.
A file read releases the lock after snapshotting a consistent served SHA,
then reads that immutable commit. Row 222's separate outer sync lock spans
fetch to prevent two sync operations interleaving (megavolt-0086).
Host adapter exports for this row:
- libraryStatus(context): return {project, ...state}, with no repository path
or content. Do not fetch or rebuild on this read.
- readLibraryFile({...context, path}): read a mapped, readable file at the
served commit, returning {project, path, commit, content}. No caller-picked
commit, working-tree fallback, fetch or write. C2-01's R1/R2/R7 exclusions
apply. A never-synced mirror refuses library-commit for a file read.
Row 222 owns syncCommand, sync, writeSyncState, its outer sync lock, the timer
and state publication. Row 215 owns the shared publication/read lock per
sage-1860. Sync rebuilds problems with existing readLibrary({repo, commit,
mapping}). It does not need a second rebuild function from this row.
## Root consumer audit and reviewer notes
On a2c72ac95, ran exactly the sage-1860 audit:
`git grep -n 'entry\.root\|\.root)' -- 'packages/*/src/*.mjs'`.
It has 14 hits. runtime.mjs:95, business/cli.mjs:61,72,78,118,
instance-settings.mjs:142-143, resolve.mjs:44, cli/config.mjs:62-63 and
project/paths.mjs:42 all get the library guard in this row. business.mjs:233
and :242 are code-project validation after the library dispatch, not an
unguarded consumer. queue.mjs:1144 compares queue genesis roots and never
iterates business.projects; it is safe and unchanged. A second search for
projectFilePath/business.projects/b.projects found no additional root reader.
ensureCheckouts destructures root rather than spelling .root; its guard and
the launcher's filter remain in T1. Bare projectFilePath/loadProject remain
strict and unchanged.
Filbert N1 is handled at the existing owned paths.mjs boundary: projectRepo
defaults to record-project use and refuses a library entry with project-invalid.
Only mirrorPath's internal call opts into allowLibrary. Thus ordinary human
and tester project init cannot occupy an uninitialized library mirror path.
No record API or bus argument exposes that internal option.
Filbert N2: ensureMirror failure refuses bus boot. No socket becomes ready.
The runtime closes resources it opened, following its existing failure path.
A T4 test uses an unsafe/pre-existing mirror and proves the refusal, rather
than serving a half-configured library adapter.
## Verification record
Planning stage. No product code, package test, suite, image build, live operation
or push for row 215 has happened. Product implementation awaits Filbert's
manifest approval. Sage owns suite and full-gate grants. Row 197's proof
commit is separate and is not part of this row's changes.
## Implementation ledger
Plan: agents/herb/work/queue-215/MANIFEST.md at 2522025af, approved by
Filbert in the reply to herb-0056. Execution is inline by Herb under the
executing-plans and test-driven-development skills. No agent delegation.
Pre-flight: T1's validated mapping feeds T2's protected document and T3's
mirror context; T3's lock/state helpers feed T4's injected host adapter.
The shared contract is pinned above, with no interface conflict found.
Sage-1867 overrides COMMON's singles allowance: every test and executable
probe needs an explicit slot grant. No further probe/test has run since
that reminder. Read-only inspection and test authoring continue while the
slot is held. The first red execution is pending; product code is unchanged.
Filbert's approved-manifest build notes: ensureMirror creates the lock in
its staged directory before publication; a missing lock in an existing
mirror refuses library-state-invalid. The top-level metadata allowlist
permits only normal bare Git entries plus the state and stable lock files.
Tests cover an unknown entry. The host needs /usr/bin/flock; ENOENT yields
a handled library-state-invalid refusal, never an unhandled child error.
Lock acquisition uses /usr/bin/flock directly and asynchronous spawn.
T1: consumer and loader regression tests authored, not run. T2: direct M9
write and serialization regression tests authored, not run. No completion
or passing claim is made for either task.
Update, sage-1869 focused slot, 2026-10-21T08:47:48Z to 08:51:07Z:
the busy check was CLEAR before the first test. T1's mapping regression
failed 0/1 on unsupported kind as expected, then passed 2/0 with the publish
refusal included. Other focused greens: business CLI 1/0, resolver 1/0,
instance settings 1/0, seat checkout 1/0, project paths 1/0, sandboxed launcher
1/0, and trackers-boot whole 2/0. T2's direct writer case failed 0/1 on the
missing refusal, then its whole new file passed 4/0. Green total 14/0 with
no skips or cancellations. Logs and counts: granted-t1/. The final /proc
scan found no test process or fixture cwd/argv reference; its private root
was removed. Clear sent as herb-0062. Existing files' other cases have not
been run yet, so T1/T2's full-file acceptance is still pending.
Sage-1874 directs continued authoring and implementation while the landing
gate/proof holds the slot. T3/T4 tests were written first. Their initial
red runs cannot execute during that hold; implementation proceeds under
that owner direction, with no claim of a witnessed red or green for T3/T4.
Their baseline red and candidate checks need a later explicit grant.
Implementation ruling: initialization uses GNU /usr/bin/mv --no-clobber
--no-target-directory for its bounded staging rename, rather than Node's
renameSync, which could replace an empty directory made by a competing
actor. This preserves pre-existing directories and needs no new package on
the Linux/coreutils bus host. The host also needs /usr/bin/flock. Neither
command has been executed for T3/T4 yet. Publication errors fail closed.
Sync-state reads are capped at 8 MiB, including a checked extra byte; an
oversized or concurrently changed file refuses library-state-invalid.
T5 static dependency audit found two partial pinned-copy fixtures that copy
business/src but not its newly imported library mapping validator:
cli/tests/stack-release.test.mjs:1772 and stack-release-fixture.mjs:309 on
the base. Each now includes packages/library/src in the existing copy list.
This changes fixture closure only, no assertion or timeout. The complete
stack-release file and each test file importing that shared fixture require
a later granted single-file run. They are outside the four-file T3/T4 slot.
Sage-1879 places the four-file T3/T4 focused run after their 212+197 proof,
before Gizmoduck, and requires their explicit clear before starting. No run
has occurred after herb-0062. Row 197's N7 base-image-tag finding is left to
Sage's DEFERRED entry; no row 197 source change is made here.
Focused T3/T4 grant sage-1882, 2026-10-21 09:13-09:18Z: busy check
CLEAR. Final whole-file results: mirror 16/0, bus library 6/0, WebUI
library-cli 1/0, project-runtime 3/0, all with zero skips/cancellations.
Syntax checks passed for the four files and five changed host modules.
Logs, all intermediate failures, counts and empty /proc cleanup scan are
in granted-t34/. Baseline a2c72ac95: three focused bus cases 0/3
(unknown-verb), CLI 0/1 (missing callbacks). Mirror baseline 0/1 is an
import refusal for the new module, not a witnessed behavioral red.
An initial whole baseline bus attempt left a runtime after its expected
failure; its exact runner was stopped, no process remained, and the
baseline was narrowed to the three bounded broker/socket cases.
Candidate fixture setup errors and a mock-child timeout were corrected
before the final whole-file passes. No suite or Docker/image/proof ran.
Filbert's 09:09 publication note: no dependence on mv -n's skipped-rename
exit status across coreutils versions. Save staging dev/ino before mv;
only its disappearance plus the same dev/ino at the mirror proves a win.
A remaining original staging inode means a loss, requiring a valid winner.
Only that owned original staging inode is removed. Numeric no-clobber
statuses 0 and 1, including a diagnostic, are tested with a competing
winner. Spawn errors, signals/timeouts and inconsistent outcomes refuse.
Coreutils must provide atomic --no-clobber --no-target-directory semantics;
it need not use any particular skipped-rename status convention.
Landing update: prepared herb/row215-on212 on fefd54e13 (includes row 212
landing ee7b2f137), without history replay or stash. Only BUILD-LOG.md and
cli/tests/stack-release.test.mjs overlap the a2c72ac95-to-landing change
set. BUILD-LOG preserves all landed entries and appends the existing Herb
entry. stack-release preserves the landing plus only the library/src copy
list addition. No test has run on this landing yet. All further execution
requires a Sage grant (supersedes the original COMMON singles rule).
Sage-1979 early singles on fefd54e13, stopped after file10 per sage-1983:
- packages/bus/tests/library.test.mjs: 6/0
- packages/bus/tests/project-runtime.test.mjs: 3/0
- packages/business/tests/business.test.mjs: 18/0
- packages/business/tests/cli.test.mjs: 10/0
- packages/business/tests/instance-settings.test.mjs: 12/0
- packages/business/tests/library-projects-protected.test.mjs: 4/0
- packages/business/tests/resolve.test.mjs: 11/0
- packages/cli/tests/launcher.test.mjs: 30/0
- packages/cli/tests/stack-release-external.test.mjs: 25/0
- packages/cli/tests/stack-release.test.mjs: 196/0
Total315/0, zero skips/cancellations. This is an early read only, not gate
evidence for a merged candidate. The sequential driver was paused during
file10 so it could not start file11, then terminated after file10 exited0.
No file11 log exists. Results/logs are in granted-singles/. Later landed
overlaps require a new branch and all16 singles again after their merge.
Current merge preparation per sage-1983/1987: new branch
herb/row215-on-f827 on f8276c5eb3ecb0aad353ee65e3dd8f7faac5dd87.
Nine landed overlaps are recorded in current-MERGE.json and the exact
candidate deltas against the landing are in current-merge-only.patch.
Two textual conflicts were resolved manually: Broker keeps hostKeys and
adds library; business.mjs keeps CREATIVE_DEFINITIONS and adds checkMapping.
The other seven merges preserve the landing plus the original C2-04 edits.
All other product/test paths match the early215 tree byte for byte. The
landing's BUILD-LOG is preserved and the existing Herb entry is appended.
No history replay, stash, force push, branch deletion or source commit.
No test/probe/syntax check ran on the merged tree after the slot was cleared.
All16 whole singles need a new Sage grant before candidate commit/review.
The10 old-base315/0 results are early reads only, never merged gate evidence.
## Source candidate and sage-2008 receipts
Source commit: e333af4dc7af3a9d3789c8c0db26a0df8bdddfee, on
f8276c5eb3ecb0aad353ee65e3dd8f7faac5dd87. SOURCE.json records all30
changed source/phase-log paths with blob SHA256 and Git mode. The next
commit contains only this packet. No product change follows the tests.
Sage-2008: start13:15:15.548943Z; end13:27:27.279728Z, 2026-10-21.
Each of the16 TEST-FILES.txt files ran whole, sequentially. All451 passed,
with0 failed/skipped/cancelled. Per-file outputs are in granted-2008/;
EVIDENCE.json records their counts, Node durations and log hashes.
| File | Pass/fail |
| --- | ---: |
| packages/bus/tests/library.test.mjs | 6/0 |
| packages/bus/tests/project-runtime.test.mjs | 3/0 |
| packages/business/tests/business.test.mjs | 19/0 |
| packages/business/tests/cli.test.mjs | 10/0 |
| packages/business/tests/instance-settings.test.mjs | 13/0 |
| packages/business/tests/library-projects-protected.test.mjs | 4/0 |
| packages/business/tests/resolve.test.mjs | 11/0 |
| packages/cli/tests/launcher.test.mjs | 31/0 |
| packages/cli/tests/stack-release-external.test.mjs | 25/0 |
| packages/cli/tests/stack-release.test.mjs | 196/0 |
| packages/cli/tests/stack-upgrade.test.mjs | 21/0 |
| packages/cli/tests/trackers-boot.test.mjs | 2/0 |
| packages/library/tests/mirror.test.mjs | 16/0 |
| packages/project/tests/project.test.mjs | 15/0 |
| packages/seat/tests/checkout.test.mjs | 78/0 |
| packages/webui/tests/library-cli.test.mjs | 1/0 |
All29 product/test hashes equal their pre-run values. The final /proc cwd
and argv scan was empty; the private test root was removed. Clear sent
as herb-0082 at13:28:38Z. No further runs occurred. No image, Docker,
lane, proof or full suite was part of this grant.
Before commit, checked f8276c5eb..8a04f4aa4 on every215 source path and
all16 test paths: no overlap. Row248's grant-executor test is outside them.
The current root-consumer grep still returns14 sites, all classified by
the earlier audit: business code roots after dispatch, guarded library
skips at consumers, and queue's unrelated genesis-root invariant.
Sage-2016 authorizes the source commit, packet receipts and seat-only push,
then Filbert's candidate review. The landing gate stays with Sage. Earlier
focused and old-base receipts are archived in granted-t1/, granted-t34/
and granted-singles/, with their limits kept above. Full package/shell
landing checks and implementation approval remain pending. No HTTP route,
remote fetch, sync writer, timer or live operation was added.
Raw Node error logs retain their original space-only lines. Packet whitespace
checks exclude *.log so the recorded output bytes stay unchanged; source and
other packet files pass the ordinary diff check.
## Merge-scoped round 2 after rows 249 and 197 (sage-2054)
Row197 landed as a36523713 after proof6 passed 8/8 gates. Row249's
broker fix is also in this base. Source a6101e21c596e2972ab5a7e3087736075899505e, single parent a3652371338e60187d4e95cee4a0f30f3c8a8f06,
is on the new herb/row215-on197 branch. The old branch stays untouched.
No force push, branch deletion or stash was used. The merge was clean.
Only broker.mjs and stack-release.test.mjs changed upstream among the30
source/phase-log paths. Both retain every landed hunk. The broker keeps
row249's installed flag and hostLineLeft handling. The stack-release test
keeps row197's leasePort import, after hook and closed-port lease helper;
215 adds only its library fixture copy. The other28 blobs and all modes
match the prior source. All30 paths have exactly the same -U0 added and
removed lines as f8276c5eb..e333af4dc. BUILD-LOG.md is append-only.
Stable patch-id, old and new: d191e7e9586cd3b7c18310daacdf0c1fdbc8be01.
r2-range-diff.txt reports only the commit message changed (!); its patch
is unchanged.
r2-MERGE.json records every path; r2-merge-only.patch contains both merged
source diffs against the new base. SOURCE.json names the new source.
Filbert approved round1 in comment30071, rev2897. This round requests
merge-only review. The earlier451/0 receipts belong to the prior base;
no test, probe, syntax check, Docker command or lane ran in this round.
Merged-tree evidence remains pending Sage's explicit clear.
r2-TEST-FILES.txt proposes all16 original whole singles plus all66
bus test files whose local import closure reaches broker.mjs, deduplicated
to80 whole files. This includes shared fixture and runtime imports.
Sage's slot and row196 landing gate come first. The full landing gate
remains Sage's. No library behavior, assertion or timeout changed.
+261
View File
@@ -0,0 +1,261 @@
{
"grant": "sage-2008",
"sourceCommit": "e333af4dc7af3a9d3789c8c0db26a0df8bdddfee",
"base": "f8276c5eb3ecb0aad353ee65e3dd8f7faac5dd87",
"start": "2026-10-21T13:15:15.548778+00:00",
"end": "2026-10-21T13:27:27.279728+00:00",
"summary": {
"tests": 451,
"pass": 451,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"results": [
{
"file": "packages/bus/tests/library.test.mjs",
"rc": 0,
"counts": {
"tests": 6,
"pass": 6,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "01-packages_bus_tests_library.test.mjs.log",
"sha256": "c19016583abc936534f41bde26eb39e095496eda944622142b1d18ffd5018bc0",
"nodeDurationMs": 359.869493
},
{
"file": "packages/bus/tests/project-runtime.test.mjs",
"rc": 0,
"counts": {
"tests": 3,
"pass": 3,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "02-packages_bus_tests_project-runtime.test.mjs.log",
"sha256": "f123231042d1562fee6dad3d885ac587967e0287f63603c861d04c1907363530",
"nodeDurationMs": 312.717335
},
{
"file": "packages/business/tests/business.test.mjs",
"rc": 0,
"counts": {
"tests": 19,
"pass": 19,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "03-packages_business_tests_business.test.mjs.log",
"sha256": "ae990ebccf7f21b9b32fd9c25fdd637873251c078e17dd95471b82b548322980",
"nodeDurationMs": 199.91256
},
{
"file": "packages/business/tests/cli.test.mjs",
"rc": 0,
"counts": {
"tests": 10,
"pass": 10,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "04-packages_business_tests_cli.test.mjs.log",
"sha256": "28512dc38e2bce04294048a2f1c7dc7cf0c322028f874d1d35aa83b87570ea54",
"nodeDurationMs": 2381.258516
},
{
"file": "packages/business/tests/instance-settings.test.mjs",
"rc": 0,
"counts": {
"tests": 13,
"pass": 13,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "05-packages_business_tests_instance-settings.test.mjs.log",
"sha256": "146623624012da1399fb4ee3de4c0e13df1af28caae81f4870315a67ac368f8f",
"nodeDurationMs": 608.08011
},
{
"file": "packages/business/tests/library-projects-protected.test.mjs",
"rc": 0,
"counts": {
"tests": 4,
"pass": 4,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "06-packages_business_tests_library-projects-protected.test.mjs.log",
"sha256": "675ae18523218ab70a4addee039f41df6ae08ee414f20527f975a2004dde65b2",
"nodeDurationMs": 118.54831
},
{
"file": "packages/business/tests/resolve.test.mjs",
"rc": 0,
"counts": {
"tests": 11,
"pass": 11,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "07-packages_business_tests_resolve.test.mjs.log",
"sha256": "6557938cb8e6d79617c0ea911a6e0ad7303cf5a4f9dd5a5631c70d093e6b06fe",
"nodeDurationMs": 94.022587
},
{
"file": "packages/cli/tests/launcher.test.mjs",
"rc": 0,
"counts": {
"tests": 31,
"pass": 31,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "08-packages_cli_tests_launcher.test.mjs.log",
"sha256": "c3404f1931595914d469cb00a951a5e1648f7792733c86bddbfa099552e5791c",
"nodeDurationMs": 24989.261794
},
{
"file": "packages/cli/tests/stack-release-external.test.mjs",
"rc": 0,
"counts": {
"tests": 25,
"pass": 25,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "09-packages_cli_tests_stack-release-external.test.mjs.log",
"sha256": "55224b6204d4d05a4cb669fb2845aa4168ab868223c3f341ab026032c47458b4",
"nodeDurationMs": 96332.465778
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"rc": 0,
"counts": {
"tests": 196,
"pass": 196,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "10-packages_cli_tests_stack-release.test.mjs.log",
"sha256": "3f7430ce1f28482a5b66fc0e7db76969298f0dd32c3793e242bc10110dc44e6e",
"nodeDurationMs": 533493.817869
},
{
"file": "packages/cli/tests/stack-upgrade.test.mjs",
"rc": 0,
"counts": {
"tests": 21,
"pass": 21,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "11-packages_cli_tests_stack-upgrade.test.mjs.log",
"sha256": "d18a3eff64b39a5128ab7e842721748b214dc3037ecc9d2c93c58f023859d695",
"nodeDurationMs": 60456.446459
},
{
"file": "packages/cli/tests/trackers-boot.test.mjs",
"rc": 0,
"counts": {
"tests": 2,
"pass": 2,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "12-packages_cli_tests_trackers-boot.test.mjs.log",
"sha256": "22e56455a6a56c98b02f942ed3d38504e4f8486343235c4035c88b967625bce5",
"nodeDurationMs": 502.723687
},
{
"file": "packages/library/tests/mirror.test.mjs",
"rc": 0,
"counts": {
"tests": 16,
"pass": 16,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "13-packages_library_tests_mirror.test.mjs.log",
"sha256": "def30105a3d561118e360535d9d3129226361f2e7902f316e47c74f65f1c0e47",
"nodeDurationMs": 5963.543966
},
{
"file": "packages/project/tests/project.test.mjs",
"rc": 0,
"counts": {
"tests": 15,
"pass": 15,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "14-packages_project_tests_project.test.mjs.log",
"sha256": "e63ab0d23ab24aa1f1f2c1ea8017e8541f415dcb20dff697a37bb7a6368e174d",
"nodeDurationMs": 355.86586
},
{
"file": "packages/seat/tests/checkout.test.mjs",
"rc": 0,
"counts": {
"tests": 78,
"pass": 78,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "15-packages_seat_tests_checkout.test.mjs.log",
"sha256": "c9e7168f589047b8176ec4ee8d2f03adb9fb68f692b363ba6d0036f670811ede",
"nodeDurationMs": 4880.907448
},
{
"file": "packages/webui/tests/library-cli.test.mjs",
"rc": 0,
"counts": {
"tests": 1,
"pass": 1,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "16-packages_webui_tests_library-cli.test.mjs.log",
"sha256": "1ca5b92bc4f2de9ef6aa7156cdea62742fb9ddf054ebfb56a9548431e0cfec0d",
"nodeDurationMs": 191.536271
}
],
"cleanup": {
"processes": [],
"removed": "/mnt/storage/scratch/gt-j5nm6iqn",
"sourceHashesUnchanged": 29
},
"drift": {
"base": "f8276c5eb",
"tip": "8a04f4aa4aba493eb89e92aa9e0c34084be7058e",
"overlap": []
},
"earlierRuns": {
"granted-t1": "focused T1/T2 red/green on a2c72ac95",
"granted-t34": "focused T3/T4 with baseline import refusal and corrected fixture errors recorded",
"granted-singles": "old fefd54e13 early315/0, not merged gate evidence"
},
"mergeRound": {
"sourceCommit": "a6101e21c596e2972ab5a7e3087736075899505e",
"base": "a3652371338e60187d4e95cee4a0f30f3c8a8f06",
"status": "not-run; awaiting Sage clear",
"priorReceipts": "sage-2008 receipts apply to e333af4dc on f8276c5eb only"
}
}
+246
View File
@@ -0,0 +1,246 @@
# C2-04 library business entry implementation plan
> Execution: Herb implements in this session using the executing-plans skill.
> Filbert approves this manifest before any product implementation.
**Goal:** Load a library mapping without giving an agent a checkout, protect
all project mappings from organization writes, and expose human-only bus
reads of the bare mirror's served commit and sync status.
**Architecture:** C2-01 checks mappings and reads commits. Business consumers
skip library entries. A host-only mirror module owns local mirror reads and
initialization. runtime.mjs injects that module into the broker, preserving
the broker's sandbox import closure. Sync and HTTP routes belong to later rows.
**Tech stack:** Existing Node ESM, node:test and bounded local Git helpers.
No dependencies. node_modules remains the plain canonical symlink.
**Spec:** Pinned C2-04 brief; Gosalyn's approved design 1.1, 1.3, 2.1,
M4/M9 and 7.2; BUILD.md's sage-1848/1852/1860 scope and shared-state contract.
## Constraints and review focus
- Base a2c72ac95; rebase only onto Sage's named 212 landing on a new branch.
- Existing code-project entries retain root/repo/remote behavior. Library
entries have no root or repo override and use C2-01 checkMapping.
- Preserve the broker's pinned imports. No library import in broker.mjs and
no new runner mount. Do not edit serve.mjs or add an HTTP route.
- No fetch, remote config, credential read, timer, sync writer, bus write or
live operation. All tests use made-up content and local repositories.
- All test/probe runs require Sage's grant (sage-1867), including singles.
Stop and report two failures in one gate. Do not push without authorization.
Review focus, with owning tasks below: library-first iteration must not
break a later code project (T1); serialized documents cannot bypass M9
(T2); racing initialization must not replace another mirror (T3); stale or
unsafe state must not leak content (T3); async socket replies must preserve
the result and human-only admission (T4).
## Paths and evidence from the base
| Product path | Allowed change |
| --- | --- |
| packages/business/src/business.mjs | Dispatch kind:library to checkMapping before code-root validation (:228-244) |
| packages/business/src/organization.mjs | Compare canonical projects in write before publication (:177 onward) |
| packages/business/src/cli.mjs | Skip library roots in validate, resolve --project and credential exclusion roots (:60-61, :71-79, :118) |
| packages/business/src/resolve.mjs | Skip library entries while matching a project file (:44) |
| packages/business/src/instance-settings.mjs | Skip library before projectFilePath (:142), nothing else |
| packages/seat/src/checkout.mjs | Skip library entries in ensureCheckouts (:315-327) |
| packages/cli/src/launcher.mjs | Pass only code entries to ensureCheckouts (:354) |
| packages/cli/src/config.mjs | Skip library entries in trackerFor's root loop (:61-63), per sage-1860 |
| packages/project/src/paths.mjs | Skip library entries in overlap loop (:41-44); default record use refuses library, mirrorPath internally opts into allowLibrary |
| packages/library/src/mirror.mjs | New bare init, checked state and served-commit file read module |
| packages/library/src/index.mjs | Export mirror module interface and SYNC_STATE_FILE |
| packages/bus/src/broker.mjs | New injected library adapter, human-only admission and two async verbs |
| packages/bus/src/runtime.mjs | Library adapter wiring, mirror initialization and protectedRoots skip only |
| packages/bus/src/server.mjs | Await the two library verbs only |
| packages/webui/src/cli.mjs | Two library read callbacks beside project callbacks on main and tester listener; no routes |
packages/business/src/project.mjs need not change: callers skip a library
before projectFilePath or loadProject. Do not weaken their code-root checks.
scripts/stack-copy.sh already copies packages/library/src and needs no edit.
Test paths to modify: business/tests/business.test.mjs, cli.test.mjs,
resolve.test.mjs, instance-settings.test.mjs; seat/tests/checkout.test.mjs;
cli/tests/launcher.test.mjs, trackers-boot.test.mjs; project/tests/project.test.mjs;
bus/tests/project-runtime.test.mjs. All paths are under packages/.
T5 dependency-audit addition: cli/tests/stack-release.test.mjs and its
stack-release-fixture.mjs helper copy packages/library/src alongside
business/src, whose validator now imports the mapping checker. No assertions
or timeouts change. Whole-file dependent singles require a later Sage slot.
New tests: packages/business/tests/library-projects-protected.test.mjs;
packages/library/tests/mirror.test.mjs; packages/bus/tests/library.test.mjs;
packages/webui/tests/library-cli.test.mjs. Reuse existing made-up fixtures.
If a new fixture is needed, define it within the corresponding new test file.
No unrelated test assertion or timeout changes.
Record paths: agents/herb/work/queue-215/{MANIFEST.md,BUILD.md}, later source
manifest and test receipts there; one append-only BUILD-LOG.md phase for this
row when implementation begins. Sage owns docs/SESSIONS.md.
## T1. Mapping and consumer skips
- [ ] Add failing business tests: accept a frozen library mapping without a
root, retain mixed/only-library documents, and propagate C2-01's mapping
and playground refusal codes. A role publish naming a library project
refuses at load, even with an otherwise valid fork identity and remote.
Existing code validation remains unchanged.
- [ ] Add a separate regression for each consumer: CLI validate and its
credential exclusion roots; CLI resolve --project books has no project
layer; resolveInstance finds a later real project; instance settings still
writes an unrelated setting; ensureCheckouts returns no library checkout;
launcher creates no books workspace clone; projectRepo skips a library
while still rejecting overlap with a real code root. Put books first.
- [ ] Add the real bus boot regression in cli/tests/trackers-boot.test.mjs:
tracker.baseUrl is set and books is a library entry; boot succeeds, books
produces no tracker, and a later real tracker project still works.
- [ ] projectRepo(context, {allowLibrary = false} = {}) refuses a library
entry by default with project-invalid. Only mirrorPath opts in. Test that
ordinary human and tester record initialization leave no directory at the
future mirror path; no public verb can supply allowLibrary.
- [ ] Add runtime's protectedRoots consumer regression in T4's real runtime
fixture. A valid credential outside code/data roots still loads with a
library entry, and existing credential protections still refuse unsafe files.
- [ ] Run each changed test singly, confirm the new cases fail for the
intended root assumption, then implement only the table's guards and
checkMapping dispatch. Re-run the same files singly.
CLI validate reports a library entry as library, without an absent-project
warning. CLI resolve --project books resolves without loading a project file,
as the design's skip requires. It does not fabricate project vars or a root.
## T2. M9 at the organization writer
- [ ] In the new business test file, call organizationWriter.write directly
with a changed playground prefix and, separately, a changed hybrid entry.
Both refuse organization-projects-protected. Snapshot business bytes and
changes.jsonl before each call; assert both remain identical afterward.
- [ ] Cover addition, deletion and changes to code projects too, since M9
protects all projects. Reordered object keys are allowed; array changes
are changes. An ordinary non-project write still succeeds and logs.
- [ ] Add a serialization regression so a proposed object's serialized
projects cannot differ from those checked before publication.
- [ ] Run the file singly to see the missing refusal, compare canonical
projects against old disk JSON and the document that actually lands,
before any publication/receipt, then re-run it singly.
## T3. Bare mirror, served file and read-only state
- [ ] Add failing tests for mirrorPath and ensureMirror: exact projectRepo
default, no checkout/remote, retained no-links/overlap protections,
idempotence, simultaneous creators and preservation of a pre-existing
non-mirror directory or unsafe Git metadata.
- [ ] Add failing state tests for BUILD.md's full version-1 contract: missing
state/ref, valid state, malformed JSON, unknown keys/types, branch mismatch,
invalid times/IDs/problem shape, symlink/hardlink/non-private file and
missing or mismatched served ref. Assert reads change no bytes.
- [ ] Add shared-lock tests: withLibraryLock(mirror, operation) waits at most
5000 ms, refuses library-sync-busy on timeout and releases after a throw.
A reader arriving between the publisher's served move and state write
waits, then reads the consistent pair. Cover first sync and fast-forward
in independent processes, same-process independent acquisitions and
release after owner death. A callback-scoped reader expires on release.
- [ ] Implement the lock by async spawn of flock on the parent's inherited
fd 3, as pinned in BUILD.md. Await acquisition-child exit before calling
operation. Never spawnSync during acquisition, which could freeze the bus
for 5 s. A parallel ordinary bus read remains responsive while a library
read waits. Only the publication/read lock is shared; fetch is outside it.
- [ ] Add served-read tests against local fixtures containing spaces,
capitals, a .canvas and unmapped top-level note. Seed a served ref and state
without fetching. A newer unrelated HEAD does not change the returned
file. R1/R2/R7 exclusions, traversal, a missing path, a caller commit and a
never-synced mirror refuse. Status contains no fixture body marker.
- [ ] Implement mirror.mjs and index exports using the shared contract.
Initialization stages into an owned private sibling directory and renames
atomically; a losing creator validates the winner and removes only its own
staging directory. Git uses existing bounded safe helpers, no remote.
- [ ] Run mirror.test.mjs singly, red then green. Re-run C2-01 reader.test.mjs
singly if a reader assumption changes. Do not change C2-01 reader rules.
mirror.mjs validates a bare mirror's metadata before Git reads it, including
no remote/include/indirection. It does not apply inspectGit's working-tree
.git assumption to a bare repository. Failures reuse library-git or the
approved library-state-invalid code. State reads are bounded and private.
## T4. Human-only bus reads and deferred Console callbacks
- [ ] Add broker tests for library.file and library.status using injected
adapters. Each agent session refuses human-required before adapter access,
including bad arguments and an absent adapter. A reader or tester does not
gain a human capability. Human calls validate keys, then use checked
library context. Secret checking and bounded refusal evidence remain.
- [ ] Add real startBroker/socket tests for both verbs. Seed only local
fixture mirror data. Assert result shape/content and an actual status
field, not merely successful transport or {}. Cover never-synced status
and runtime's protectedRoots skip. Keep the pinned import-closure test.
- [ ] Inject a separate library adapter via runtime, initialize only library
mirrors, add the two async request paths, and include them in server's
awaited list. Do not import library or fs into broker.
- [ ] Unsafe or pre-existing non-mirror initialization refuses bus boot,
closes opened resources and never publishes a ready socket. Add that
runtime regression in library.test.mjs.
- [ ] Wire main and tester Console callbacks beside project-file callbacks.
No HTTP route is shipped. The new CLI test captures the startServer
options with a test-only module loader and calls the supplied callbacks
against a fixture bus, checking the verb and transport identity. No
product test seam, UI page, endpoint or serve.mjs change is needed.
- [ ] Run library.test.mjs, project-runtime.test.mjs and library-cli.test.mjs
individually, red then green. Human admission may use the existing
fixture authentication seam; do not forge or bypass a live identity.
## T5. Review packet and Sage's gate
- [ ] Search packages/*/tests and scripts/test-*.sh for each changed source
path, imports and 64-hex pins. Record the resulting test-file list and run
each changed/touched test singly. Stop and report a scope error.
- [ ] Self-review the diff against this manifest and all acceptance cases.
Record results, commands, skips, source hashes and remaining limits in
BUILD.md. Request Sage's slot for the required business, seat, cli, project,
bus and webui suites and every scripts/test-*.sh. Do not run them ungranted.
- [ ] After applicable tests are green, make the source commit, build the
candidate manifest from the authorized pushed tip, and move the row with
scripts/mosaic. Sage posts token-requiring requests; Filbert reviews the
candidate. Push only when authorized. No merge or live change by Herb.
## Self-review
Every brief acceptance item is in T1-T4. Added host support paths match
sage-1848/1860. BUILD.md records the complete 14-hit root audit. State shape
and authority match sage-1852; the shared read/publication lock follows
sage-1860 and Megavolt's agreement. Fetch, timers, outer sync serialization
and HTTP stay with their later rows.
This is a plan only; no product edits or test execution precede approval.
Landing update: prepared herb/row215-on212 on fefd54e13 (includes row 212
landing ee7b2f137), without history replay or stash. Only BUILD-LOG.md and
cli/tests/stack-release.test.mjs overlap the a2c72ac95-to-landing change
set. BUILD-LOG preserves all landed entries and appends the existing Herb
entry. stack-release preserves the landing plus only the library/src copy
list addition. No test has run on this landing yet. All further execution
requires a Sage grant (supersedes the original COMMON singles rule).
Current merge preparation per sage-1983/1987: new branch
herb/row215-on-f827 on f8276c5eb3ecb0aad353ee65e3dd8f7faac5dd87.
Nine landed overlaps are recorded in current-MERGE.json and the exact
candidate deltas against the landing are in current-merge-only.patch.
Two textual conflicts were resolved manually: Broker keeps hostKeys and
adds library; business.mjs keeps CREATIVE_DEFINITIONS and adds checkMapping.
The other seven merges preserve the landing plus the original C2-04 edits.
All other product/test paths match the early215 tree byte for byte. The
landing's BUILD-LOG is preserved and the existing Herb entry is appended.
No history replay, stash, force push, branch deletion or source commit.
No test/probe/syntax check ran on the merged tree after the slot was cleared.
All16 whole singles need a new Sage grant before candidate commit/review.
The10 old-base315/0 results are early reads only, never merged gate evidence.
Candidate status: source commit e333af4dc7af3a9d3789c8c0db26a0df8bdddfee. The16 whole
singles passed451/0 under sage-2008 with zero skips/cancellations; raw
outputs and merge receipts are in this packet, indexed by EVIDENCE.json
and SOURCE.json. Sage keeps the landing gate. Filbert's implementation
review and that gate remain pending. Prior315/0 on fefd54e13 is early
read evidence only. Sage-2016 authorizes the seat-only source/packet push.
+157
View File
@@ -0,0 +1,157 @@
{
"sourceCommit": "a6101e21c596e2972ab5a7e3087736075899505e",
"base": "a3652371338e60187d4e95cee4a0f30f3c8a8f06",
"paths": [
{
"path": "BUILD-LOG.md",
"sha256": "b7c8f906f2db095dfe02d29b4064836af83ae877a87b38c09466b8c9a327e019",
"mode": "100644"
},
{
"path": "packages/bus/src/broker.mjs",
"sha256": "762234259d72db87aff510751fe14003099759ae37bc8eaf14f1ea1de61d2005",
"mode": "100644"
},
{
"path": "packages/bus/src/runtime.mjs",
"sha256": "c759e665514a5bcad40536bc9f979837187fe6523462b3c0a0687182885c3ac6",
"mode": "100644"
},
{
"path": "packages/bus/src/server.mjs",
"sha256": "207c7a76752c753ef7380f18f9ce1a4f23e8fa35142973b6358aaf0c45f44899",
"mode": "100644"
},
{
"path": "packages/bus/tests/library.test.mjs",
"sha256": "58d839709e005571b398a11abda5a50f542bab0e37bd97b72f43cde926434301",
"mode": "100644"
},
{
"path": "packages/business/src/business.mjs",
"sha256": "5fd3fee664d3e70153d988b4c1a42406f888a31b3927fa52375e66619d8cec59",
"mode": "100644"
},
{
"path": "packages/business/src/cli.mjs",
"sha256": "31cfe9090900169416e1207dc6db865b046439273917e9de442add4159c89eb3",
"mode": "100644"
},
{
"path": "packages/business/src/instance-settings.mjs",
"sha256": "3873411688d927336a909d0299acddeba92a653102c660d276471f5a0f6eebd3",
"mode": "100644"
},
{
"path": "packages/business/src/organization.mjs",
"sha256": "eaa650499d2d078552a2c0e5b13a6126e0aca02f73160032a7325b3f5aa90964",
"mode": "100644"
},
{
"path": "packages/business/src/resolve.mjs",
"sha256": "712183465fee099606223d84423afc54e3386a0c8e468c68d5ac39c1e676d109",
"mode": "100644"
},
{
"path": "packages/business/tests/business.test.mjs",
"sha256": "910f924d90b6f9b18c1f36d97cebd0b7a0a15a080b73530c4fb847bfd435fb3f",
"mode": "100644"
},
{
"path": "packages/business/tests/cli.test.mjs",
"sha256": "d54ba2338bcdfec22764030cf3ec9e3464ab275b15321d6d1b039169ea37fd85",
"mode": "100644"
},
{
"path": "packages/business/tests/instance-settings.test.mjs",
"sha256": "072e25c5237e1be4294c7a4a95daa2ab593832786013e1283b19725c330647c1",
"mode": "100644"
},
{
"path": "packages/business/tests/library-projects-protected.test.mjs",
"sha256": "cfc726ec7bf73c1dc9fc6395cba85e374b7a006bac45bd8c47c969550557147f",
"mode": "100644"
},
{
"path": "packages/business/tests/resolve.test.mjs",
"sha256": "c74af44a83ee918196e2c1016b62786e757c0ae774c9b1613280d3fbbf3ffb22",
"mode": "100644"
},
{
"path": "packages/cli/src/config.mjs",
"sha256": "ab7b9f452123b6f7e98f4f59cbb3cc4483fd296e4af5522412410ee53d1b20e0",
"mode": "100644"
},
{
"path": "packages/cli/src/launcher.mjs",
"sha256": "d8a870d8eb8254adfd576688696705359fefbf517b40e5d7482500bf27941022",
"mode": "100644"
},
{
"path": "packages/cli/tests/launcher.test.mjs",
"sha256": "99234f247e8414120d2fd214efd590e9d573eca86180d7b775cb506ac1c12c78",
"mode": "100644"
},
{
"path": "packages/cli/tests/stack-release-fixture.mjs",
"sha256": "86a3c15446d4c119206097c754cd15f5bef7b7e93c9f16e9d8696ec8c475eea9",
"mode": "100644"
},
{
"path": "packages/cli/tests/stack-release.test.mjs",
"sha256": "46cc95a6e75a89b907cdd5e9bd03853e1d1f75f1ced81084d968c5476466822e",
"mode": "100644"
},
{
"path": "packages/cli/tests/trackers-boot.test.mjs",
"sha256": "c90493e45bfa09bf0b01600aaf04d6ecd79d939357beacc1a9cd65f958bb7c22",
"mode": "100644"
},
{
"path": "packages/library/src/index.mjs",
"sha256": "ecda99dea46390b81fffa6f414d56a13737233e01d883b82cd186e0e21b6c4d1",
"mode": "100644"
},
{
"path": "packages/library/src/mirror.mjs",
"sha256": "75ce08fba0956c846f2ca9ab1b565974657925f94ac952c720e7308783946480",
"mode": "100644"
},
{
"path": "packages/library/tests/mirror.test.mjs",
"sha256": "e96473be70ece8c8232ce5325822244b593f273371b74b4126920654cde9d72c",
"mode": "100644"
},
{
"path": "packages/project/src/paths.mjs",
"sha256": "bd9debcf3f9bb92560e74f220a0b108dfe24154bc6d1fb03c5ec414356d451f9",
"mode": "100644"
},
{
"path": "packages/project/tests/project.test.mjs",
"sha256": "0ebe1862ce70f26c4a6a44304182327f9a6944d16cc87f674fbb1d32e08ac602",
"mode": "100644"
},
{
"path": "packages/seat/src/checkout.mjs",
"sha256": "859201bd192522e51c33b86ef45f73487b27cdfc48ea7e4e09870e26d056048a",
"mode": "100644"
},
{
"path": "packages/seat/tests/checkout.test.mjs",
"sha256": "663040d7c737aba6403e6397bacf1325b558cc14df3e5677f0a28f9499c45127",
"mode": "100644"
},
{
"path": "packages/webui/src/cli.mjs",
"sha256": "e98fd2fe4e621d5b4fa1ed95c4d4585469e7ab7a11dd379dc541dddb1b1a1f02",
"mode": "100644"
},
{
"path": "packages/webui/tests/library-cli.test.mjs",
"sha256": "1983912b76f01f8e6d0c564e3bbcd763c0b2c44044988b192ef827446f98556c",
"mode": "100644"
}
],
"priorSourceCommit": "e333af4dc7af3a9d3789c8c0db26a0df8bdddfee"
}
+16
View File
@@ -0,0 +1,16 @@
packages/bus/tests/library.test.mjs
packages/bus/tests/project-runtime.test.mjs
packages/business/tests/business.test.mjs
packages/business/tests/cli.test.mjs
packages/business/tests/instance-settings.test.mjs
packages/business/tests/library-projects-protected.test.mjs
packages/business/tests/resolve.test.mjs
packages/cli/tests/launcher.test.mjs
packages/cli/tests/stack-release-external.test.mjs
packages/cli/tests/stack-release.test.mjs
packages/cli/tests/stack-upgrade.test.mjs
packages/cli/tests/trackers-boot.test.mjs
packages/library/tests/mirror.test.mjs
packages/project/tests/project.test.mjs
packages/seat/tests/checkout.test.mjs
packages/webui/tests/library-cli.test.mjs
@@ -0,0 +1,229 @@
{
"base": "fefd54e13",
"landing": "f8276c5eb3ecb0aad353ee65e3dd8f7faac5dd87",
"overlaps": [
"packages/bus/src/broker.mjs",
"packages/bus/src/runtime.mjs",
"packages/business/src/business.mjs",
"packages/business/src/instance-settings.mjs",
"packages/business/tests/business.test.mjs",
"packages/business/tests/instance-settings.test.mjs",
"packages/cli/src/launcher.mjs",
"packages/cli/tests/launcher.test.mjs",
"packages/library/src/index.mjs"
],
"initialConflicts": [
"packages/bus/src/broker.mjs",
"packages/business/src/business.mjs"
],
"remainingConflicts": [],
"resolutions": {
"packages/bus/src/broker.mjs": "retain landed hostKeys constructor parameter and add library parameter",
"packages/business/src/business.mjs": "retain landed CREATIVE_DEFINITIONS import and add checkMapping import"
},
"sourcePaths": [
{
"path": "packages/bus/src/broker.mjs",
"merged": true,
"sameAsEarlyTree": false,
"earlySha256": "dca11dd58561bf3f399d18d7521aefa93f43e0ee3de9c532c1da073ce5a17466",
"sha256": "925b3dd6744565349ec421a58b3374ce3d21181e6c2d3d32ffb99c79271b9450"
},
{
"path": "packages/bus/src/runtime.mjs",
"merged": true,
"sameAsEarlyTree": false,
"earlySha256": "6f69e9fb838cf1d8ee60d1b7090395193d3dbfdc6492a401a4b72c356c371ca9",
"sha256": "c759e665514a5bcad40536bc9f979837187fe6523462b3c0a0687182885c3ac6"
},
{
"path": "packages/bus/src/server.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "207c7a76752c753ef7380f18f9ce1a4f23e8fa35142973b6358aaf0c45f44899",
"sha256": "207c7a76752c753ef7380f18f9ce1a4f23e8fa35142973b6358aaf0c45f44899"
},
{
"path": "packages/bus/tests/library.test.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "58d839709e005571b398a11abda5a50f542bab0e37bd97b72f43cde926434301",
"sha256": "58d839709e005571b398a11abda5a50f542bab0e37bd97b72f43cde926434301"
},
{
"path": "packages/business/src/business.mjs",
"merged": true,
"sameAsEarlyTree": false,
"earlySha256": "e132f92211cd35f951e85ff5e90471a2316755cb35ad7eabf516e780ce6c13ad",
"sha256": "5fd3fee664d3e70153d988b4c1a42406f888a31b3927fa52375e66619d8cec59"
},
{
"path": "packages/business/src/cli.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "31cfe9090900169416e1207dc6db865b046439273917e9de442add4159c89eb3",
"sha256": "31cfe9090900169416e1207dc6db865b046439273917e9de442add4159c89eb3"
},
{
"path": "packages/business/src/instance-settings.mjs",
"merged": true,
"sameAsEarlyTree": false,
"earlySha256": "04d61c1ec89a5d67da75aede46c3a8e2197aadd63b11bef8e4fc37d90e8862cf",
"sha256": "3873411688d927336a909d0299acddeba92a653102c660d276471f5a0f6eebd3"
},
{
"path": "packages/business/src/organization.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "eaa650499d2d078552a2c0e5b13a6126e0aca02f73160032a7325b3f5aa90964",
"sha256": "eaa650499d2d078552a2c0e5b13a6126e0aca02f73160032a7325b3f5aa90964"
},
{
"path": "packages/business/src/resolve.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "712183465fee099606223d84423afc54e3386a0c8e468c68d5ac39c1e676d109",
"sha256": "712183465fee099606223d84423afc54e3386a0c8e468c68d5ac39c1e676d109"
},
{
"path": "packages/business/tests/business.test.mjs",
"merged": true,
"sameAsEarlyTree": false,
"earlySha256": "55aafd3f0eea816d5f42283b7b74323dd32a698d7b95dd9cc54529dbf7b16058",
"sha256": "910f924d90b6f9b18c1f36d97cebd0b7a0a15a080b73530c4fb847bfd435fb3f"
},
{
"path": "packages/business/tests/cli.test.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "d54ba2338bcdfec22764030cf3ec9e3464ab275b15321d6d1b039169ea37fd85",
"sha256": "d54ba2338bcdfec22764030cf3ec9e3464ab275b15321d6d1b039169ea37fd85"
},
{
"path": "packages/business/tests/instance-settings.test.mjs",
"merged": true,
"sameAsEarlyTree": false,
"earlySha256": "c0959f3af3df653bbf469471d3bbe3f28bc4478cba8ba57910af7f09498c4c3f",
"sha256": "072e25c5237e1be4294c7a4a95daa2ab593832786013e1283b19725c330647c1"
},
{
"path": "packages/business/tests/library-projects-protected.test.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "cfc726ec7bf73c1dc9fc6395cba85e374b7a006bac45bd8c47c969550557147f",
"sha256": "cfc726ec7bf73c1dc9fc6395cba85e374b7a006bac45bd8c47c969550557147f"
},
{
"path": "packages/business/tests/resolve.test.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "c74af44a83ee918196e2c1016b62786e757c0ae774c9b1613280d3fbbf3ffb22",
"sha256": "c74af44a83ee918196e2c1016b62786e757c0ae774c9b1613280d3fbbf3ffb22"
},
{
"path": "packages/cli/src/config.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "ab7b9f452123b6f7e98f4f59cbb3cc4483fd296e4af5522412410ee53d1b20e0",
"sha256": "ab7b9f452123b6f7e98f4f59cbb3cc4483fd296e4af5522412410ee53d1b20e0"
},
{
"path": "packages/cli/src/launcher.mjs",
"merged": true,
"sameAsEarlyTree": false,
"earlySha256": "2272232dbe6c362eddb23ad903987fd4b8e0ca1eb94dbe93b66b3e785e7d0712",
"sha256": "d8a870d8eb8254adfd576688696705359fefbf517b40e5d7482500bf27941022"
},
{
"path": "packages/cli/tests/launcher.test.mjs",
"merged": true,
"sameAsEarlyTree": false,
"earlySha256": "38fb51c15764333642d75db7fb358d611e448049cf894b24686ea6cf2cf169c4",
"sha256": "99234f247e8414120d2fd214efd590e9d573eca86180d7b775cb506ac1c12c78"
},
{
"path": "packages/cli/tests/stack-release-fixture.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "86a3c15446d4c119206097c754cd15f5bef7b7e93c9f16e9d8696ec8c475eea9",
"sha256": "86a3c15446d4c119206097c754cd15f5bef7b7e93c9f16e9d8696ec8c475eea9"
},
{
"path": "packages/cli/tests/stack-release.test.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "aa63fadca8b3a51e7af1ff7560fd2645f3ef56bf4e5625b2d5a5f6946db40996",
"sha256": "aa63fadca8b3a51e7af1ff7560fd2645f3ef56bf4e5625b2d5a5f6946db40996"
},
{
"path": "packages/cli/tests/trackers-boot.test.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "c90493e45bfa09bf0b01600aaf04d6ecd79d939357beacc1a9cd65f958bb7c22",
"sha256": "c90493e45bfa09bf0b01600aaf04d6ecd79d939357beacc1a9cd65f958bb7c22"
},
{
"path": "packages/library/src/index.mjs",
"merged": true,
"sameAsEarlyTree": false,
"earlySha256": "b80e6c27daf908a0e71fcc1e5e8c91d05e857790bd77d92e06ff086ab3623306",
"sha256": "ecda99dea46390b81fffa6f414d56a13737233e01d883b82cd186e0e21b6c4d1"
},
{
"path": "packages/library/src/mirror.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "75ce08fba0956c846f2ca9ab1b565974657925f94ac952c720e7308783946480",
"sha256": "75ce08fba0956c846f2ca9ab1b565974657925f94ac952c720e7308783946480"
},
{
"path": "packages/library/tests/mirror.test.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "e96473be70ece8c8232ce5325822244b593f273371b74b4126920654cde9d72c",
"sha256": "e96473be70ece8c8232ce5325822244b593f273371b74b4126920654cde9d72c"
},
{
"path": "packages/project/src/paths.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "bd9debcf3f9bb92560e74f220a0b108dfe24154bc6d1fb03c5ec414356d451f9",
"sha256": "bd9debcf3f9bb92560e74f220a0b108dfe24154bc6d1fb03c5ec414356d451f9"
},
{
"path": "packages/project/tests/project.test.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "0ebe1862ce70f26c4a6a44304182327f9a6944d16cc87f674fbb1d32e08ac602",
"sha256": "0ebe1862ce70f26c4a6a44304182327f9a6944d16cc87f674fbb1d32e08ac602"
},
{
"path": "packages/seat/src/checkout.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "859201bd192522e51c33b86ef45f73487b27cdfc48ea7e4e09870e26d056048a",
"sha256": "859201bd192522e51c33b86ef45f73487b27cdfc48ea7e4e09870e26d056048a"
},
{
"path": "packages/seat/tests/checkout.test.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "663040d7c737aba6403e6397bacf1325b558cc14df3e5677f0a28f9499c45127",
"sha256": "663040d7c737aba6403e6397bacf1325b558cc14df3e5677f0a28f9499c45127"
},
{
"path": "packages/webui/src/cli.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "e98fd2fe4e621d5b4fa1ed95c4d4585469e7ab7a11dd379dc541dddb1b1a1f02",
"sha256": "e98fd2fe4e621d5b4fa1ed95c4d4585469e7ab7a11dd379dc541dddb1b1a1f02"
},
{
"path": "packages/webui/tests/library-cli.test.mjs",
"merged": false,
"sameAsEarlyTree": true,
"earlySha256": "1983912b76f01f8e6d0c564e3bbcd763c0b2c44044988b192ef827446f98556c",
"sha256": "1983912b76f01f8e6d0c564e3bbcd763c0b2c44044988b192ef827446f98556c"
}
]
}
@@ -0,0 +1,155 @@
diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs
index 3baa709f9..512d57519 100644
--- a/packages/bus/src/broker.mjs
+++ b/packages/bus/src/broker.mjs
@@ -90,0 +91 @@ export const PROJECT_FILE_VERBS = Object.freeze(['project.files', 'project.file'
+export const LIBRARY_VERBS = Object.freeze(['library.file', 'library.status']);
@@ -281,0 +283 @@ export class Broker {
+ #library;
@@ -297 +299 @@ export class Broker {
- constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [], organization = null, project = null, usage = null, limits = null, harnessOn = null, credentialVerbs = null, hostGrant = null, hostKeys = null }) {
+ constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [], organization = null, project = null, library = null, usage = null, limits = null, harnessOn = null, credentialVerbs = null, hostGrant = null, hostKeys = null }) {
@@ -308,0 +311 @@ export class Broker {
+ this.#library = library;
@@ -2189,0 +2193,2 @@ export class Broker {
+ if (LIBRARY_VERBS.includes(request.verb))
+ return this.#requestLibrary(s, { verb: request.verb, args: structuredClone(args) });
@@ -2438,0 +2444,20 @@ export class Broker {
+ // C2-04 M4: only a verified human reads library text or status. Host Git
+ // functions stay injected, preserving this module's sandbox import closure.
+ async #requestLibrary(s, request) {
+ const { verb, args } = request;
+ try {
+ this.#human(s);
+ const fields = verb === 'library.file' ? ['project', 'path'] : ['project'];
+ keys(args, fields, fields);
+ if (!this.#library?.readFile || !this.#library?.status) fail('project-not-configured');
+ const context = { business: this.#business(s.business), project: args.project };
+ const result = verb === 'library.file'
+ ? await this.#library.readFile({ ...context, path: args.path })
+ : await this.#library.status(context);
+ this.#secretCheck(result);
+ return result;
+ } catch (error) {
+ const Failure = this.#library?.LibraryError;
+ throw this.#refused(s, Failure && error instanceof Failure ? new BusError(error.code) : error, request);
+ }
+ }
diff --git a/packages/bus/src/runtime.mjs b/packages/bus/src/runtime.mjs
index f81f956f2..e265ff5d1 100644
--- a/packages/bus/src/runtime.mjs
+++ b/packages/bus/src/runtime.mjs
@@ -5,0 +6 @@ import * as projectModule from '../../project/src/index.mjs';
+import * as libraryModule from '../../library/src/index.mjs';
@@ -95 +96 @@ export async function startBroker({
- ...Object.values(businesses).flatMap((b) => Object.values(b.projects ?? {}).map((p) => p.root)),
+ ...Object.values(businesses).flatMap((b) => Object.values(b.projects ?? {}).filter(p => p.kind !== 'library').map((p) => p.root)),
@@ -130,0 +132,5 @@ export async function startBroker({
+ // C2-04: mirrors are host state, never session checkouts. An unsafe
+ // existing mirror refuses startup before a socket can become ready.
+ for (const business of Object.values(businesses))
+ for (const [project, entry] of Object.entries(business.projects ?? {}))
+ if (entry.kind === 'library') await libraryModule.ensureMirror({ business, project, dataRoot });
@@ -138,0 +145,5 @@ export async function startBroker({
+ library: {
+ readFile: context => libraryModule.readLibraryFile({ ...context, dataRoot }),
+ status: context => libraryModule.libraryStatus({ ...context, dataRoot }),
+ LibraryError: libraryModule.LibraryError,
+ },
diff --git a/packages/business/src/business.mjs b/packages/business/src/business.mjs
index d08a4f52e..e8337ea2b 100644
--- a/packages/business/src/business.mjs
+++ b/packages/business/src/business.mjs
@@ -12,0 +13 @@ import { checkPlugins } from "../../plugins/src/kinds.mjs";
+import { checkMapping } from "../../library/src/mapping.mjs";
@@ -241,0 +243,4 @@ export function validateBusinessDocument(document, file, { rolesDir }) {
+ if (entry.kind === 'library') {
+ projects[id] = checkMapping(entry, `${file} projects.${id}`, { fail: (code, message) => refuse(`${code}: ${message}`) });
+ continue;
+ }
@@ -257,0 +263 @@ export function validateBusinessDocument(document, file, { rolesDir }) {
+ if (projects[project].kind === 'library') refuse(`${file} roles.${instance}.publish names library project ${project}; library repositories cannot be published by agents`);
diff --git a/packages/business/src/instance-settings.mjs b/packages/business/src/instance-settings.mjs
index ff3a6d9d3..50cc36edc 100644
--- a/packages/business/src/instance-settings.mjs
+++ b/packages/business/src/instance-settings.mjs
@@ -149,0 +150 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null })
+ if (entry.kind === 'library') continue;
diff --git a/packages/business/tests/business.test.mjs b/packages/business/tests/business.test.mjs
index b64659fa1..0f214a005 100644
--- a/packages/business/tests/business.test.mjs
+++ b/packages/business/tests/business.test.mjs
@@ -10,0 +11,30 @@ import { launchDoc, businessDoc, REPO, REPO_ROLES, rolesCopy, tmp, writeJson } f
+test('C2-04: library mappings load without roots, remain frozen, and retain mapping refusals', async t => {
+ const { mappingEntry } = await import('../../library/tests/helpers.mjs');
+ const { rmSync } = await import('node:fs');
+ const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true }));
+ const doc = businessDoc(root), file = join(root, 'businesses/acme.json');
+ doc.projects = { books: mappingEntry(), ...doc.projects };
+ const loaded = validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES });
+ const { checkMapping } = await import('../../library/src/mapping.mjs');
+ assert.deepEqual(loaded.projects.books, checkMapping(mappingEntry()));
+ assert.equal(loaded.projects.books.root, undefined);
+ assert.ok(Object.isFrozen(loaded.projects.books.universes.harbor.books));
+ const only = structuredClone(doc); delete only.projects.stack;
+ assert.deepEqual(Object.keys(validateBusinessDocument(only, file, { rolesDir: REPO_ROLES }).projects), ['books']);
+ doc.projects.books.universes.harbor.playground = ['Elsewhere/Playground/'];
+ refuses(() => validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }), /library-playground-outside-book/);
+ doc.projects.books = { ...mappingEntry(), root: '/forbidden' };
+ refuses(() => validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }), /library-mapping-invalid/);
+});
+
+test('C2-04: an otherwise valid publish grant cannot name a library repo', async t => {
+ const { mappingEntry } = await import('../../library/tests/helpers.mjs');
+ const { rmSync } = await import('node:fs');
+ const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true }));
+ const doc = businessDoc(root);
+ doc.projects.books = { ...mappingEntry(), remote: 'https://git.example/writer/books' };
+ doc.roles.coder.identity = { name: 'Coder', email: '[email protected]', gitea: { url: 'https://git.example', user: 'acme-coder' } };
+ doc.roles.coder.publish = ['books'];
+ refuses(() => validateBusinessDocument(doc, join(root, 'businesses/acme.json'), { rolesDir: REPO_ROLES }), /publish.*library/);
+});
+
diff --git a/packages/business/tests/instance-settings.test.mjs b/packages/business/tests/instance-settings.test.mjs
index 6b19f9e89..a6c8fe665 100644
--- a/packages/business/tests/instance-settings.test.mjs
+++ b/packages/business/tests/instance-settings.test.mjs
@@ -22,0 +23,10 @@ const LEVELS = { pi: ["off", "minimal", "low", "medium", "high", "xhigh", "max"]
+test('C2-04: instance settings skip a library while checking a later code project', async () => {
+ const { mappingEntry } = await import('../../library/tests/helpers.mjs');
+ const f = fixture(doc => { doc.projects = { books: mappingEntry(), ...doc.projects }; });
+ writeJson(projectFilePath(f.doc.projects.stack.root), { projectVersion: 1, id: 'stack', vars: { 'tracker.project': 3 } });
+ const before = f.settings.read('acme');
+ f.settings.set('acme', { revision: before.revision, instance: 'gate-215', op: 'add', definition: 'merge-gate' }, 'jason');
+ assert.deepEqual(JSON.parse(readFileSync(f.file, 'utf8')).projects.books, mappingEntry());
+ assert.ok(f.settings.read('acme').instances['gate-215']);
+});
+
diff --git a/packages/cli/src/launcher.mjs b/packages/cli/src/launcher.mjs
index 604cef203..c721ee9e8 100644
--- a/packages/cli/src/launcher.mjs
+++ b/packages/cli/src/launcher.mjs
@@ -354 +354 @@ export function createLauncher({
- checkouts = await ensureCheckouts({ workspace, projects: b.projects, env });
+ checkouts = await ensureCheckouts({ workspace, projects: Object.fromEntries(Object.entries(b.projects).filter(([, entry]) => entry.kind !== 'library')), env });
diff --git a/packages/cli/tests/launcher.test.mjs b/packages/cli/tests/launcher.test.mjs
index 3b663ba94..4636df3de 100644
--- a/packages/cli/tests/launcher.test.mjs
+++ b/packages/cli/tests/launcher.test.mjs
@@ -32,0 +33,10 @@ const skip = sandboxProblem() ?? false;
+test('C2-04: launcher workspace clones code but never the library', { skip, timeout: 30000 }, async t => {
+ const { mappingEntry } = await import('../../library/tests/helpers.mjs');
+ const f = await setup(t, doc => { doc.projects = { books: mappingEntry(), ...doc.projects }; return doc; });
+ await f.launcher.launchPm();
+ const workspace = workspaceDir(f.f.dataRoot, 'acme', 'chief');
+ assert.equal(existsSync(join(workspace, 'stack', '.git')), true);
+ assert.equal(existsSync(join(workspace, 'books')), false);
+ assert.equal(await f.close(), 0);
+});
+
diff --git a/packages/library/src/index.mjs b/packages/library/src/index.mjs
index f64d77860..53ce5cb43 100644
--- a/packages/library/src/index.mjs
+++ b/packages/library/src/index.mjs
@@ -8,0 +9 @@ export { survey, formatSurvey, surveyMain } from './survey.mjs';
+export { SYNC_STATE_FILE, mirrorPath, ensureMirror, withLibraryLock, servedCommit, readSyncState, libraryStatus, readLibraryFile } from './mirror.mjs';
@@ -0,0 +1,14 @@
✔ C2-04 M4: both verbs reject every agent before arguments or adapter access (30.144386ms)
✔ C2-04 bus: human reads validate keys and return adapter results without path leakage (11.434614ms)
✔ C2-04 socket: each async library verb resolves its result rather than serializing {} (17.684117ms)
✔ C2-04 runtime: protectedRoots skips library but retains real code-root credential protection (45.253455ms)
✔ C2-04 runtime: unsafe existing mirror refuses boot and never publishes a socket (23.246812ms)
✔ C2-04 runtime: real library adapters reach the socket and lock waits leave other bus reads responsive (107.052077ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 359.869493
@@ -0,0 +1,11 @@
✔ Broker keeps the sandbox client import closure: only the listed imports, no lazy project import (2.265255ms)
✔ without a trusted project adapter all three async paths refuse project-not-configured (57.448982ms)
✔ real startBroker injects the project adapter for socket reads and bound-author record writes (102.784823ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 312.717335
@@ -0,0 +1,27 @@
✔ C2-04: library mappings load without roots, remain frozen, and retain mapping refusals (12.852402ms)
✔ C2-04: an otherwise valid publish grant cannot name a library repo (2.693014ms)
✔ config directory and file path follow MOSAIC_CONFIG (0.229245ms)
✔ the fixture business validates and comes back frozen (1.006969ms)
✔ two instances may share a definition (0.94271ms)
✔ top-level refusals (3.474817ms)
✔ arbiters and projects (5.543282ms)
✔ role instances (2.7386ms)
✔ Vikunja bots (6.181044ms)
✔ a role without Vikunja takes no tracker block (5.900981ms)
✔ credential references match the definition's services (3.012597ms)
✔ launch (8.561546ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 launch.threadIdleSeconds: absent is the default, a positive integer is used, anything else refuses (6.420869ms)
✔ loadBusiness: file checks (2.241353ms)
✔ loadBusiness: not a regular file (46.573969ms)
✔ loading writes nothing (2.802338ms)
✔ names that are Object.prototype properties don't count as declared (3.291238ms)
✔ the shipped example refuses as written and validates once filled in (1.195924ms)
✔ persona vars: agent layer, creative definition only, both or neither (9.0736ms)
ℹ tests 19
ℹ suites 0
ℹ pass 19
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 199.91256
@@ -0,0 +1,18 @@
✔ C2-04: business validate and resolve skip a library before code projects and credential checks (235.663956ms)
✔ usage errors exit 4 (325.805811ms)
✔ validate: a good business exits 0 and prints instance digests (69.331646ms)
✔ validate: project files (344.919464ms)
✔ validate: missing files and a broken system config (252.779029ms)
✔ validate: credential reference problems exit 2 and name each one (66.663076ms)
✔ validate: a token file inside the repository is refused (78.714732ms)
✔ validate: role definitions come from MOSAIC_ROLES_DIR (201.487424ms)
✔ resolve: prints one instance's record (281.684478ms)
✔ resolve: refusals (454.610462ms)
ℹ tests 10
ℹ suites 0
ℹ pass 10
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2381.258516
@@ -0,0 +1,21 @@
✔ C2-04: instance settings skip a library while checking a later code project (32.527878ms)
✔ row 165 read lists each instance and the definitions add accepts (3.218296ms)
✔ row 165 add: two instances join, validate exits 0, the log names the human, and the bus restart is reported (93.702292ms)
✔ row 165 change: definition, vars and launch membership log before and after; only a definition change asks for a restart (99.793939ms)
✔ row 165 refuses each validation failure and leaves the file's bytes alone (33.820979ms)
✔ row 165 refuses an added instance whose definition declares credentials, and says why (5.999086ms)
✔ row 165 refuses a definition change to or from one that declares credentials (6.767714ms)
✔ row 165 a vars or launch change on an instance with its binding succeeds and keeps the binding (89.83786ms)
✔ row 165 refuses an END_PROTECTED definition and every change to a protected instance (9.793219ms)
✔ row 165 refuses while a project file doesn't resolve, as validate would (15.487298ms)
✔ row 165 refuses a stale revision and leaves the file's bytes alone (10.871014ms)
✔ row 65 an instance write refuses harness-off, thinking-unsupported, an unknown model and a partial fallback (38.02395ms)
✔ row 213 a creative instance is the human's: add, vars and definition changes, persona digest only (90.045141ms)
ℹ tests 13
ℹ suites 0
ℹ pass 13
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 608.08011
@@ -0,0 +1,12 @@
✔ C2-04 M9: changed playground and hybrid entries leave business and changes.jsonl byte-identical (23.242034ms)
✔ C2-04 M9: code projects, additions, deletions and array changes are protected too (15.756145ms)
✔ C2-04 M9: canonical key order is allowed and a non-project setter still records its change (17.060499ms)
✔ C2-04 M9: a serialization hook cannot change projects after the canonical comparison (9.833495ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 118.54831
@@ -0,0 +1,19 @@
✔ C2-04: a library first in projects does not hide a later project layer (9.584516ms)
✔ systemVars flattens the validated config (1.063324ms)
✔ precedence: system, business, project, project role, agent (2.001802ms)
✔ limits narrow the definition and never widen it (2.563505ms)
✔ role.launch stays within-role only for the instance the launch block names (5.04696ms)
✔ limits.authority without role.launch leaves the launcher with no launch block (2.106162ms)
✔ limits.authority narrows cross-role actions too (1.098761ms)
✔ classify (1.065895ms)
✔ the record carries what the broker and launcher need (0.966677ms)
✔ digest: key order doesn't matter, any value change does (7.43546ms)
✔ refusals (2.150825ms)
ℹ tests 11
ℹ suites 0
ℹ pass 11
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 94.022587
@@ -0,0 +1,54 @@
✔ C2-04: launcher workspace clones code but never the library (1298.398732ms)
✔ bus start --pm: the launcher instance (the chief of staff) runs under its own PID namespace, registered, with a manifest (1264.618734ms)
usage: {"event":"usage-unknown","business":"acme","run":"r4accc4841d41","instance":"chief","turn":"81814ac7-9330-484a-95f8-cc2f80a9aeef","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r0719029c31b1","instance":"coder","turn":"fd2dfc33-9ab6-4d4d-9eef-ba46b6d7a510","source":"@none","code":"baseline-unknown"}
✔ the chief of staff launches a coder through its launch tool; the coder answers; refusals name their code (657.713119ms)
usage: {"event":"usage-unknown","business":"acme","run":"r4b37f983290f","instance":"chief","turn":"6c8768d3-4742-4ae2-87c6-4653182c8520","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r7bc8dc8db66c","instance":"pm","turn":"4b196787-1bf9-4702-82d2-397c84ab1e3c","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r4b37f983290f","instance":"chief","turn":"1c6ec26d-97da-43b6-8cd8-91994e6c7c7b","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r7bc8dc8db66c","instance":"pm","turn":"a12fdeb9-e25d-45a4-bd25-e048621a3557","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r7bc8dc8db66c","instance":"pm","turn":"ad419687-0675-48a8-9932-90b2da5333ef","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r4b37f983290f","instance":"chief","turn":"0d54926f-f5b8-4dbb-b38a-8ea23fdd5cb5","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r7bc8dc8db66c","instance":"pm","turn":"6891e20c-472e-4e67-815e-88728024acf4","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r4b37f983290f","instance":"chief","turn":"9ac6fe37-9044-4f54-bae0-826a6c84c15b","source":"@none","code":"baseline-unknown"}
✔ a launched pm's request_staff reaches the chief of staff; no other session has the tool and the launch limits still hold (1202.248105ms)
✔ each launch makes sure the workspace has a clone of the project: cloned, fast-forwarded, or fetched and logged (3926.301471ms)
✔ a role the broker says holds a merge decision gets land_work, its prompt line, its landing and the git socket; without it, none (593.731622ms)
✔ REQ-LAUNCH-1 the tick starts a listed merge decision holder once, with land_work, the Landing line and its landing turn; an unlisted holder starts nothing (755.589469ms)
usage: {"event":"usage-unknown","business":"acme","run":"rf7ecf1ef2b65","instance":"coder","turn":"744cb116-e163-4b18-831e-556df7e7bfd9","source":"@none","code":"baseline-unknown"}
✔ a holder running since before the resolution is stopped once the broker releases it, land-relaunch, and the next tick starts it: landing turn first, then the message that waited (739.551352ms)
✔ a landing run that ended isn't started again for its decision, a spent decision starts nothing, and a new decision starts the holder (564.790867ms)
✔ after a restart, the new host's first tick starts a holder whose decision is still held (883.94802ms)
✔ a refused landing start is logged once per code with its decision, isn't a start, and the next tick with room starts the holder (595.491854ms)
✔ a project clone that can't be made refuses checkout-failed, from --chief and from a session, with no run and no clone left (250.651304ms)
✔ a runner that stops at once ends its launch with the runner's reason (336.620423ms)
✔ a Claude Code launch with no session credential refuses before it starts (172.971844ms)
✔ a pi launch needs no session credential: pi logs in through its own ENV_ALLOW variables (287.021469ms)
usage: {"event":"plugin-refused","kind":"harness","name":"down","code":"missing","message":"/home/jwoltje/.t3/worktrees/mosaic-stack/herb-row215-on-f827/packages/plugins/harness/down is missing","kept":[]}
plugins: {"event":"plugin-refused","kind":"harness","name":"down","code":"missing","message":"/home/jwoltje/.t3/worktrees/mosaic-stack/herb-row215-on-f827/packages/plugins/harness/down is missing","kept":[]}
✔ row 65 a failed primary preflight launches the fallback on a fixture harness module, recording the route and the reason (327.373038ms)
✔ a runner killed from the host ends its launch as killed, not failed (381.927989ms)
✔ a missing or failing bwrap refuses sandbox-unavailable before anything starts; there is no unconfined fallback (198.778768ms)
✔ row 64 a role skill that isn't enabled refuses skill-not-enabled, and one that isn't a plain directory skill-refused, before anything starts (184.44803ms)
✔ row 213 a creative instance's persona is read from <configDir>/personas: off its digest the launch fails with the mismatch as its reason (239.197527ms)
✔ row 64 on Claude Code a role skill that isn't enabled refuses the same way, before anything starts (230.64829ms)
usage: {"event":"usage-unknown","business":"acme","run":"r800039251a09","instance":"chief","turn":"f2e6a1ad-767e-4316-be90-6194b840e2ef","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"rfeeb081c2983","instance":"coder","turn":"1abdb906-521d-4100-897b-f5eba12663cf","source":"@none","code":"baseline-unknown"}
✔ a real Claude Code coder's shell can't read a stand-in secret or anything else its sandbox doesn't hold (1612.164523ms)
✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (909.716449ms)
✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3844.434582ms)
✔ bus start --no-launch leaves an earlier host's live session alone: no signal, no end, sessions.json as it was (224.265713ms)
✔ bus start --no-launch refuses every launch: the PM launch and a session's request, each logged, nothing spawned (193.925449ms)
✔ bus start --no-launch starts no thread tick; the same host with launches runs it (1181.783938ms)
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (166.842635ms)
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (171.101521ms)
✔ a launch client that never closes its side doesn't hold the host's close (170.266667ms)
✔ a runner that ignores SIGTERM is killed when the host closes (1305.753454ms)
ℹ tests 31
ℹ suites 0
ℹ pass 31
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 24989.261794
@@ -0,0 +1,33 @@
✔ row 232: confirm, revert, gate, --to and --expect-from need --units external; the user-only flags are usage under it (1129.148785ms)
✔ row 232: pm-on and activate --pm-after-harness refuse external-pm (1112.631619ms)
✔ row 232: units-running (N4) refuses while a unit is active and leaves the store, the log and TAKEN; failed counts as down (2301.278572ms)
✔ row 232: activate stages: the pointer stays, AWAITING-UNITS names TO, a backup of the stopped store, no directory in the set, no --user, no journal (2229.906955ms)
✔ row 232: A3 order: capture-failed and marker-failed refuse with exit 3 and leave no marker or FAILED (1913.7476ms)
✔ row 232: confirm and revert refuse open-activation with no marker, two, the wrong copy, or one the log holds closed; refusals name no activation (3892.845676ms)
✔ row 232: confirm polls: bus status good after 3 s fails with --timeout 1, writes FAILED and leaves the rest; FAILED then refuses activation-failed (3703.021367ms)
✔ row 232: confirm polls to a pass with --timeout 10: the pointer, an ok line that reads as the started run, no marker (6350.955178ms)
✔ row 232: stale-start: @1792540799 fails a start floor of 1792540800 with stale-start (3365.187863ms)
✔ row 232: stale-start is a gate code: an enter time that turns to @1792540800 after 2 s passes with --timeout 10 (4713.287991ms)
✔ row 232: gate (N2, A2f): --since 1792540800 passes @1792540800 and fails @1792540799; it writes nothing (3014.18649ms)
✔ row 232: the external PM check reads launches list: one live run of launch.by since the start passes (7715.019003ms)
✔ row 232: revert puts the wrapper back and leaves the store when nothing migrated (1540.240269ms)
✔ row 232: confirm --rollback refuses rollback-mismatch, and logs rollback-failed with exit 2 when OLD fails its gate (3838.859298ms)
✔ row 232: confirm --rollback after a --restore swap logs rollbackCode not-attempted; rollback --restore finds an external backup (4385.316411ms)
✔ row 232: sequence 1: activate, confirm times out, revert, confirm --rollback --cause A5: gate-failed rollback ok (5668.17255ms)
✔ row 232: sequence 2: activate --migrate, the wrapper install fails, revert restores TAKEN, confirm --rollback --cause A3 (3356.512703ms)
✔ row 232: sequence 3: activate, root's A4 fails, revert, confirm --rollback --cause A4: failed code units-start (3090.998502ms)
✔ row 232: sequence 4: a capture failure leaves nothing open, then A2f's gate OLD writes nothing (2006.820211ms)
✔ row 232: sequence 5: a confirm of the wrong copy is refused, and revert and confirm --rollback still close the activation (3716.790879ms)
✔ row 232: sequence 6: a revert that fails leaves the activation open, and the next --plan refuses interrupted-run (2084.865556ms)
✔ row 232: sequence 7: adopt, two confirmed activates, rollback --to the previous copy, confirmed, logs op rollback (8859.407093ms)
✔ row 232: sequence 8: a migration that writes and fails: revert restores the backup byte for byte, confirm --rollback --cause A3 (4066.89509ms)
✔ row 232: user mode: every systemctl call carries --user, and activate, rollback and check make the base's calls (8472.325246ms)
✔ row 232: user mode: a default activate --plan prints the base's bytes (2725.266801ms)
ℹ tests 25
ℹ suites 0
ℹ pass 25
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 96332.465778
@@ -0,0 +1,227 @@
✔ T1 adopt writes the pointer from the units once; a failing check refuses check-failed (1604.406515ms)
✔ T2 activate installs exactly the pointer's pm, claude and Console flags (2285.234425ms)
▶ T3 a hand-edited unit refuses unit-flags-drift, names the difference and changes nothing
✔ pm (1074.84155ms)
✔ claude (1048.597035ms)
✔ console (1069.472197ms)
✔ T3 a hand-edited unit refuses unit-flags-drift, names the difference and changes nothing (3193.872973ms)
▶ T4 each item 3 refusal changes nothing and logs one line with its code
✔ copy-missing (970.101799ms)
✔ copy-unreachable (976.867501ms)
✔ copy-schema-unknown (942.411453ms)
✔ already-active (966.01391ms)
✔ instance-busy (another business active) (1033.865857ms)
✔ instance-busy (this business activating) (989.106934ms)
✔ drop-in-override (1051.252734ms)
✔ no-pointer (535.256184ms)
✔ T4 each item 3 refusal changes nothing and logs one line with its code (7466.71817ms)
✔ T5 an activate with no migration stops the Console then the bus, installs, starts the bus then the Console (2298.616234ms)
✔ T6 a migration without --migrate rolls back with migration-needs-flag (2085.91559ms)
✔ T7 --migrate takes the backup before the migration runs (2415.592569ms)
✔ T8 a failed migration restores the backup and keeps the store it replaced (2571.261148ms)
✔ T9 a store outside the target's chain rolls back with store-newer (2405.058696ms)
▶ T10 each gate check failing alone rolls back and puts every file back
✔ units-inactive (5907.690999ms)
✔ bus-status (6093.058453ms)
✔ healthz (6393.330472ms)
✔ copy-check (5442.480521ms)
✔ pm-launch-unconfirmed (5607.705975ms)
✔ T10 each gate check failing alone rolls back and puts every file back (29445.44482ms)
▶ T10b the PM check needs exactly one launched line for a live run of this start
✔ no launched line (5534.978468ms)
✔ two lines (5372.975087ms)
✔ a changed word (5420.826895ms)
✔ run not in launches list (5601.520776ms)
✔ run started before the start (5448.208485ms)
✔ a human launch with no journal line (5346.70067ms)
✔ run for another business (5684.948726ms)
✔ run not live (5860.307931ms)
✔ one well-formed line and a live run pass (2078.4402ms)
✔ T10b the PM check needs exactly one launched line for a live run of this start (46350.52861ms)
✔ T11 a rollback whose gate fails too: rollback-failed, exit 2, both units stopped, no retry (7145.208417ms)
✔ T12 rollback takes the previous copy from the log (3164.021139ms)
✔ T13 a rollback across a migration needs --restore, then restores and goes live (3796.056873ms)
✔ T14 events written after the backup need --events N with the right N (4384.802883ms)
✔ T14b a rollback --restore whose gate fails stops both units and skips item 6 (5813.087127ms)
✔ T14c a rollback without --restore whose gate fails runs item 6 (6237.609182ms)
✔ T15 --plan names the migration and the restore and changes nothing (2849.139286ms)
✔ T26 --plan lists the commits an activate adds, after the migration lines (1481.868533ms)
✔ T27 --plan names each migration step, or says the steps cannot be read (1173.748951ms)
✔ T25 --plan refuses copies-unrelated when neither copy is an ancestor of the other (569.344968ms)
▶ T23 --plan reads the store without changing a file, stopped or held
✔ stopped store (721.626407ms)
✔ held store (690.820461ms)
✔ T23 --plan reads the store without changing a file, stopped or held (1413.175682ms)
✔ T24 business-mismatch and capture-failed refuse with exit 3; a short id is a usage error (1463.46361ms)
✔ T16 status prints the pointer, the log, the check, bus status and drift, and writes nothing (910.07629ms)
✔ T19 a started line with no final line: status shows it and every changing verb refuses (1271.59212ms)
✔ T20 a held run lock refuses run-locked without a log line; status and --plan still run (1515.929929ms)
✔ T21 a bus unit with no MOSAIC_CLAUDE_BIN: adopt and activate refuse no-claude-pin (1789.2511ms)
▶ T22 the stopped store read: a -wal or -shm refuses store-unclean and is left alone; a clean read adds no file
✔ bus.sqlite-wal (1954.988144ms)
✔ bus.sqlite-shm (2042.456191ms)
✔ clean (2209.768749ms)
✔ T22 the stopped store read: a -wal or -shm refuses store-unclean and is left alone; a clean read adds no file (6208.13226ms)
▶ T28 a unit path, drop-in directory or drop-in file that cannot be read, or a relative unit path, refuses drop-in-override before any stop
✔ unit path (938.058563ms)
✔ drop-in directory (934.896589ms)
✔ drop-in file (958.129787ms)
✔ relative unit path (943.563721ms)
✔ T28 a unit path, drop-in directory or drop-in file that cannot be read, or a relative unit path, refuses drop-in-override before any stop (3775.662901ms)
▶ T29 a bus that stops during the held-store read: --plan says so, rollback --restore refuses restore-mismatch
✔ --plan (932.393863ms)
✔ rollback --restore (2879.958858ms)
✔ T29 a bus that stops during the held-store read: --plan says so, rollback --restore refuses restore-mismatch (3812.923223ms)
✔ T30 a copy check that agrees on another copy fails the gate with copy-check and rolls back (6646.085499ms)
▶ T31 a quoted or escaped Environment= or ExecStart= is unreadable: refusals by name, nothing read from /a
✔ Environment double quotes (1976.227811ms)
✔ Environment single quotes (2063.211816ms)
✔ Environment backslash (2038.183771ms)
✔ ExecStart quotes (1984.451614ms)
✔ Console ExecStart quotes (1980.466835ms)
✔ T31 a quoted or escaped Environment= or ExecStart= is unreadable: refusals by name, nothing read from /a (10043.710809ms)
▶ T32 item 1: --pm-after-harness refuses a target without harness or a file that sets it, and passes without the PM
✔ usage: not with --pm, and only for activate (721.955423ms)
✔ pm-after-harness-target (1015.757752ms)
✔ pm-after-harness-set (1371.242025ms)
✔ a pointer without pm (841.537689ms)
✔ phase 1 installs without --pm, skips pm_launched and writes the pending pointer (1915.010297ms)
✔ T32 item 1: --pm-after-harness refuses a target without harness or a file that sets it, and passes without the PM (5866.555622ms)
▶ T33 item 2: read_pointer takes pmPending only as true beside pm false; a pending pointer refuses activate and shows in status
✔ read_pointer (1570.154285ms)
✔ pm-pending, already-adopted and the status line (3037.065347ms)
✔ T33 item 2: read_pointer takes pmPending only as true beside pm false; a pending pointer refuses activate and shows in status (4607.778248ms)
▶ T34 item 3: pm-on refuses before any stop, passes with the PM, or puts the phase-1 bus back
✔ not-pending (719.010856ms)
✔ harness-unset (2272.485716ms)
✔ a launched line passes: the bus alone restarts with --pm (3276.598137ms)
✔ a failed pm_launched puts the phase-1 unit back: exit 1, still pending (6693.543553ms)
✔ a failed phase-1 gate too: pm-on-down, exit 2, both units stopped (8141.636634ms)
✔ T34 item 3: pm-on refuses before any stop, passes with the PM, or puts the phase-1 bus back (21104.286659ms)
▶ T35 item 4: rollback from pending installs --pm; a plug-in kind the target lacks refuses by name
✔ rollback from pending gates pm_launched on the old copy (3372.092831ms)
✔ after the harness set, rollback refuses until harness is gone (5046.857905ms)
✔ activate names every kind the target lacks (1244.308621ms)
✔ T35 item 4: rollback from pending installs --pm; a plug-in kind the target lacks refuses by name (9663.997607ms)
✔ T36 --plan shows the pending state and lists the row 169 refusals it can see (3695.067003ms)
✔ T37 tester-port activate writes pointer and unit, replaces the port, and supported re-pin and rollback keep it (6897.072525ms)
✔ T38 tester-port rollback to unsupported target drops flag in plan, steps, unit and pointer (3812.546819ms)
✔ T39 tester-port explicit unsupported target reads target copy and refuses before stopping either unit (904.276249ms)
✔ T40 tester-port invalid values refuse exit 4 before stopping, including tunnel and main ports (819.160889ms)
✔ T41 tester-port inherited activate to unsupported target drops it and retains drift refusal and failed-run recovery (8462.827367ms)
✔ T42 tester-port pm-on preserves pointer Console flags (3200.83272ms)
▶ T43 row 180 item 1: runs in one second get distinct activation ids, rollback sets and post-attempt stores
✔ activate then pm-on on the same copy (3520.417628ms)
✔ two restores: an old-form id and a suffixed one, each with its own post-attempt store (6507.524064ms)
✔ adopt takes the next free id too (703.050442ms)
✔ all ten names taken: capture-failed before anything stops, and the log line names no activation (956.29941ms)
✔ --restore takes an id with or without -1 to -9 and nothing else (971.565868ms)
✔ T43 row 180 item 1: runs in one second get distinct activation ids, rollback sets and post-attempt stores (12659.952034ms)
▶ T44 row 180 item 2: --plan lists tester-port-invalid and tester-port-target with its other refusals
✔ a bad tester port (1970.260005ms)
✔ without --plan a bad port is usage before any pointer read (650.605696ms)
✔ a target Console without --tester-port (1005.8136ms)
✔ T44 row 180 item 2: --plan lists tester-port-invalid and tester-port-target with its other refusals (3627.354723ms)
▶ T45 row 181 items 2 and 3: the restore guard counts events and every COUNTED table
✔ both modes count events and each listed table, and a table the store lacks counts 0 (817.850391ms)
✔ one instance_names row and no event refuses before any stop; --events 1 restores (4258.491111ms)
✔ one usage_records row and no event refuses before any stop; --events 1 restores (4200.77682ms)
✔ a listed table that cannot be read refuses restore-mismatch before any stop (2308.610496ms)
✔ the in-run recount counts the listed tables too (3113.596786ms)
✔ T45 row 181 items 2 and 3: the restore guard counts events and every COUNTED table (14700.29833ms)
✔ T46 row 181 item 4: every table in the bus schema is in exactly one of four lists (3.037176ms)
✔ T47 row 185 test 1: a same-schema activate leaves a whole backup and the log line names it (1868.973993ms)
✔ T48 row 185 test 2: a gate failure on a same-schema activate rolls back without a restore (5804.760552ms)
▶ T49 row 185 test 3: a same-schema rollback --restore restores, or refuses rows written after STAMP
✔ no rows after STAMP: restores and goes live (3320.288695ms)
✔ one instance_names row after STAMP refuses post-backup-events; --events 1 restores (3651.108762ms)
✔ T49 row 185 test 3: a same-schema rollback --restore restores, or refuses rows written after STAMP (6972.06843ms)
✔ T50 row 185 test 4: activate --plan prints the backup line and writes nothing (2352.202566ms)
✔ T51 row 185 test 5: an activate with no store file takes no backup (2180.71818ms)
▶ T52 row 186: the activate plan's backup line says what the run will do
✔ same schema (2482.119077ms)
✔ same schema with --migrate (2194.383877ms)
✔ in chain with --migrate (2161.378827ms)
✔ in chain without --migrate (1945.096692ms)
✔ outside the chain (1933.991036ms)
✔ outside the chain with --migrate (1982.219098ms)
✔ T52 row 186: the activate plan's backup line says what the run will do (12700.35568ms)
✔ T178 proxy proof header and path survive re-pin and rollback unchanged in the pointer and unit (3084.56264ms)
✔ T178 proxy proof pair survives a tester-port replacement as well (1871.352274ms)
▶ T53 row 187: activate sets, carries and drops the proof pair
✔ 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 (5957.458612ms)
✔ tests 2 and 3: an activate of the live copy writes the pair; a later activate and a rollback carry it (4748.919152ms)
✔ test 2: only a pair change may name the live copy; the log keeps from and to, and rollback still goes to the earlier copy (6348.550383ms)
✔ test 2: a live-copy activate says only the Console flags change, and backs up the stopped store before it installs (3766.45236ms)
✔ test 3: pm-on keeps the pair (2959.180793ms)
✔ test 4: --no-proof drops the pair from the pointer and the unit and says so; with no pair it says so and passes (5233.036939ms)
✔ test 5: each refusal exits 3 before anything stops (3649.68534ms)
✔ tests 5 and 9: --plan lists every proof refusal with the others and writes nothing (836.025341ms)
✔ test 6: usage errors (773.25631ms)
✔ test 7: a rollback to a copy without proof support drops the pair and says so (3311.183201ms)
✔ test 7: an activate to a copy without proof support refuses proof-target without --no-proof and passes with it (3616.1202ms)
✔ T53 row 187: activate sets, carries and drops the proof pair (41201.876404ms)
▶ T54 row 188: a carried pair gets the proof file checks, and a proof file inside the repository refuses
✔ test 1: an activate that carries the pair refuses after chmod 644, in the plan and the run, before anything stops (2194.421439ms)
✔ test 1: an activate that carries the pair refuses after a symlink to a mode-600 file, in the plan and the run, before anything stops (2189.529552ms)
✔ test 1: an activate that carries the pair refuses after removal, in the plan and the run, before anything stops (2243.035352ms)
✔ test 2: a rollback that carries the pair refuses after chmod 644, in the plan and the run, before anything stops (2336.000835ms)
✔ test 3: a proof file inside the repository refuses, in the plan and the run, before anything stops (1105.046097ms)
✔ test 4: a path outside the repository through a directory symlink into it refuses on its realpath (1150.666433ms)
✔ T54 row 188: a carried pair gets the proof file checks, and a proof file inside the repository refuses (11219.876632ms)
▶ T55 row 189: activate --no-send drops --send, and rollback and pm-on carry it off
✔ test 1: a live-copy --no-send drops --send from the pointer and the unit, keeps the rest in order, logs the step and backs up the stopped store (2274.3857ms)
✔ test 2: a plain live-copy activate still refuses already-active, and so does --no-send once --send is gone (3110.763383ms)
✔ test 3: --plan prints one send: line in each case, with the live-copy note on a same-copy run (4485.563548ms)
✔ test 4: after --no-send, a rollback to the earlier copy leaves --send out of the pointer and the unit (3260.741012ms)
✔ test 4: pm-on after --no-send keeps --send off (3151.996202ms)
✔ test 5: --no-send on rollback, pm-on, status, adopt or rehearse is a usage error (654.253174ms)
✔ test 6: with both a pair and --send in the pointer, --no-send and --no-proof each drop what they name and keep the rest (9881.189087ms)
✔ T55 row 189: activate --no-send drops --send, and rollback and pm-on carry it off (26820.278319ms)
▶ T57 row 200: activate --sso, its refusals before any stop, and the rollback to a Console without --sso
✔ item 1: --sso drops the preview set and the pair, puts --sso before --send and the tester port, and the plan names what it drops and carries (3703.052075ms)
✔ item 1: --sso may re-pin the live copy once; later activates carry it; --no-send still drops --send (8819.731338ms)
✔ item 1: --sso with a pair or --no-proof, or on any verb but activate, is a usage error (692.195455ms)
✔ item 2: a target without --sso, a missing or bad console.sso, and each slot problem refuse before any stop (5662.393194ms)
✔ item 3: a config whose console key the target loader refuses stops an activate or a rollback before any stop, naming the first step (4284.150525ms)
✔ item 3: a rollback from --sso to a Console without it takes the flags from the set of the activation onto the copy, past a same-copy re-pin (sage-1389) (11448.283647ms)
✔ item 3: a rollback set that is missing, fails its sums, holds no argv or already ran --sso refuses sso-rollback-flags (4548.577973ms)
✔ round 2 (webby-0028 N1): run through a symlinked repository path, a slot or a proof file inside the repository still refuses (2401.295904ms)
✔ round 2 (webby-0028 N2): a config.json that does not parse stops the run before anything is read or stopped (2172.272961ms)
✔ round 3 (sage-1556): a named instance turns --sso on with its own config, slot and Console unit, and rolls back from its own set (4291.803296ms)
✔ row 238 (#1744): on a named instance the sso-config first step names --instance NAME in both commands (1134.427878ms)
✔ T57 row 200: activate --sso, its refusals before any stop, and the rollback to a Console without --sso (49160.357454ms)
✔ R1 rehearse passes on the same schema; the scratch root holds its files and the live files stay byte-identical (3480.635997ms)
✔ R2 rehearse runs the migration checks when the schema differs; the live store stays at 3g (3538.664935ms)
✔ R3 a failing migration gives REHEARSAL_FAIL migrate and starts nothing (352.394887ms)
✔ R4 a host that does not start, does not answer status, reports another pid, launches anyway, makes a launches directory or keeps its lock each fails by name (22497.071985ms)
✔ R5 a host that ignores SIGTERM gets SIGKILL and FAIL host-stopped (4425.852712ms)
✔ R6 a Console that does not answer /healthz gives REHEARSAL_FAIL healthz (5628.652507ms)
✔ R7 a host on another copy fails copy-match (5626.942239ms)
✔ R8 a scratch port in use or reserved refuses, and the rehearsal takes no other port (483.876429ms)
✔ R9 mos-0963: a role reference left outside placeholders/, or left on an env variable, fails credential-ref-live before the host starts (737.183647ms)
✔ R10 mos-0962: a scratch business that still sets a tracker fails tracker-configured (336.641991ms)
✔ R11 mos-0961: a scratch config on the live data root or on any root but the scratch one, or a sessions.json naming a running pid, refuses by name (1329.146892ms)
✔ R12 mos-0962: a notifier config with a binding fails notify-off (322.530573ms)
✔ R13 the pre-checks: an old copy, a backup that fails its sums, a missing backup or pointer, and usage (467.892209ms)
✔ R14 a host that writes the live config, the live business file or the backup fails the end check by name (9332.612807ms)
✔ R15 a rehearsal stopped by TERM, INT or HUP fails signal and still stops the host and Console and runs the end checks (9980.867141ms)
✔ row 233: --backup last takes the newest backup the log records; a later line with backup null does not hide it, and the store line names it (3438.884089ms)
✔ row 233: --backup last with no backup in the log, or no log, exits 3 with no-backup and makes no rehearsal directory (190.873827ms)
✔ row 233: --backup last refuses a newest backup that is not an activation id, with exit 1 (117.903969ms)
▶ T56 row 190: named instances side by side on one unit directory, $BIN and set of stand-ins
✔ T-I3 each instance renders its own config, session env and Description, and nothing of another (1497.847969ms)
✔ T-I4 a Console port another installed Console takes refuses port-claimed, and a named instance keeps off the default ports (1995.198242ms)
✔ T-I6 alpha's activate and rollback reach nothing of beta's (3765.448219ms)
✔ T-I6 swapped: beta's reach nothing of alpha's (3738.069466ms)
✔ T-I7 MOSAIC_CONFIG naming alpha's config with no flag refuses instance-root-mismatch and changes nothing (672.561735ms)
✔ T-I9 another instance's bus active refuses nothing; one of this instance's does (5819.537565ms)
✔ T56 row 190: named instances side by side on one unit directory, $BIN and set of stand-ins (17490.000329ms)
ℹ tests 196
ℹ suites 0
ℹ pass 196
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 533493.817869
@@ -0,0 +1,29 @@
✔ row 233: sequence 1: confirm fails healthz, the driver reverts, renders the old copy and confirm --rollback --cause A5 passes: exit 1 (6210.373434ms)
✔ row 233: sequence 2: activate --migrate fails at the wrapper, revert restores the store, confirm --rollback --cause A3: exit 1 (3236.556603ms)
✔ row 233: sequence 3: root's render of the new copy fails, the driver rolls back with --cause A4, and holds the lock to the end: exit 1 (3088.506825ms)
✔ row 233: sequence 4: a capture failure is exit 3 from the stage, and A2f restarts the old units and gates them: exit 1 (2296.722984ms)
✔ row 233: N1: a bus that outlives A2's timeout keeps its old start, so A2f's gate fails stale-start: exit 2, both units stopped (7977.855045ms)
✔ row 233: sequence 5: confirm of the wrong copy is refused, and the driver rolls back with --cause A5: exit 1 (3484.271196ms)
✔ row 233: sequence 6: a revert that fails stops both units at exit 2, and the next run refuses at its --plan with nothing stopped (2709.834241ms)
✔ row 233: sequence 7: two upgrades and a rollback --to the previous copy with --restore, the args file following each confirm (7481.097653ms)
✔ row 240: I3 N1: a failed args move after A5 exits 5 args-failed, the new copy live and confirmed and both args files kept (2791.714439ms)
✔ row 233: sequence 8: a migration that writes and fails: the driver reverts to the backup byte for byte, --cause A3: exit 1 (3155.287145ms)
✔ row 233: a second run while the lock is held refuses upgrade-running with one log line and no call (659.849042ms)
✔ row 233: A1 refuses open-activation on any AWAITING-UNITS file, through the stack's user's --plan, with nothing stopped (1064.03698ms)
✔ row 233: a target that migrates refuses migrate-required without --migrate, with nothing stopped (1067.011634ms)
✔ row 233: the rehearsal runs after the plan and before A2; a failed one refuses rehearsal-failed with nothing stopped (1099.022074ms)
✔ row 233: the rehearsal's BACKUP_STORE line goes to the step log after a match, a malformed one as none, the rest of its output never (2918.095196ms)
✔ row 233: a real rehearsal with no backup in the log refuses no-backup, and goes on only with --no-rehearsal, which logs the skip (3400.448853ms)
✔ row 233: a real rehearsal after an upgrade names the log's newest backup; --no-rehearsal with a backup refuses rehearsal-required (3795.598553ms)
✔ row 233: the args file must be the running user's, with no group or other write, one --copy, and the copy the units run (839.804685ms)
✔ row 233: hostcheck (row 240): a nonzero --check or a missing file refuses profile-drift before the plan (962.315692ms)
✔ row 233: the checkout must be at --sha (head-mismatch), and a commit the clone can't fast-forward to refuses fetch-failed (936.914531ms)
✔ row 233: usage exits 4 before any log or lock (685.887579ms)
ℹ tests 21
ℹ suites 0
ℹ pass 21
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 60456.446459
@@ -0,0 +1,12 @@
✔ C2-04: a library-only business with tracker.baseUrl boots without a project tracker (143.257728ms)
task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200
task.close on a missing task answered: task-not-found; it made GET /tasks/999 404
✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (274.054339ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 502.723687
@@ -0,0 +1,24 @@
✔ C2-04 mirror: exact default path, bare without checkout or remote, and idempotent initialization (35.180686ms)
✔ C2-04 mirror: simultaneous creators publish one intact mirror and leave no staging paths (28.575775ms)
✔ C2-04 mirror: skipped publication accepts both coreutils statuses without removing the winner (100.935123ms)
✔ C2-04 mirror: preserve occupied directories and reject symlink ancestors and code overlaps (11.206477ms)
✔ C2-04 mirror: existing metadata accepts only bare Git plus state and the precreated lock (20.077192ms)
✔ C2-04 state: missing state is empty only while the served ref is absent (32.816347ms)
✔ C2-04 state: valid problems and sync metadata return no content and change no state bytes (34.864084ms)
✔ C2-04 state: strict keys, types, times, branch, commit and fixed R messages fail closed (97.291867ms)
✔ C2-04 state: symlink, hardlink, non-private state and missing lock refuse without replacement (29.030379ms)
✔ C2-04 file: served commit, not HEAD or a caller commit, owns the exact returned text (71.409306ms)
✔ C2-04 file: R1, R2 and R7 exclusions and never-synced mirrors cannot return file bodies (78.061049ms)
✔ C2-04 lock: a first-sync and fast-forward reader wait for a consistent pair across processes (230.30816ms)
✔ C2-04 lock: same-process waits, timeout code, callback expiry and throw release (5027.25259ms)
✔ C2-04 lock: owner death releases the kernel lock without replacing its file (52.471475ms)
✔ C2-04 lock: a missing flock binary becomes a handled refusal (44.850141ms)
✔ C2-04 lock: an unawaited scoped read drains before the lock is released (19.645975ms)
ℹ tests 16
ℹ suites 0
ℹ pass 16
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5963.543966
@@ -0,0 +1,23 @@
✔ C2-04: record APIs refuse a library before creating a repository (2.136303ms)
✔ init creates a checked versioned layout, valid templates and human-authored local history (25.282114ms)
✔ init honors repo, refuses a non-empty directory and an undeclared project without touching files (13.067957ms)
✔ check reports every bad record, filename mismatch and unknown top-level path (11.851979ms)
✔ one writer commits validated docs, requirements and decisions with the bound agent author (66.653ms)
✔ writer refuses spec paths, bad schemas, mismatched IDs and invalid actor metadata before changing history (14.848593ms)
✔ writer refuses human pending edits and remotes instead of absorbing or contacting them (18.544136ms)
✔ links, repo/code overlap, identity mismatch and a busy writer refuse safely (17.705717ms)
✔ schemas remain package-owned and reject invalid nested data and dates (0.249795ms)
✔ git commit failure restores only the attempted record and leaves human history unchanged (36.217826ms)
✔ git hooks and ambient git identity cannot alter the record writer or execute outside actions (25.483658ms)
✔ project-local schemas cannot weaken checks; invalid UTF-8 and linked git directories refuse (14.206204ms)
✔ schema constraints and layout omissions are reported independently (12.868115ms)
✔ unsupported schema keywords, malformed contexts, ignored local config and nonempty markers refuse (18.523225ms)
✔ a linked git directory is refused before a writer lock can touch its target (30.574055ms)
ℹ tests 15
ℹ suites 0
ℹ pass 15
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 355.86586
@@ -0,0 +1,91 @@
✔ C2-04: ensureCheckouts skips a library before a real clone and leaves no books path (22.355495ms)
✔ a first launch clones the root at its branch, with the root's path as origin (23.522928ms)
✔ the clone is not at its place until git clone has finished (19.304369ms)
✔ a later launch fast-forwards a clean clone after a commit to the root, then finds it current (56.365259ms)
✔ a clone ahead of the root keeps its commits and counts as current (35.573942ms)
▶ a dirty clone and one on another branch are fetched, and nothing but refs/remotes moves
✔ a modified file (41.519272ms)
✔ an untracked file (42.31579ms)
✔ a staged file (42.328417ms)
✔ another branch (40.470049ms)
✔ a detached HEAD (40.702516ms)
✔ a dirty clone and one on another branch are fetched, and nothing but refs/remotes moves (208.344457ms)
✔ a diverged clone gets the fetch into refs/remotes and is kept, not-fast-forward (42.026437ms)
▶ a clone kept before the branch check, or for a failed status, gets no fetch
✔ not-a-clone (22.404264ms)
✔ hooks-present (23.073574ms)
✔ submodule (30.027272ms)
✔ config-changed (26.84014ms)
✔ root-unreadable (27.464656ms)
✔ root-detached (32.242501ms)
✔ status-failed, on the root's branch and clean (35.760734ms)
✔ status-failed, dirty (35.340017ms)
✔ a clone kept before the branch check, or for a failed status, gets no fetch (234.251087ms)
✔ a dirty clone whose fetch fails is kept, fetch-failed, exactly as it was (37.035833ms)
✔ every git call the host makes, for a new clone and an existing one, carries maintenance.auto=false and gc.auto=0 (44.392371ms)
✔ a clone with loose objects enough for gc --auto gets no maintenance or gc from the fetch and the merge (71.033894ms)
▶ a clone with user.name or user.email in its config is kept, and git runs nothing in it
✔ user.name (25.544784ms)
✔ user.email (25.14754ms)
✔ a clone with user.name or user.email in its config is kept, and git runs nothing in it (51.146879ms)
✔ a commit git can't read after the fast-forward is null (45.746486ms)
✔ a new clone whose commit can't be read refuses the launch and leaves nothing behind (18.576825ms)
▶ a clone the session rigged is kept, and git runs nothing and writes nothing outside it
✔ core.fsmonitor (33.120199ms)
✔ core.hooksPath (33.027016ms)
✔ a hook file (30.549575ms)
✔ include.path (33.355749ms)
✔ includeIf (33.915275ms)
✔ a filter driver (34.236367ms)
✔ origin moved (34.552998ms)
✔ a second origin url (33.217597ms)
✔ the origin url twice, both the root (34.870837ms)
✔ an ext:: origin (33.6526ms)
✔ a branch tracking another remote (33.582081ms)
✔ a branch merging a tag (33.08648ms)
✔ a fetch line onto local branches (34.366694ms)
✔ no origin (33.938644ms)
✔ a config file over the size cap (31.285987ms)
✔ config a symlink to /dev/zero (31.094858ms)
✔ the clone a symlink to the bait (32.754738ms)
✔ the clone a file (31.24085ms)
✔ the clone a plain directory (30.973366ms)
✔ .git a gitdir: file (31.404094ms)
✔ .git a symlink (30.769522ms)
✔ .git/logs a symlink (30.883317ms)
✔ .git/refs/heads a symlink (31.770074ms)
✔ .git/objects a symlink (30.840803ms)
✔ a reflog hard-linked to a file outside (30.322982ms)
✔ HEAD a fifo (31.496048ms)
✔ a commondir file (30.352537ms)
✔ an alternates file (30.782307ms)
✔ a gitlink staged, no .gitmodules (43.468245ms)
✔ a gitlink and .gitmodules staged (44.893483ms)
✔ a gitlink to a repository with its own gitlink (51.849481ms)
✔ a gitlink and .gitmodules committed (47.718755ms)
✔ a gitlink and submodule.active in config (44.861481ms)
✔ a .git/modules with no gitlink (33.279554ms)
✔ a clone the session rigged is kept, and git runs nothing and writes nothing outside it (1175.645437ms)
▶ a root that can't be cloned refuses with the project id and leaves nothing behind
✔ missing (10.161685ms)
✔ not a repository (9.929001ms)
✔ a plain directory inside another repository (14.613067ms)
✔ unreadable (9.85915ms)
✔ on no branch (11.8808ms)
✔ missing an object (15.129485ms)
✔ a root that can't be cloned refuses with the project id and leaves nothing behind (72.336456ms)
✔ a clone whose root is on no branch, or gone, is kept and named (28.236024ms)
✔ no credential or forge remote of the root reaches a clone (47.533999ms)
✔ every project in the business file gets its own clone at its own branch (36.497038ms)
✔ a clone past its time gets SIGTERM and refuses the launch, with nothing left in the workspace (513.397369ms)
✔ a clone that ignores SIGTERM gets SIGKILL once the grace period is over (1018.126958ms)
✔ a timeout signals git's process group, so a process git started dies with it (515.579592ms)
✔ once a signalled git has closed, its group gets SIGKILL, so a process that ignored SIGTERM dies too (515.62296ms)
ℹ tests 78
ℹ suites 0
ℹ pass 78
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 4880.907448
@@ -0,0 +1,9 @@
✔ C2-04 CLI: both listener callback pairs dispatch library verbs with their own transport (109.519835ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 191.536271
@@ -0,0 +1 @@
{"processes": [], "removed": "/mnt/storage/scratch/gt-j5nm6iqn", "sourceHashesUnchanged": 29}
@@ -0,0 +1,5 @@
{
"base": "f8276c5eb",
"tip": "8a04f4aa4aba493eb89e92aa9e0c34084be7058e",
"overlap": []
}
@@ -0,0 +1 @@
2026-10-21T13:27:27.279728+00:00
@@ -0,0 +1,194 @@
[
{
"file": "packages/bus/tests/library.test.mjs",
"rc": 0,
"counts": {
"tests": 6,
"pass": 6,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "01-packages_bus_tests_library.test.mjs.log"
},
{
"file": "packages/bus/tests/project-runtime.test.mjs",
"rc": 0,
"counts": {
"tests": 3,
"pass": 3,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "02-packages_bus_tests_project-runtime.test.mjs.log"
},
{
"file": "packages/business/tests/business.test.mjs",
"rc": 0,
"counts": {
"tests": 19,
"pass": 19,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "03-packages_business_tests_business.test.mjs.log"
},
{
"file": "packages/business/tests/cli.test.mjs",
"rc": 0,
"counts": {
"tests": 10,
"pass": 10,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "04-packages_business_tests_cli.test.mjs.log"
},
{
"file": "packages/business/tests/instance-settings.test.mjs",
"rc": 0,
"counts": {
"tests": 13,
"pass": 13,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "05-packages_business_tests_instance-settings.test.mjs.log"
},
{
"file": "packages/business/tests/library-projects-protected.test.mjs",
"rc": 0,
"counts": {
"tests": 4,
"pass": 4,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "06-packages_business_tests_library-projects-protected.test.mjs.log"
},
{
"file": "packages/business/tests/resolve.test.mjs",
"rc": 0,
"counts": {
"tests": 11,
"pass": 11,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "07-packages_business_tests_resolve.test.mjs.log"
},
{
"file": "packages/cli/tests/launcher.test.mjs",
"rc": 0,
"counts": {
"tests": 31,
"pass": 31,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "08-packages_cli_tests_launcher.test.mjs.log"
},
{
"file": "packages/cli/tests/stack-release-external.test.mjs",
"rc": 0,
"counts": {
"tests": 25,
"pass": 25,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "09-packages_cli_tests_stack-release-external.test.mjs.log"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"rc": 0,
"counts": {
"tests": 196,
"pass": 196,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "10-packages_cli_tests_stack-release.test.mjs.log"
},
{
"file": "packages/cli/tests/stack-upgrade.test.mjs",
"rc": 0,
"counts": {
"tests": 21,
"pass": 21,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "11-packages_cli_tests_stack-upgrade.test.mjs.log"
},
{
"file": "packages/cli/tests/trackers-boot.test.mjs",
"rc": 0,
"counts": {
"tests": 2,
"pass": 2,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "12-packages_cli_tests_trackers-boot.test.mjs.log"
},
{
"file": "packages/library/tests/mirror.test.mjs",
"rc": 0,
"counts": {
"tests": 16,
"pass": 16,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "13-packages_library_tests_mirror.test.mjs.log"
},
{
"file": "packages/project/tests/project.test.mjs",
"rc": 0,
"counts": {
"tests": 15,
"pass": 15,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "14-packages_project_tests_project.test.mjs.log"
},
{
"file": "packages/seat/tests/checkout.test.mjs",
"rc": 0,
"counts": {
"tests": 78,
"pass": 78,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "15-packages_seat_tests_checkout.test.mjs.log"
},
{
"file": "packages/webui/tests/library-cli.test.mjs",
"rc": 0,
"counts": {
"tests": 1,
"pass": 1,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "16-packages_webui_tests_library-cli.test.mjs.log"
}
]
@@ -0,0 +1 @@
/mnt/storage/scratch/gt-j5nm6iqn
@@ -0,0 +1 @@
2026-10-21T13:15:15.548778+00:00
@@ -0,0 +1 @@
{"tests": 451, "pass": 451, "fail": 0, "cancelled": 0, "skipped": 0}
@@ -0,0 +1,14 @@
✔ C2-04 M4: both verbs reject every agent before arguments or adapter access (31.111616ms)
✔ C2-04 bus: human reads validate keys and return adapter results without path leakage (12.907709ms)
✔ C2-04 socket: each async library verb resolves its result rather than serializing {} (14.631614ms)
✔ C2-04 runtime: protectedRoots skips library but retains real code-root credential protection (42.32493ms)
✔ C2-04 runtime: unsafe existing mirror refuses boot and never publishes a socket (20.701022ms)
✔ C2-04 runtime: real library adapters reach the socket and lock waits leave other bus reads responsive (82.322654ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 342.882774
@@ -0,0 +1,11 @@
✔ Broker keeps the sandbox client import closure: only the listed imports, no lazy project import (1.629219ms)
✔ without a trusted project adapter all three async paths refuse project-not-configured (43.976856ms)
✔ real startBroker injects the project adapter for socket reads and bound-author record writes (68.038516ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 209.393305
@@ -0,0 +1,26 @@
✔ C2-04: library mappings load without roots, remain frozen, and retain mapping refusals (9.717487ms)
✔ C2-04: an otherwise valid publish grant cannot name a library repo (1.932435ms)
✔ config directory and file path follow MOSAIC_CONFIG (0.219374ms)
✔ the fixture business validates and comes back frozen (0.988926ms)
✔ two instances may share a definition (0.940973ms)
✔ top-level refusals (3.202519ms)
✔ arbiters and projects (4.893652ms)
✔ role instances (2.749701ms)
✔ Vikunja bots (5.88131ms)
✔ a role without Vikunja takes no tracker block (4.097438ms)
✔ credential references match the definition's services (2.14689ms)
✔ launch (7.492201ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 launch.threadIdleSeconds: absent is the default, a positive integer is used, anything else refuses (4.633228ms)
✔ loadBusiness: file checks (1.761847ms)
✔ loadBusiness: not a regular file (36.320112ms)
✔ loading writes nothing (1.117059ms)
✔ names that are Object.prototype properties don't count as declared (2.898886ms)
✔ the shipped example refuses as written and validates once filled in (0.943369ms)
ℹ tests 18
ℹ suites 0
ℹ pass 18
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 152.091247
@@ -0,0 +1,18 @@
✔ C2-04: business validate and resolve skip a library before code projects and credential checks (219.31729ms)
✔ usage errors exit 4 (321.939555ms)
✔ validate: a good business exits 0 and prints instance digests (76.14497ms)
✔ validate: project files (353.409636ms)
✔ validate: missing files and a broken system config (238.885921ms)
✔ validate: credential reference problems exit 2 and name each one (72.187146ms)
✔ validate: a token file inside the repository is refused (66.603163ms)
✔ validate: role definitions come from MOSAIC_ROLES_DIR (212.341091ms)
✔ resolve: prints one instance's record (340.865306ms)
✔ resolve: refusals (526.732262ms)
ℹ tests 10
ℹ suites 0
ℹ pass 10
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2493.367293
@@ -0,0 +1,20 @@
✔ C2-04: instance settings skip a library while checking a later code project (33.384562ms)
✔ row 165 read lists each instance and the definitions add accepts (2.667885ms)
✔ row 165 add: two instances join, validate exits 0, the log names the human, and the bus restart is reported (87.186598ms)
✔ row 165 change: definition, vars and launch membership log before and after; only a definition change asks for a restart (92.493768ms)
✔ row 165 refuses each validation failure and leaves the file's bytes alone (29.566007ms)
✔ row 165 refuses an added instance whose definition declares credentials, and says why (3.207564ms)
✔ row 165 refuses a definition change to or from one that declares credentials (4.215516ms)
✔ row 165 a vars or launch change on an instance with its binding succeeds and keeps the binding (72.107052ms)
✔ row 165 refuses an END_PROTECTED definition and every change to a protected instance (7.281711ms)
✔ row 165 refuses while a project file doesn't resolve, as validate would (13.645202ms)
✔ row 165 refuses a stale revision and leaves the file's bytes alone (10.119096ms)
✔ row 65 an instance write refuses harness-off, thinking-unsupported, an unknown model and a partial fallback (29.171515ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 450.444507
@@ -0,0 +1,12 @@
✔ C2-04 M9: changed playground and hybrid entries leave business and changes.jsonl byte-identical (20.276877ms)
✔ C2-04 M9: code projects, additions, deletions and array changes are protected too (16.209877ms)
✔ C2-04 M9: canonical key order is allowed and a non-project setter still records its change (13.103686ms)
✔ C2-04 M9: a serialization hook cannot change projects after the canonical comparison (12.598611ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 112.92317
@@ -0,0 +1,19 @@
✔ C2-04: a library first in projects does not hide a later project layer (6.304379ms)
✔ systemVars flattens the validated config (0.828852ms)
✔ precedence: system, business, project, project role, agent (1.532829ms)
✔ limits narrow the definition and never widen it (2.353655ms)
✔ role.launch stays within-role only for the instance the launch block names (4.439921ms)
✔ limits.authority without role.launch leaves the launcher with no launch block (1.815884ms)
✔ limits.authority narrows cross-role actions too (0.827532ms)
✔ classify (0.829892ms)
✔ the record carries what the broker and launcher need (0.755977ms)
✔ digest: key order doesn't matter, any value change does (5.452ms)
✔ refusals (1.724971ms)
ℹ tests 11
ℹ suites 0
ℹ pass 11
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 79.941483
@@ -0,0 +1,53 @@
✔ C2-04: launcher workspace clones code but never the library (1262.341325ms)
✔ bus start --pm: the launcher instance (the chief of staff) runs under its own PID namespace, registered, with a manifest (1237.473965ms)
usage: {"event":"usage-unknown","business":"acme","run":"r4414332d2ef5","instance":"chief","turn":"382a782c-719a-48e8-a249-ec24caf5ad1e","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"reaca93954ae7","instance":"coder","turn":"70bcac05-fbc1-41b2-85cb-725c15884a94","source":"@none","code":"baseline-unknown"}
✔ the chief of staff launches a coder through its launch tool; the coder answers; refusals name their code (579.516254ms)
usage: {"event":"usage-unknown","business":"acme","run":"rb21267233555","instance":"chief","turn":"c228faeb-6e3c-4453-ba9b-b9a27b9790af","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r1fc01260daa7","instance":"pm","turn":"a5d09ce0-4208-4eb4-8178-6e06a1792ab9","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"rb21267233555","instance":"chief","turn":"25722023-5270-4c50-a8a0-7c3645d08bd7","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r1fc01260daa7","instance":"pm","turn":"f4a76756-4300-4cdf-a64b-542fc075c137","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r1fc01260daa7","instance":"pm","turn":"99ea9441-ce4e-4aab-a56c-614a30c9f0da","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"rb21267233555","instance":"chief","turn":"d85fa6e4-7557-4adf-b1f6-5e76455a1e51","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r1fc01260daa7","instance":"pm","turn":"87f4bf91-2ef2-4fe7-8e99-09fc8b14887e","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"rb21267233555","instance":"chief","turn":"6db9d009-2cb9-41ba-a07e-da13d92b8182","source":"@none","code":"baseline-unknown"}
✔ a launched pm's request_staff reaches the chief of staff; no other session has the tool and the launch limits still hold (917.244851ms)
✔ each launch makes sure the workspace has a clone of the project: cloned, fast-forwarded, or fetched and logged (3692.977114ms)
✔ a role the broker says holds a merge decision gets land_work, its prompt line, its landing and the git socket; without it, none (439.559581ms)
✔ REQ-LAUNCH-1 the tick starts a listed merge decision holder once, with land_work, the Landing line and its landing turn; an unlisted holder starts nothing (324.77099ms)
usage: {"event":"usage-unknown","business":"acme","run":"rd33ea739f0c8","instance":"coder","turn":"434622cc-98ff-44b4-99c9-58a8c92f32af","source":"@none","code":"baseline-unknown"}
✔ a holder running since before the resolution is stopped once the broker releases it, land-relaunch, and the next tick starts it: landing turn first, then the message that waited (502.385034ms)
✔ a landing run that ended isn't started again for its decision, a spent decision starts nothing, and a new decision starts the holder (453.667705ms)
✔ after a restart, the new host's first tick starts a holder whose decision is still held (654.880099ms)
✔ a refused landing start is logged once per code with its decision, isn't a start, and the next tick with room starts the holder (498.662638ms)
✔ a project clone that can't be made refuses checkout-failed, from --chief and from a session, with no run and no clone left (208.782448ms)
✔ a runner that stops at once ends its launch with the runner's reason (261.153743ms)
✔ a Claude Code launch with no session credential refuses before it starts (143.737058ms)
✔ a pi launch needs no session credential: pi logs in through its own ENV_ALLOW variables (239.874115ms)
usage: {"event":"plugin-refused","kind":"harness","name":"down","code":"missing","message":"/home/jwoltje/.t3/worktrees/mosaic-stack/herb-row215-on212/packages/plugins/harness/down is missing","kept":[]}
plugins: {"event":"plugin-refused","kind":"harness","name":"down","code":"missing","message":"/home/jwoltje/.t3/worktrees/mosaic-stack/herb-row215-on212/packages/plugins/harness/down is missing","kept":[]}
✔ row 65 a failed primary preflight launches the fallback on a fixture harness module, recording the route and the reason (281.577833ms)
✔ a runner killed from the host ends its launch as killed, not failed (297.263791ms)
✔ a missing or failing bwrap refuses sandbox-unavailable before anything starts; there is no unconfined fallback (151.327191ms)
✔ row 64 a role skill that isn't enabled refuses skill-not-enabled, and one that isn't a plain directory skill-refused, before anything starts (151.537445ms)
✔ row 64 on Claude Code a role skill that isn't enabled refuses the same way, before anything starts (166.552674ms)
usage: {"event":"usage-unknown","business":"acme","run":"rc42c73baf400","instance":"chief","turn":"e62acc78-0ddd-4cd3-bb90-c828cf5dc76e","source":"@none","code":"baseline-unknown"}
usage: {"event":"usage-unknown","business":"acme","run":"r9fac1139247f","instance":"coder","turn":"e634dd74-b64a-4c8d-8e8d-90244a1070d4","source":"@none","code":"baseline-unknown"}
✔ a real Claude Code coder's shell can't read a stand-in secret or anything else its sandbox doesn't hold (1685.572916ms)
✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (791.200293ms)
✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3731.707174ms)
✔ bus start --no-launch leaves an earlier host's live session alone: no signal, no end, sessions.json as it was (196.993732ms)
✔ bus start --no-launch refuses every launch: the PM launch and a session's request, each logged, nothing spawned (159.584006ms)
✔ bus start --no-launch starts no thread tick; the same host with launches runs it (1086.093241ms)
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (132.916552ms)
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (143.651083ms)
✔ a launch client that never closes its side doesn't hold the host's close (144.715063ms)
✔ a runner that ignores SIGTERM is killed when the host closes (1275.914847ms)
ℹ tests 30
ℹ suites 0
ℹ pass 30
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 21918.233853
@@ -0,0 +1,33 @@
✔ row 232: confirm, revert, gate, --to and --expect-from need --units external; the user-only flags are usage under it (832.842347ms)
✔ row 232: pm-on and activate --pm-after-harness refuse external-pm (934.889182ms)
✔ row 232: units-running (N4) refuses while a unit is active and leaves the store, the log and TAKEN; failed counts as down (2121.604528ms)
✔ row 232: activate stages: the pointer stays, AWAITING-UNITS names TO, a backup of the stopped store, no directory in the set, no --user, no journal (1777.227274ms)
✔ row 232: A3 order: capture-failed and marker-failed refuse with exit 3 and leave no marker or FAILED (1712.692045ms)
✔ row 232: confirm and revert refuse open-activation with no marker, two, the wrong copy, or one the log holds closed; refusals name no activation (3642.165244ms)
✔ row 232: confirm polls: bus status good after 3 s fails with --timeout 1, writes FAILED and leaves the rest; FAILED then refuses activation-failed (3522.196896ms)
✔ row 232: confirm polls to a pass with --timeout 10: the pointer, an ok line that reads as the started run, no marker (5641.501857ms)
✔ row 232: stale-start: @1792540799 fails a start floor of 1792540800 with stale-start (3076.294408ms)
✔ row 232: stale-start is a gate code: an enter time that turns to @1792540800 after 2 s passes with --timeout 10 (4471.752137ms)
✔ row 232: gate (N2, A2f): --since 1792540800 passes @1792540800 and fails @1792540799; it writes nothing (2692.190142ms)
✔ row 232: the external PM check reads launches list: one live run of launch.by since the start passes (9781.807961ms)
✔ row 232: revert puts the wrapper back and leaves the store when nothing migrated (1409.775029ms)
✔ row 232: confirm --rollback refuses rollback-mismatch, and logs rollback-failed with exit 2 when OLD fails its gate (3556.739549ms)
✔ row 232: confirm --rollback after a --restore swap logs rollbackCode not-attempted; rollback --restore finds an external backup (3258.873111ms)
✔ row 232: sequence 1: activate, confirm times out, revert, confirm --rollback --cause A5: gate-failed rollback ok (4878.560819ms)
✔ row 232: sequence 2: activate --migrate, the wrapper install fails, revert restores TAKEN, confirm --rollback --cause A3 (3203.155908ms)
✔ row 232: sequence 3: activate, root's A4 fails, revert, confirm --rollback --cause A4: failed code units-start (2844.170694ms)
✔ row 232: sequence 4: a capture failure leaves nothing open, then A2f's gate OLD writes nothing (1940.493148ms)
✔ row 232: sequence 5: a confirm of the wrong copy is refused, and revert and confirm --rollback still close the activation (3423.561744ms)
✔ row 232: sequence 6: a revert that fails leaves the activation open, and the next --plan refuses interrupted-run (1732.106092ms)
✔ row 232: sequence 7: adopt, two confirmed activates, rollback --to the previous copy, confirmed, logs op rollback (8298.40004ms)
✔ row 232: sequence 8: a migration that writes and fails: revert restores the backup byte for byte, confirm --rollback --cause A3 (3658.849346ms)
✔ row 232: user mode: every systemctl call carries --user, and activate, rollback and check make the base's calls (7450.661653ms)
✔ row 232: user mode: a default activate --plan prints the base's bytes (2572.062595ms)
ℹ tests 25
ℹ suites 0
ℹ pass 25
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 89317.249955
@@ -0,0 +1,227 @@
✔ T1 adopt writes the pointer from the units once; a failing check refuses check-failed (1372.806902ms)
✔ T2 activate installs exactly the pointer's pm, claude and Console flags (2175.278069ms)
▶ T3 a hand-edited unit refuses unit-flags-drift, names the difference and changes nothing
✔ pm (940.434448ms)
✔ claude (905.331444ms)
✔ console (953.801488ms)
✔ T3 a hand-edited unit refuses unit-flags-drift, names the difference and changes nothing (2800.414725ms)
▶ T4 each item 3 refusal changes nothing and logs one line with its code
✔ copy-missing (828.258816ms)
✔ copy-unreachable (750.040135ms)
✔ copy-schema-unknown (804.784952ms)
✔ already-active (767.83377ms)
✔ instance-busy (another business active) (836.742584ms)
✔ instance-busy (this business activating) (824.181046ms)
✔ drop-in-override (819.802212ms)
✔ no-pointer (432.597537ms)
✔ T4 each item 3 refusal changes nothing and logs one line with its code (6065.707472ms)
✔ T5 an activate with no migration stops the Console then the bus, installs, starts the bus then the Console (1974.205406ms)
✔ T6 a migration without --migrate rolls back with migration-needs-flag (1689.71261ms)
✔ T7 --migrate takes the backup before the migration runs (1874.080499ms)
✔ T8 a failed migration restores the backup and keeps the store it replaced (1879.088438ms)
✔ T9 a store outside the target's chain rolls back with store-newer (1695.743767ms)
▶ T10 each gate check failing alone rolls back and puts every file back
✔ units-inactive (5145.320966ms)
✔ bus-status (5504.739824ms)
✔ healthz (4630.494167ms)
✔ copy-check (4953.814776ms)
✔ pm-launch-unconfirmed (5175.714883ms)
✔ T10 each gate check failing alone rolls back and puts every file back (25411.164743ms)
▶ T10b the PM check needs exactly one launched line for a live run of this start
✔ no launched line (5309.620588ms)
✔ two lines (5271.450965ms)
✔ a changed word (5207.189363ms)
✔ run not in launches list (5249.94294ms)
✔ run started before the start (5297.104798ms)
✔ a human launch with no journal line (5012.384828ms)
✔ run for another business (5255.258408ms)
✔ run not live (5155.194619ms)
✔ one well-formed line and a live run pass (1936.667118ms)
✔ T10b the PM check needs exactly one launched line for a live run of this start (43696.539474ms)
✔ T11 a rollback whose gate fails too: rollback-failed, exit 2, both units stopped, no retry (9445.48752ms)
✔ T12 rollback takes the previous copy from the log (3164.86878ms)
✔ T13 a rollback across a migration needs --restore, then restores and goes live (3832.433037ms)
✔ T14 events written after the backup need --events N with the right N (4300.182861ms)
✔ T14b a rollback --restore whose gate fails stops both units and skips item 6 (5706.078353ms)
✔ T14c a rollback without --restore whose gate fails runs item 6 (7364.684709ms)
✔ T15 --plan names the migration and the restore and changes nothing (2976.558ms)
✔ T26 --plan lists the commits an activate adds, after the migration lines (1448.921448ms)
✔ T27 --plan names each migration step, or says the steps cannot be read (1074.961775ms)
✔ T25 --plan refuses copies-unrelated when neither copy is an ancestor of the other (504.611631ms)
▶ T23 --plan reads the store without changing a file, stopped or held
✔ stopped store (612.654192ms)
✔ held store (615.45405ms)
✔ T23 --plan reads the store without changing a file, stopped or held (1228.613968ms)
✔ T24 business-mismatch and capture-failed refuse with exit 3; a short id is a usage error (1504.453363ms)
✔ T16 status prints the pointer, the log, the check, bus status and drift, and writes nothing (969.540282ms)
✔ T19 a started line with no final line: status shows it and every changing verb refuses (1272.112268ms)
✔ T20 a held run lock refuses run-locked without a log line; status and --plan still run (1363.537596ms)
✔ T21 a bus unit with no MOSAIC_CLAUDE_BIN: adopt and activate refuse no-claude-pin (1516.277233ms)
▶ T22 the stopped store read: a -wal or -shm refuses store-unclean and is left alone; a clean read adds no file
✔ bus.sqlite-wal (1776.498328ms)
✔ bus.sqlite-shm (1805.948894ms)
✔ clean (1860.970301ms)
✔ T22 the stopped store read: a -wal or -shm refuses store-unclean and is left alone; a clean read adds no file (5444.145497ms)
▶ T28 a unit path, drop-in directory or drop-in file that cannot be read, or a relative unit path, refuses drop-in-override before any stop
✔ unit path (838.2428ms)
✔ drop-in directory (770.669306ms)
✔ drop-in file (787.861386ms)
✔ relative unit path (799.918614ms)
✔ T28 a unit path, drop-in directory or drop-in file that cannot be read, or a relative unit path, refuses drop-in-override before any stop (3197.49908ms)
▶ T29 a bus that stops during the held-store read: --plan says so, rollback --restore refuses restore-mismatch
✔ --plan (842.842612ms)
✔ rollback --restore (2383.664939ms)
✔ T29 a bus that stops during the held-store read: --plan says so, rollback --restore refuses restore-mismatch (3227.016275ms)
✔ T30 a copy check that agrees on another copy fails the gate with copy-check and rolls back (4822.028878ms)
▶ T31 a quoted or escaped Environment= or ExecStart= is unreadable: refusals by name, nothing read from /a
✔ Environment double quotes (1747.115914ms)
✔ Environment single quotes (1747.871141ms)
✔ Environment backslash (1718.42505ms)
✔ ExecStart quotes (1733.916441ms)
✔ Console ExecStart quotes (1805.192313ms)
✔ T31 a quoted or escaped Environment= or ExecStart= is unreadable: refusals by name, nothing read from /a (8753.514814ms)
▶ T32 item 1: --pm-after-harness refuses a target without harness or a file that sets it, and passes without the PM
✔ usage: not with --pm, and only for activate (630.81989ms)
✔ pm-after-harness-target (949.989597ms)
✔ pm-after-harness-set (1400.026643ms)
✔ a pointer without pm (849.389257ms)
✔ phase 1 installs without --pm, skips pm_launched and writes the pending pointer (2037.948918ms)
✔ T32 item 1: --pm-after-harness refuses a target without harness or a file that sets it, and passes without the PM (5869.092521ms)
▶ T33 item 2: read_pointer takes pmPending only as true beside pm false; a pending pointer refuses activate and shows in status
✔ read_pointer (1646.25297ms)
✔ pm-pending, already-adopted and the status line (2899.447167ms)
✔ T33 item 2: read_pointer takes pmPending only as true beside pm false; a pending pointer refuses activate and shows in status (4546.229216ms)
▶ T34 item 3: pm-on refuses before any stop, passes with the PM, or puts the phase-1 bus back
✔ not-pending (781.816005ms)
✔ harness-unset (2528.770078ms)
✔ a launched line passes: the bus alone restarts with --pm (3635.97518ms)
✔ a failed pm_launched puts the phase-1 unit back: exit 1, still pending (6129.763508ms)
✔ a failed phase-1 gate too: pm-on-down, exit 2, both units stopped (10681.440065ms)
✔ T34 item 3: pm-on refuses before any stop, passes with the PM, or puts the phase-1 bus back (23758.959718ms)
▶ T35 item 4: rollback from pending installs --pm; a plug-in kind the target lacks refuses by name
✔ rollback from pending gates pm_launched on the old copy (3432.493856ms)
✔ after the harness set, rollback refuses until harness is gone (4814.40527ms)
✔ activate names every kind the target lacks (1278.267995ms)
✔ T35 item 4: rollback from pending installs --pm; a plug-in kind the target lacks refuses by name (9525.935986ms)
✔ T36 --plan shows the pending state and lists the row 169 refusals it can see (3442.637109ms)
✔ T37 tester-port activate writes pointer and unit, replaces the port, and supported re-pin and rollback keep it (6713.755862ms)
✔ T38 tester-port rollback to unsupported target drops flag in plan, steps, unit and pointer (3556.909655ms)
✔ T39 tester-port explicit unsupported target reads target copy and refuses before stopping either unit (816.518752ms)
✔ T40 tester-port invalid values refuse exit 4 before stopping, including tunnel and main ports (798.151327ms)
✔ T41 tester-port inherited activate to unsupported target drops it and retains drift refusal and failed-run recovery (8335.191178ms)
✔ T42 tester-port pm-on preserves pointer Console flags (3045.995877ms)
▶ T43 row 180 item 1: runs in one second get distinct activation ids, rollback sets and post-attempt stores
✔ activate then pm-on on the same copy (3077.866023ms)
✔ two restores: an old-form id and a suffixed one, each with its own post-attempt store (6478.022663ms)
✔ adopt takes the next free id too (676.463788ms)
✔ all ten names taken: capture-failed before anything stops, and the log line names no activation (927.509173ms)
✔ --restore takes an id with or without -1 to -9 and nothing else (1026.017549ms)
✔ T43 row 180 item 1: runs in one second get distinct activation ids, rollback sets and post-attempt stores (12186.978758ms)
▶ T44 row 180 item 2: --plan lists tester-port-invalid and tester-port-target with its other refusals
✔ a bad tester port (2101.036117ms)
✔ without --plan a bad port is usage before any pointer read (417.577025ms)
✔ a target Console without --tester-port (1050.997021ms)
✔ T44 row 180 item 2: --plan lists tester-port-invalid and tester-port-target with its other refusals (3570.568101ms)
▶ T45 row 181 items 2 and 3: the restore guard counts events and every COUNTED table
✔ both modes count events and each listed table, and a table the store lacks counts 0 (819.811738ms)
✔ one instance_names row and no event refuses before any stop; --events 1 restores (4281.220981ms)
✔ one usage_records row and no event refuses before any stop; --events 1 restores (4339.553222ms)
✔ a listed table that cannot be read refuses restore-mismatch before any stop (3011.194623ms)
✔ the in-run recount counts the listed tables too (3456.814799ms)
✔ T45 row 181 items 2 and 3: the restore guard counts events and every COUNTED table (15910.111456ms)
✔ T46 row 181 item 4: every table in the bus schema is in exactly one of four lists (2.96277ms)
✔ T47 row 185 test 1: a same-schema activate leaves a whole backup and the log line names it (1839.922479ms)
✔ T48 row 185 test 2: a gate failure on a same-schema activate rolls back without a restore (5782.007105ms)
▶ T49 row 185 test 3: a same-schema rollback --restore restores, or refuses rows written after STAMP
✔ no rows after STAMP: restores and goes live (3276.614244ms)
✔ one instance_names row after STAMP refuses post-backup-events; --events 1 restores (3603.193575ms)
✔ T49 row 185 test 3: a same-schema rollback --restore restores, or refuses rows written after STAMP (6880.328466ms)
✔ T50 row 185 test 4: activate --plan prints the backup line and writes nothing (2392.362463ms)
✔ T51 row 185 test 5: an activate with no store file takes no backup (2055.74343ms)
▶ T52 row 186: the activate plan's backup line says what the run will do
✔ same schema (2076.652498ms)
✔ same schema with --migrate (2103.728264ms)
✔ in chain with --migrate (2343.740358ms)
✔ in chain without --migrate (2172.44891ms)
✔ outside the chain (2018.610199ms)
✔ outside the chain with --migrate (2286.782735ms)
✔ T52 row 186: the activate plan's backup line says what the run will do (13003.221353ms)
✔ T178 proxy proof header and path survive re-pin and rollback unchanged in the pointer and unit (3428.609169ms)
✔ T178 proxy proof pair survives a tester-port replacement as well (2005.665148ms)
▶ T53 row 187: activate sets, carries and drops the proof pair
✔ 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 (5369.617405ms)
✔ tests 2 and 3: an activate of the live copy writes the pair; a later activate and a rollback carry it (4877.926077ms)
✔ test 2: only a pair change may name the live copy; the log keeps from and to, and rollback still goes to the earlier copy (6411.791056ms)
✔ test 2: a live-copy activate says only the Console flags change, and backs up the stopped store before it installs (3823.440479ms)
✔ test 3: pm-on keeps the pair (2929.528358ms)
✔ test 4: --no-proof drops the pair from the pointer and the unit and says so; with no pair it says so and passes (6134.571168ms)
✔ test 5: each refusal exits 3 before anything stops (4080.818538ms)
✔ tests 5 and 9: --plan lists every proof refusal with the others and writes nothing (921.495619ms)
✔ test 6: usage errors (791.940368ms)
✔ test 7: a rollback to a copy without proof support drops the pair and says so (3569.742095ms)
✔ test 7: an activate to a copy without proof support refuses proof-target without --no-proof and passes with it (4102.029003ms)
✔ T53 row 187: activate sets, carries and drops the proof pair (43015.196531ms)
▶ T54 row 188: a carried pair gets the proof file checks, and a proof file inside the repository refuses
✔ test 1: an activate that carries the pair refuses after chmod 644, in the plan and the run, before anything stops (2443.927487ms)
✔ test 1: an activate that carries the pair refuses after a symlink to a mode-600 file, in the plan and the run, before anything stops (2451.543098ms)
✔ test 1: an activate that carries the pair refuses after removal, in the plan and the run, before anything stops (2371.886229ms)
✔ test 2: a rollback that carries the pair refuses after chmod 644, in the plan and the run, before anything stops (2440.808004ms)
✔ test 3: a proof file inside the repository refuses, in the plan and the run, before anything stops (1028.022624ms)
✔ test 4: a path outside the repository through a directory symlink into it refuses on its realpath (1092.396277ms)
✔ T54 row 188: a carried pair gets the proof file checks, and a proof file inside the repository refuses (11829.775747ms)
▶ T55 row 189: activate --no-send drops --send, and rollback and pm-on carry it off
✔ test 1: a live-copy --no-send drops --send from the pointer and the unit, keeps the rest in order, logs the step and backs up the stopped store (2297.315751ms)
✔ test 2: a plain live-copy activate still refuses already-active, and so does --no-send once --send is gone (3372.219555ms)
✔ test 3: --plan prints one send: line in each case, with the live-copy note on a same-copy run (4822.032599ms)
✔ test 4: after --no-send, a rollback to the earlier copy leaves --send out of the pointer and the unit (3681.502406ms)
✔ test 4: pm-on after --no-send keeps --send off (3384.224907ms)
✔ test 5: --no-send on rollback, pm-on, status, adopt or rehearse is a usage error (646.332677ms)
✔ test 6: with both a pair and --send in the pointer, --no-send and --no-proof each drop what they name and keep the rest (10576.556687ms)
✔ T55 row 189: activate --no-send drops --send, and rollback and pm-on carry it off (28781.800109ms)
▶ T57 row 200: activate --sso, its refusals before any stop, and the rollback to a Console without --sso
✔ item 1: --sso drops the preview set and the pair, puts --sso before --send and the tester port, and the plan names what it drops and carries (3731.038851ms)
✔ item 1: --sso may re-pin the live copy once; later activates carry it; --no-send still drops --send (9441.965146ms)
✔ item 1: --sso with a pair or --no-proof, or on any verb but activate, is a usage error (804.681755ms)
✔ item 2: a target without --sso, a missing or bad console.sso, and each slot problem refuse before any stop (6340.571003ms)
✔ item 3: a config whose console key the target loader refuses stops an activate or a rollback before any stop, naming the first step (4474.330517ms)
✔ item 3: a rollback from --sso to a Console without it takes the flags from the set of the activation onto the copy, past a same-copy re-pin (sage-1389) (11258.382547ms)
✔ item 3: a rollback set that is missing, fails its sums, holds no argv or already ran --sso refuses sso-rollback-flags (4811.481384ms)
✔ round 2 (webby-0028 N1): run through a symlinked repository path, a slot or a proof file inside the repository still refuses (2548.588058ms)
✔ round 2 (webby-0028 N2): a config.json that does not parse stops the run before anything is read or stopped (2148.082108ms)
✔ round 3 (sage-1556): a named instance turns --sso on with its own config, slot and Console unit, and rolls back from its own set (4427.346674ms)
✔ row 238 (#1744): on a named instance the sso-config first step names --instance NAME in both commands (1072.216503ms)
✔ T57 row 200: activate --sso, its refusals before any stop, and the rollback to a Console without --sso (51060.894572ms)
✔ R1 rehearse passes on the same schema; the scratch root holds its files and the live files stay byte-identical (3501.665564ms)
✔ R2 rehearse runs the migration checks when the schema differs; the live store stays at 3g (2615.765886ms)
✔ R3 a failing migration gives REHEARSAL_FAIL migrate and starts nothing (406.432761ms)
✔ R4 a host that does not start, does not answer status, reports another pid, launches anyway, makes a launches directory or keeps its lock each fails by name (23643.921414ms)
✔ R5 a host that ignores SIGTERM gets SIGKILL and FAIL host-stopped (4522.753011ms)
✔ R6 a Console that does not answer /healthz gives REHEARSAL_FAIL healthz (5695.642056ms)
✔ R7 a host on another copy fails copy-match (5707.109175ms)
✔ R8 a scratch port in use or reserved refuses, and the rehearsal takes no other port (564.078959ms)
✔ R9 mos-0963: a role reference left outside placeholders/, or left on an env variable, fails credential-ref-live before the host starts (739.463019ms)
✔ R10 mos-0962: a scratch business that still sets a tracker fails tracker-configured (356.527698ms)
✔ R11 mos-0961: a scratch config on the live data root or on any root but the scratch one, or a sessions.json naming a running pid, refuses by name (1415.617777ms)
✔ R12 mos-0962: a notifier config with a binding fails notify-off (346.615997ms)
✔ R13 the pre-checks: an old copy, a backup that fails its sums, a missing backup or pointer, and usage (495.179039ms)
✔ R14 a host that writes the live config, the live business file or the backup fails the end check by name (10325.286968ms)
✔ R15 a rehearsal stopped by TERM, INT or HUP fails signal and still stops the host and Console and runs the end checks (9991.220342ms)
✔ row 233: --backup last takes the newest backup the log records; a later line with backup null does not hide it, and the store line names it (3454.052038ms)
✔ row 233: --backup last with no backup in the log, or no log, exits 3 with no-backup and makes no rehearsal directory (203.934243ms)
✔ row 233: --backup last refuses a newest backup that is not an activation id, with exit 1 (127.433768ms)
▶ T56 row 190: named instances side by side on one unit directory, $BIN and set of stand-ins
✔ T-I3 each instance renders its own config, session env and Description, and nothing of another (1595.173571ms)
✔ T-I4 a Console port another installed Console takes refuses port-claimed, and a named instance keeps off the default ports (1992.011586ms)
✔ T-I6 alpha's activate and rollback reach nothing of beta's (3927.69359ms)
✔ T-I6 swapped: beta's reach nothing of alpha's (4046.222781ms)
✔ T-I7 MOSAIC_CONFIG naming alpha's config with no flag refuses instance-root-mismatch and changes nothing (707.673985ms)
✔ T-I9 another instance's bus active refuses nothing; one of this instance's does (6093.674502ms)
✔ T56 row 190: named instances side by side on one unit directory, $BIN and set of stand-ins (18364.096442ms)
ℹ tests 196
ℹ suites 0
ℹ pass 196
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 530980.947325
@@ -0,0 +1 @@
{"processes": [], "removed": "/mnt/storage/scratch/gt-t2e8hpyu", "trees": ["/home/jwoltje/.t3/worktrees/mosaic-stack/herb-row215-on212", "/home/jwoltje/.t3/worktrees/mosaic-stack/herb-row197-r8"]}
@@ -0,0 +1 @@
{"pid": 765123, "reason": "sage-1983 stop after file10; do not start file11"}
@@ -0,0 +1,122 @@
[
{
"file": "packages/bus/tests/library.test.mjs",
"rc": 0,
"counts": {
"tests": 6,
"pass": 6,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "01-packages_bus_tests_library.test.mjs.log"
},
{
"file": "packages/bus/tests/project-runtime.test.mjs",
"rc": 0,
"counts": {
"tests": 3,
"pass": 3,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "02-packages_bus_tests_project-runtime.test.mjs.log"
},
{
"file": "packages/business/tests/business.test.mjs",
"rc": 0,
"counts": {
"tests": 18,
"pass": 18,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "03-packages_business_tests_business.test.mjs.log"
},
{
"file": "packages/business/tests/cli.test.mjs",
"rc": 0,
"counts": {
"tests": 10,
"pass": 10,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "04-packages_business_tests_cli.test.mjs.log"
},
{
"file": "packages/business/tests/instance-settings.test.mjs",
"rc": 0,
"counts": {
"tests": 12,
"pass": 12,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "05-packages_business_tests_instance-settings.test.mjs.log"
},
{
"file": "packages/business/tests/library-projects-protected.test.mjs",
"rc": 0,
"counts": {
"tests": 4,
"pass": 4,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "06-packages_business_tests_library-projects-protected.test.mjs.log"
},
{
"file": "packages/business/tests/resolve.test.mjs",
"rc": 0,
"counts": {
"tests": 11,
"pass": 11,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "07-packages_business_tests_resolve.test.mjs.log"
},
{
"file": "packages/cli/tests/launcher.test.mjs",
"rc": 0,
"counts": {
"tests": 30,
"pass": 30,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "08-packages_cli_tests_launcher.test.mjs.log"
},
{
"file": "packages/cli/tests/stack-release-external.test.mjs",
"rc": 0,
"counts": {
"tests": 25,
"pass": 25,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "09-packages_cli_tests_stack-release-external.test.mjs.log"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"rc": 0,
"counts": {
"tests": 196,
"pass": 196,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
"log": "10-packages_cli_tests_stack-release.test.mjs.log"
}
]
@@ -0,0 +1 @@
/mnt/storage/scratch/gt-t2e8hpyu
@@ -0,0 +1 @@
2026-10-21T11:56:56.177608+00:00
@@ -0,0 +1,10 @@
✔ C2-04: library mappings load without roots, remain frozen, and retain mapping refusals (9.004507ms)
✔ C2-04: an otherwise valid publish grant cannot name a library repo (1.807264ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 67.488189
@@ -0,0 +1,23 @@
✖ C2-04: library mappings load without roots, remain frozen, and retain mapping refusals (5.192384ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 54.487487
✖ failing tests:
test at packages/business/tests/business.test.mjs:11:1
✖ C2-04: library mappings load without roots, remain frozen, and retain mapping refusals (5.192384ms)
Error [BusinessError]: unsupported /mnt/storage/scratch/gt-PvGtsQ/mosaic-business-4vAhxB/businesses/acme.json projects.books key: "kind"
at refuse (file:///home/jwoltje/.t3/worktrees/mosaic-stack/herb-row197/packages/business/src/errors.mjs:12:9)
at rejectUnknownKeys (file:///home/jwoltje/.t3/worktrees/mosaic-stack/herb-row197/packages/business/src/util.mjs:17:33)
at validateBusinessDocument (file:///home/jwoltje/.t3/worktrees/mosaic-stack/herb-row197/packages/business/src/business.mjs:232:5)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/herb-row197/packages/business/tests/business.test.mjs:17:18)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
exitCode: 2
}
@@ -0,0 +1,9 @@
✔ C2-04: ensureCheckouts skips a library before a real clone and leaves no books path (24.411494ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 72.211367
@@ -0,0 +1,8 @@
{
"at": "2026-10-21T08:51:07Z",
"tempRoot": "/mnt/storage/scratch/gt-PvGtsQ",
"fixtureCwdOrArgv": [],
"testProcesses": [],
"excluded": "current scan process and its ancestors",
"tempRootRemoved": true
}
@@ -0,0 +1,9 @@
✔ C2-04: business validate and resolve skip a library before code projects and credential checks (206.597157ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 260.97412
@@ -0,0 +1,9 @@
✔ C2-04: instance settings skip a library while checking a later code project (22.656666ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 79.494698
@@ -0,0 +1,9 @@
✔ C2-04: launcher workspace clones code but never the library (1241.43402ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1338.915726
@@ -0,0 +1,12 @@
✔ C2-04 M9: changed playground and hybrid entries leave business and changes.jsonl byte-identical (21.916712ms)
✔ C2-04 M9: code projects, additions, deletions and array changes are protected too (17.081275ms)
✔ C2-04 M9: canonical key order is allowed and a non-project setter still records its change (14.304068ms)
✔ C2-04 M9: a serialization hook cannot change projects after the canonical comparison (9.595281ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 116.250811
@@ -0,0 +1,28 @@
✖ C2-04 M9: changed playground and hybrid entries leave business and changes.jsonl byte-identical (21.728048ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 71.097427
✖ failing tests:
test at packages/business/tests/library-projects-protected.test.mjs:35:1
✖ C2-04 M9: changed playground and hybrid entries leave business and changes.jsonl byte-identical (21.728048ms)
AssertionError [ERR_ASSERTION]: Missing expected exception.
at protectedWrite (file:///home/jwoltje/.t3/worktrees/mosaic-stack/herb-row197/packages/business/tests/library-projects-protected.test.mjs:29:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/herb-row197/packages/business/tests/library-projects-protected.test.mjs:37:3)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
expected: { code: 'organization-projects-protected' },
operator: 'throws',
diff: 'simple'
}
@@ -0,0 +1,9 @@
✔ C2-04: record APIs refuse a library before creating a repository (3.426617ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 61.266328
@@ -0,0 +1,9 @@
✔ C2-04: a library first in projects does not hide a later project layer (6.517821ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 62.270511
@@ -0,0 +1,90 @@
[
{
"file": "business-green.log",
"tests": 2,
"pass": 2,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
{
"file": "business-red.log",
"tests": 1,
"pass": 0,
"fail": 1,
"cancelled": 0,
"skipped": 0
},
{
"file": "checkout-green.log",
"tests": 1,
"pass": 1,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
{
"file": "cli-green.log",
"tests": 1,
"pass": 1,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
{
"file": "instance-settings-green.log",
"tests": 1,
"pass": 1,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
{
"file": "launcher-green.log",
"tests": 1,
"pass": 1,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
{
"file": "m9-green.log",
"tests": 4,
"pass": 4,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
{
"file": "m9-red.log",
"tests": 1,
"pass": 0,
"fail": 1,
"cancelled": 0,
"skipped": 0
},
{
"file": "project-green.log",
"tests": 1,
"pass": 1,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
{
"file": "resolve-green.log",
"tests": 1,
"pass": 1,
"fail": 0,
"cancelled": 0,
"skipped": 0
},
{
"file": "trackers-boot-green.log",
"tests": 2,
"pass": 2,
"fail": 0,
"cancelled": 0,
"skipped": 0
}
]
@@ -0,0 +1 @@
2026-10-21T08:48:14Z
@@ -0,0 +1 @@
/mnt/storage/scratch/gt-PvGtsQ
@@ -0,0 +1,12 @@
✔ C2-04: a library-only business with tracker.baseUrl boots without a project tracker (132.133246ms)
task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200
task.close on a missing task answered: task-not-found; it made GET /tasks/999 404
✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (261.808635ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 480.083569
@@ -0,0 +1 @@
{"processes": [], "removed": "/mnt/storage/scratch/gt-iwfcrf5s"}
@@ -0,0 +1 @@
["packages/library/tests/mirror.test.mjs", "packages/bus/tests/library.test.mjs", "packages/webui/tests/library-cli.test.mjs", "packages/bus/tests/project-runtime.test.mjs"]
@@ -0,0 +1,31 @@
✖ C2-04 CLI: both listener callback pairs dispatch library verbs with their own transport (140.263767ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 244.49705
✖ failing tests:
test at packages/webui/tests/library-cli.test.mjs:10:1
✖ C2-04 CLI: both listener callback pairs dispatch library verbs with their own transport (140.263767ms)
AssertionError [ERR_ASSERTION]: refused: missing library callbacks
2 !== 0
at TestContext.<anonymous> (file:///mnt/storage/scratch/gt-iwfcrf5s/base/packages/webui/tests/library-cli.test.mjs:27:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 2,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,47 @@
✖ C2-04 CLI: both listener callback pairs dispatch library verbs with their own transport (31.496523ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 122.065824
✖ failing tests:
test at packages/webui/tests/library-cli.test.mjs:10:1
✖ C2-04 CLI: both listener callback pairs dispatch library verbs with their own transport (31.496523ms)
AssertionError [ERR_ASSERTION]: file:///mnt/storage/scratch/gt-iwfcrf5s/base/packages/webui/src/bus.mjs:1
import {appendFileSync} from 'node:fs';export class BusReadError extends Error{};export const busCaller=o=>async(verb,args)=>{appendFileSync("/mnt/storage/scratch/gt-iwfcrf5s/library-cli-1eUPUX/calls.jsonl",JSON.stringify({verb,args,cli:o.cli??"/mnt/storage/scratch/gt-iwfcrf5s/base/packages/bus/src/human-cli.mjs"})+'
^
SyntaxError: Invalid or unexpected token
at compileSourceTextModule (node:internal/modules/esm/utils:355:16)
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:93:18)
at #translate (node:internal/modules/esm/loader:441:20)
at afterLoad (node:internal/modules/esm/loader:509:29)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:514:12)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:567:36)
at afterResolve (node:internal/modules/esm/loader:614:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:620:12)
at ModuleJob.syncLink (node:internal/modules/esm/module_job:277:33)
at ModuleJob.link (node:internal/modules/esm/module_job:389:17)
Node.js v26.8.1
1 !== 0
at TestContext.<anonymous> (file:///mnt/storage/scratch/gt-iwfcrf5s/base/packages/webui/tests/library-cli.test.mjs:27:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,9 @@
✔ C2-04 CLI: both listener callback pairs dispatch library verbs with their own transport (110.620217ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 194.067409

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