Compare commits

..
1674 changed files with 10910 additions and 222920 deletions

No files matched your search

+6 -11
View File
@@ -25,15 +25,10 @@ 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**: 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).
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).
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
@@ -182,8 +177,8 @@ Full reference — usage, fields, exit codes, safety notes:
## Data map (canon)
- `~/.config/mosaic-dev/config.json`, or `~/.config/mosaic-NAME/config.json`
per named instance: system config (user-authored; never auto-written).
- `~/.config/mosaic-dev/config.json` — 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
-158
View File
@@ -5174,11 +5174,6 @@ 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.
@@ -5189,156 +5184,3 @@ Before: managed project/thread verbs had no Console conversation area. After: fi
2026-10-20 19:34:28 UTC | Gander row 196 build paused at design mismatch | Before: approved design promised Unix tester cleanup only for the owned inode. After: scratch test shows Node/libuv close unlinks a replacement inode before JS close handlers. Evidence agents/gander/work/queue-196/evidence/tester-cleanup-blocker.txt. Sent Sage/Duckworth gander-0016/0017, recommending explicit same-uid residual with private 0700 parent retained. Interpreter, concurrent peer, principal, human, peer-transport, transport, tester and process single files passed. Partial code uncommitted, tester transport not wired. No suites, mutant batches or live actions.
2026-10-21 18:45 UTC | Gander row 196 preservation before rebase | Sage-2051, Mos order mos-1193, explicitly authorizes commit and fast-forward push of the current build before deferred full verification. Product and evidence preserved together; prior focused checks and stopped gates are recorded in the row196 packet, with no claim of full acceptance. Next: rebase onto a36523713 on new gander/row196-r2, report range-diff/patch-id/conflicts, then await Sage's clear for M1-M29 and all requested suites. No live action, force push, branch deletion or tests during hold.
2026-10-21T16:34:44Z | Huey | Q198/Q199 system-manager design, mos-1183, Vikunja 452 | Before: pinned foundation 6c75887b0 has separate bus, development queue, run evidence, turn/cohort controls and launch/usage paths; the assignment requests a mechanical manager design. After: docs/plans/2026-10-21_q198-system-manager.md proposes admission/cooldown, priority and safe interruption, durable requests, bounded retry/DLQ/replay/rerouting, Console monitoring, optional decision adapters with USD reservations, six ordered slices and three recommended owner decisions. Verification: 26 pinned file:line references resolve, ten-line summary, manual source/contract review, git diff --check and installed queue guard pass. No builds/suites, runtime writes, credential reads, service changes or paid calls. Huey queue next refused the linked worktree, then returned nothing from canonical checkout; direct brief controls this assignment. Gitea issue and queue-row capture are handed to Sage under the no-credential-read/no-queue-write boundary.
2026-10-21T18:45:47Z | Huey | Q198 system-manager design r2, mos-1194, Vikunja 452 | Before: Scrooge requested changes on add7cb506 for decision-key worker exposure, cap scope and absent sampler ownership. After: distinct MOSAIC_DECISION_OPENROUTER_KEY with S6 sessionEnv canary requirement; exclusive per-instance key with per-key caps and provider limit on every enablement; S2 owns supervisor sampler and waits until after MVP/re-pin. Also records P1 interpretation, missing-confidence fallback and stand-in timeout selection, explains verified Noul spelling, excludes notifier/gateway retry loops with citations, and cites lifecycle revocation requirement. Verification: 30 pinned citation spans resolve, ten-line summary, manual diff/source review and whitespace/queue-guard checks. No builds, suites, live changes, credential reads or paid calls.
2026-10-21T19:56:11Z | Huey | Q198 design r3, mos-1201/1201a, Vikunja 452 | Before: Scrooge approved r2 7e785a08f with three notes. After: N1 names proposed system.health bus read verb and includes broker handler/authorization in S2 scope and review; N2 labels exclusive ownership as a setup attestation that cannot be mechanically proved and identifies provider key limit as real ceiling; N3 adds recommended Q4 for P1 narrowing and removes the Mos-flag instruction. Other design content retained. Verification: manual r2-to-r3 delta review, ten-line summary, whitespace and installed queue-guard checks. No builds, suites, live changes or paid calls.
2026-10-21T20:19:53Z | Huey | Q198 design r4, mos-1203, DECISIONS Q204, Vikunja 452 | Before: r3 a36952564 presented Q1–Q4 for owner approval. After: section 9 records Q20a–Q20c verbatim as approved; D11 labels Huey’s reading of configurable daily/monthly caps with $0.10/$1 defaults in the one instance config via reviewed schema; provider key ceiling and explicit enable/key requirements retained; Q4 pending, Jason restated 2026-10-21, P1 unchanged. Verification: quotes match DECISIONS ledger, unrelated sections and P1 equal r3, ten-line summary, whitespace and installed queue guard checked. No builds, suites, live changes or paid calls.
2026-10-21T20:48:49Z | Huey | Q198 design r5, mos-1206, DECISIONS Q205/Q206, Vikunja 452 | Before: r4 1e8bccff1 left Q4 pending and long outages could exhaust request retry budgets. After: protected mode separate from unchanged D1 human kill authority; mechanical detection, safe-point pause/proof, JEV-first endpoint-before-headless probes at 60-second increments to 600 seconds without attempt limit, upstream reset waits, shared D11 probe accounting, one test session then batches, proposed three-failed-exit escalation, frozen outage clocks/counters without DLQ, advisory online-service routing, Q4 superseded and Q22 pending. Thresholds/deadlines/batches/escalation explicitly labelled Huey readings. Acceptance proposals updated, no implementation. Verification: complete Q205/Q206 read, quotes match ledger, D1/inventory/Q1–Q3 unchanged, 30 pinned citation references resolve, 11-line summary, manual consistency/diff review, whitespace and installed queue guard checked. No builds, suites, real probes, live changes or paid calls.
2026-10-21T21:04:37Z | Huey | Q198 design r6, mos-1208/mos-1210, DECISIONS Q207, scrooge-0003, Vikunja 452 | Before: r5 fd44f7c60 gated probes on JEV and lacked request-local resume failure separation. After: parallel independent probes; central JEV/smaller-model/rules endpoint chain, per-model timeout/calibration, 4 KiB metadata limit and isolated fallback credentials; subscription quota vs metered USD accounting and separately enabled paid-probe budget (proposed $0 defaults). S-F1–S-F7: bounded outage classification, request-local D6/two-resume DLQ rule and batch definition; escalation latch with authorized revision-bound system.recovery.release separate from kill clear; cooperative F5 pause/F6 proof only; proposed configurable 24-hour cumulative pause expiry; full Q205 kill sentence with labelled protected-mode interpretation; r3–r6 history. S5/S6 acceptance proposals updated. Verification: Q205/Q207 quotes match ledger, D1/inventory/Q1–Q3 unchanged, 30 pinned references resolve, summary within 15 lines, manual consistency/delta review, whitespace and installed queue guard checked. No builds, suites, real probes, live changes, credential reads or paid calls.
2026-10-21T21:39:25Z | Huey | Q198 r6.1 touch-up, mos-1222, scrooge-0004 N1–N3, Vikunja 452 | Before: approved r6 2ee68cd69 had unprovable session-call denial wording, implicit queue/late-call behavior and a partial Q207 paragraph. After: rule/S6 tests require session decision calls/output have no routing authority; non-waiting dispatchers use internal rules immediately; timed-out JEV does not occupy fallback decision slot but retains spend reservation; full Q207 paragraph copied exactly from DECISIONS; matching Q22 reading corrected. ENV_ALLOW and all runtime code unchanged. Verification: exact full-quote equality, obsolete wording absent, D1 unchanged, summary within 15 lines, manual N1–N3 delta review, whitespace and installed queue guard checked. No builds, suites, probes, live changes or paid calls. Review/handoff via T3 as Mos authorized.
2026-10-21T21:11:44Z | Della mos-1213 / Vikunja 460 planning phase | Before: PRD 1.10 has no email feature; MP 95b0298 and EP 7a64921 are read-only prior inputs. Scope: independent post-v1 email plan on della/email-plan, no implementation/live access/MVP or queue changes. Gitea issue enrollment belongs to Mos/Sage because this seat has no API credential.
2026-10-21T21:12:18Z | Della mos-1213 / Vikunja 460 planning verification | After: docs/plans/2026-10-21_email-plan-della.md is 341 lines, with 13 post-v1 rows and six Jason questions. Verified 42 pinned citation anchors, ten requested coverage areas, identical prior src trees, append-only records, clean whitespace and repository queue guard. No configured CI pipeline or CI-queue wrapper found in the current foundation; documentation-only validation applies, no product suites or live tests run. Phase B/C and Gitea issue/queue enrollment remain with Mos/Sage. Only della/email-plan is authorized for push.
2026-10-21T21:37:07Z | Della mos-1220 / Vikunja 460 phase B cross-review | Before: independent plans Della 191dff32f and Ludwig 4a27b73ac delivered. After: docs/plans/2026-10-21_email-review-by-della.md reviews all 218 Ludwig plan lines, records ten findings, five grafts, six retained strengths, nine disagreements and eleven merged Jason questions. Ludwig review 143415779 read in full after ludwig-0002 arrived; reconciliation records new mail-access action, no agent reroute and correction of Della dataRoot credential example. Original plan unchanged. Planning only, no product/MVP/queue/live change.
2026-10-21T21:43:02Z | Della mos-1223 / Vikunja 460 phase C synthesis | Before: independent plans/reviews complete, Mos selected Della base. After: docs/plans/2026-10-21_email-plan.md merges Ludwig grafts and accepted corrections, with 16 post-v1 rows (eight READY, eight WAIT/optional), eleven deduplicated Jason questions, per-row test paths and explicit Q210 non-v1 scheduling. Original Della plan/review preserved; Ludwig originals remain on their branch. Static checks cover graft/row/question/citation anchors and original blobs. No source code, suite, mailbox, secret file, service, MVP or queue change. Candidate goes to Ludwig before Sage landing/queue handoff, copying Mos.
2026-10-21T21:45:20Z | Della mos-1223 / ludwig-0004 focused corrections | Before: b096a16cc left content-key location implicit and E05 core registration marked ready. After: merged plan 399 lines fixes external configDir secret key/lifecycle namespace outside dataRoot backup/restore, E01 schema and E03 missing/expired-key restore tests, and E05/E06 test-local registry readiness with core kinds.mjs registration gated on approved E01/Q4. Sixteen rows and eleven questions retained; originals unchanged. Focused static checks and queue guard apply, no product/live changes. Reviewer will check changed lines only.
2026-10-21T22:24:12Z | Huey | T1 item 7 sessions design r1, mos-1232, Vikunja 463 | Before: origin/refactor cca206cdc keys assistant claims/stores/messages by thread and renders Pi compaction markers; row 194 r4 excludes thread context clear. After: docs/plans/2026-10-21_t1-sessions.md proposes independent per-agent sessions within threads on Pi/Claude, scoped claims/stores/messages/workspaces, completed compaction counts, owner-write/RBAC matrix, idempotent proven-stop/fresh-successor clear, common admission/kill checks, migration and seven acceptance groups, replacement REQ-ROLE-2/REQ-THREAD-2 text and three owner questions. PRD 1.12 USER-3/4 candidate 1509c7cc5 cited separately from base. Verification: 22 pinned references resolve, eight-line summary and three exact owner quotes; manual source/consistency review, whitespace and installed queue guard checked. No code, queue/PRD edits, builds, suites, credential reads, live changes or paid calls. MVP priority ahead of Q198 S1. Review via Scrooge, then Sage/Ludwig as Mos authorized.
2026-10-21T22:28:56Z | Huey | T1 sessions design r2, scrooge-0006, mos-1235/1236, Vikunja 463 | Before: r1 347278044 lacked the exact per-session checkout/single-writer invariant and safe runner drain ordering, and admitted other role instances. After: F1 exact workspace path, fenced checkout setup, legacy shared-workspace quiescence, clear retains old dirty/untracked/unpushed checkout and creates fresh approved checkout, T2 mutant; F2 dequeue fence and generation-bound turn-boundary idle proof before runner signal, busy drain stays clearing, T4 cases; F3 THREAD_DEFINITION only per Mos scope ruling, no role eligibility change; N1 private titles/history vs owner-visible count/activity projection; N2 Claude offline adapter/managed-store-reader dependency flagged as MVP risk. Section 8 records Mos’s Q1–Q3 readings, no questions to Jason. Verification: 27 pinned references resolve, summary within 15 lines, source/delta consistency review, whitespace and installed queue guard checked. No code, queue/PRD edits, builds/suites/live changes/provider calls/credential reads.
2026-10-21T22:30:20Z | Huey | T1 sessions design r3, scrooge-0007 R1, Vikunja 463 | Before: r2 e19e30df2 nested new session checkouts under the retained legacy instance workspace, allowing project-name collision or historical parent inspection to reach live siblings. After: exact path is dataRoot/session-workspaces/business/instance/session, outside the legacy root; canonicalized ancestor/descendant overlap refuses; T2 nesting mutant covers historical mount/inspection and a legacy project named sessions. All other r2 content retained as approved, revision label updated. Verification: old nested path absent, manual R1-only delta, whitespace and installed queue guard checked; no code/builds/suites/live changes/provider calls/credential reads.
2026-10-20 18:51:10 UTC | Row 191 #1689 design phase | Before: per-business launch revoke and per-binding Discord STOP leave task containers, active sessions and background work without one switch. Proposed per-data-root durable halt with human-only resume, generation-fenced starts, bounded concurrent termination, write-once stop receipts and existing event mirror; no schema migration. DESIGN.md inventories starter/continuation paths and container control-mount restriction. Sent to Dewey with Sage copied; implementation waits independent design approval. No tests, commits or live actions.
2026-10-20 18:57:51 UTC | Row 191 design revision | Dewey r1 changes: interim Console --send absence, reserved processing events and missing-state semantics required correction. DESIGN-r2.md now names CLI interim path, unavailable UI controls, trusted initialization and fail-closed missing leaf, container delete/replace mount tests, reservations and id-bound reconciliation. Small notes addressed including diagnostic seats and aliases. Recommended smaller Docker scope to Sage; design and scope approval pending, no code or test runs.
2026-10-20T19:50:56Z | Rocko | row 191 #1689 implementation stage: from independent brakes to shared root state, generation-fenced launcher and tracked stops, Discord/task guards and control HTTP/UI. Evidence is agents/rocko/work/queue-191/logs/ and PROGRESS.md. Fixture single files only so far; heavy verification and candidate not complete. Scope additions sage-1367 (one read-only CODE file) and sage-1392 (processing-only client socket timeout); no schema, proof baseline, live file/unit, commit or push change.
2026-10-20 22:58:45 UTC | Rocko | row 191 #1689 rebase inspection candidate | Rebuilt uncommitted kill-switch tree from da230ca48 onto landed row 190 base 00d4cc1f6 under new branch rocko/row191-r2-00d4. Hand merges retained instance flags and SSO/sign-out changes; fixture tests exposed and fixed SSO signer compatibility and two unknown-runtime cleanup defects. Final touched singles 249/249 across 26 files; 15 overlapping Client reader files 102/102. Manifest a4f780c2 has 78 paths; packet 185 files verified with scan clear. Mutants and owner gate remain pending; no review-ready or activation claim, no commit/push/live action.
2026-10-21 23:39:21 UTC | Rocko | row 191 #1689 inspection snapshot | Sage-2242 authorized a local inspection commit for Honker's row 194 S6. Created d52b4db1c on new rocko/row191-inspection-20261021, parent 00d4cc1f6, preserving the reviewed-design build and candidate a4f780c2 (78 paths). Static manifest/index/source scan and diff check verified; no tests run, no push credential or push attempt. Existing single-file evidence is historical, 249/249 and overlapping Client readers 102/102; suites/mutants/gate remain pending. Current code has verified runtime signalling and instance halt, but no dedicated per-run stop API yet; Honker and Sage notified of that seam directly. Snapshot is not review-ready or approved for live use.
2026-10-22 00:04:05 UTC | Rocko | row 191 #1689 per-run stop inspection | Implemented agreed Honker-0079/0081 stopRun contract with verified per-signal identity, fixed 5s+2s, durable first-owner join and immutable result plus later-exit observations. Focused CLEAR sage-2250: final 9 files 62/62, no skipped, after correcting host guard placement and peer-discovery fixture timeout isolation; initial failures and failed teardown logs retained, only own failed group stopped. Slot returned with cwd sweep clear. Sage published f7bf7dfec; sage-2265 clean rebase creates 7aa18b0f6 on dcd8d4dda under new branch, explicit Rocko identity, all 79 manifest product blobs identical so 62/62 evidence carried. Direct S6 handoff sent to Honker and Sage. Wider suites, mutants, review and activation remain pending.
2026-10-22T00:18:12Z | Sage (Opus 5.5) | row 254 landing (#1759), library.sync human timeout | Before: the human transport gave library.sync the 5-second default, so a sync longer than that reported outcome-unknown, and six library-* codes reached the Console as 502. After: 331597709 lands Megavolt's candidate (7 paths, tree d49e4626f, manifest digest be63cfbe recomputed and matched). human-cli.mjs waits 130 s for library.sync, past the server's 120 s hold, and 5 s for other verbs. webui bus.mjs maps library-sync-busy, library-diverged and library-accept-stale to 409, library-accept-invalid to 400, and library-credential-missing and library-fetch-failed to 503. channel-proof re-pinned. Merged-tree gate on the landing commit: library 77/0, bus 628/0, webui 305/0 (1 skip), cli 749/0 (23:44:33-23:55:11Z). eb9672c91 adds the README request-timeout sentence (Dewey N1) and a DEFERRED entry for the four library codes still at 502 (Dewey N2, binds the Console library routes row). Overlap sweep before push: 268 rebased to r2 ecd9b35eb (README only, approval carried webby-0096), 194 rebased to b7. Done rev 3072, #1759 closed.
2026-10-22 00:24:34 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 full gate on c78f48946 | stopped on second integration failure per sage-2275; singles 269/269, processing-control 61/61, bus 646/646; CLI interrupted with 26 observed failures; no further edits; cwd sweep empty and slot returned to Sage.
2026-10-22 00:48:20 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 eb3abba12 full gate | stopped at Discord 190/192; 32 singles 304/304, processing-control 61/61, bus 646/646, cli 761/761; two uninitialized Discord fixtures reported to Sage; no later run; cwd sweep empty, slot clear.
2026-10-22 00:58:27 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2294 fixture fix | e6800380d on eb3abba12, two test files/four additions, assertions preserved; 338-entry static all-package sweep recorded; no tests; sent Sage for publication and Honker for dependent gate.
2026-10-22 01:30:04 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 e6800380d resume | binding15/15, recover5/5, Discord192/192; stopped WebUI314 passed/4 failed/1 esbuild skip; processing polling violates sign-out/manual redirect and bar changes layout; no fixes/retries, cwd sweep empty, slot returned.
2026-10-22 01:38:08 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2317 Console integration fix | 8e1f55c1d on e6800380d; shared sign-out/manual redirect/interval and desktop frame fixes, assertions retained, six browser cases, 73 mutant sites preflight; syntax only, no tests; sent Sage/Honker.
2026-10-22 02:06:38 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 rebase sage-2329 | eafcbd3e7 on ddfb29af7, new branch, dispatcher retains processing and access; CLI/readme auto-merges checked; 87-path fd0f2740 manifest and 73-site preflight; Sage2333 published, awaiting CLEAR; no tests.
2026-10-22 02:20:51 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 eafcbd3e7 full gate | stopped singles313/6: six new browser cases count app freshness timer as processing timer; diagnosis sent Sage, no fix/retry; no suites/shells/mutants; cwd sweep empty, slot clear.
2026-10-22 02:23:13 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2345 timer fixture | 4db8b5778 on eafcbd3e7, one line attributes timer to processing.js; all assertions retained, no product change or tests; sent Sage and Honker; awaiting publication/CLEAR.
2026-10-22 02:25:29 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2349 record commit | 0f8235277 on 4db8b5778 adds packet BUILD.md cause record; product manifest remains ffb46fbd/87 paths; no amend, product change or tests; Sage/Honker notified.
2026-10-22 02:48:36 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 0f8235277 full gate second red | singles330/330, processing61/61, bus646/646, cli783/783, discord192/192; WebUI323/1/1 host esbuild skip; shell geometry1062vs1000; stopped/no retry, no later suites/shells/mutants; cwd sweep empty, slot returned, Sage escalates Mos.
2026-10-22 02:57:36 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 mos-1248 desktop layout restart | FF 5cd32f4c1 on 0f8235277: desktop grid allocates processing bar, existing shell assertions kept, visibility/no-scroll assertions added; 74 static mutant sites; candidate 2f93ef19; sent Webby/Sage for static review; no runs, awaiting review and CLEAR.
2026-10-22 03:11:09 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 static nav layout, mos-1249/1250 | FF da4334806 on 5cd32f4c1; controls first in nav, non-sticky per sage-2386/webby-0108; candidate-only placement assertion changed, landed shell assertions preserved; 77 mutants statically preflighted; manifest 245dceb1; Webby/Sage handoff sent, no runs, awaiting static look/publish/debug CLEAR.
2026-10-22 03:15:11 UTC | Rocko (d99326cc) | row 191 sage-2394 debug | da4334806 shell5/5 and processing-browser9/9, awk14/0/0; geometry heights reported with read-only distinction; cwd sweep[] and slot returned; full window awaits separate CLEAR.
2026-10-22 02:01:30 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 202 #1700 FED R5 landing | ddfb29af7 on 9c9732983: 6 manifest paths from a74cd9f87 (digests match CANDIDATE.sha256, patch-id 9d89077b equal); Duckworth APPROVE r5 rev 3026; C1 gate by Quackerjack on merge 3b86180b9: cli 771/771 skip 0 (run 2, run 1 red on gate TMPDIR length), 10 test-*.sh rc 0, M1-M13 killed; done rev 3108; landing note refused for length before the done move, so the record is the commit body and #1700 c30265; 191 and 194 rebased onto it (one scripts/mosaic case line)
2026-10-22 03:35:55 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 273 #1778 activate --send/--no-sso landing | 0bbf0f6d1 landed on 378a76b85, a rebase of cac7d0f67 (0042f713d) with all 6 range-diff pairs = and equal patch-ids; 4-path manifest e1031023 unchanged; Filbert APPROVE rev 3111 (c30308) carried merge-scoped under mos-1160, carry comment c30319 names 0bbf0f6d1. Gate on 0bbf0f6d1 by Darkwing: stack-release.test.mjs 218/218 skip 0, stack-release-external.test.mjs 25/25 skip 0, test-release.sh rc 0 (selftest 14/0, no skips), T58 controls c0 11/11 pass and c1 11/11 fail, M1-M7 killed. Rerun because row 202 changed scripts/mosaic, which stack-release.test.mjs copies. Red 1 was 978143bf4 (T53 test 6 usage pin). This doc commit adds the TOOLS.md usage line and F39 rules (Filbert N1).
2026-10-22 04:02:12 UTC | Rocko (d99326cc) | row191 full window0fa7bf421 | touched35files336pass, packages2403pass/1esbuildskip, shells425selftests+156queue node pass; SR01–07killed, SR08timeout,69unrun; stopped/no retry;88blobs restored,cwd sweep[],owned network removed,slot returned Sage; await Mos ruling.
2026-10-22 04:14:56 UTC | Rocko (d99326cc) | row191 control/resume sage-2427 | control12/12;SR08separate timeoutkill;49resumed assertionkills thenKS43survived;total56assertionkills+1timeout,1survivor,19unrun;88blobs restored,cwd sweep[],slot returned,Sage/Mos informed;no retry/edit.
2026-10-22 04:21:30 UTC | Rocko (d99326cc) | row191 KS43+19 retarget run | KS43assertionkill thenKS44invalid mutation syntax;57assertionkills+1timeout,1invalid,18unrun;stopped,no edit/retry;88hashes restored,cwd sweep[],slot returned,Sage/Mos notified.
2026-10-22 04:30:06 UTC | Rocko (d99326cc) | row191 KS44+18 resume | 10assertionkills thenKS54timeout-without-control despite dependency assertion;total67assertionkills+1timeoutkill,1unqualifiedtimeout,8unrun;88hashes restored,cwd sweep[],slot returned,Sage/Mos notified,no retry/edit.
2026-10-22 04:41:46 UTC | Rocko (Codex gpt-6.1-sol,d99326cc) | row191 #1689 builder complete/in-review | published0fa7bf421,manifest0bcbf337;35singles336pass,9packages2403pass/1knownskip,10shells425selftests+156queue nodepass;75assertionkills+2controlledtimeoutkills=77;88hashrestore,cwd sweep[],slotreturned;in-review rev3114,expected no-token failure3115(exit1);Sage/Dewey handoff,no live action.
2026-10-22 05:03:18 UTC | Rocko (Codex gpt-6.1-sol,d99326cc) | Vikunja502 processing test/runner hardening | brief+isolatedrocko-v502;expected wordingred→green,Node27/27,Python4/4,5/5assertionkills(includingSR08andKS54nohang),0skips/timeouts;sweep[],candidate5hashrestored,slotreturned05:01:29Z;candidate968d6ee3,runnerc5bdc764;packetready,no commit/push/liveaction.
2026-10-22 05:07:34 UTC | Rocko (d99326cc) | Vikunja502 committed/review handoff sage-2482 | newbranchrocko/vikunja502-ce6a-20261022,source8e3c6a79b+packet9bf4683c6 on ce6a96ad6;5paths no base drift,range-diff=,patch-id37135ebe unchanged;manifest968d6ee3,packet93889519 verifies39/39;focused runs carry;Sage publish/Dewey review handoff sent,no push/liveaction.
2026-10-22 05:10:00 UTC | Sage (1ef1e4f8) | serial landing 205 then 268 | 205: 3 commits, 4 paths, digest 858463b1, every scripts/test-*.sh green on ce6a96ad6, Duckworth N1-N4 carried (N1/N3 to 205b or R3, N2 to the 193b2 brief, N4 #1782 before any user thread goes live). 268 r4: c27207f6e on 205, 9 paths, digest caa0fa4e, patch-id 2942acc8 equal to r3. Overlap checks clean before both pushes. No live action; LIVE stays 86f95bb27.
2026-10-22 05:58:59 UTC | Rocko | row 279 #1784: before, deadline overshoot skipped final observation and returned unknown; after, one-line for (;;) retains final mapping and unchanged unreachable fallback. Regression red as specified; whole stop-run 13/13 and processing-control 62/62; M1 assertion kill, no skips, restored and swept. Source 1016b8584, packet cad927137; manifest 312ed511d8f1cff826ee2fd547de8368923130c7bfe06236d8e29d8655b80aaa. Pending Sage publication and Dewey review; no live action.
2026-10-22 06:15:54 UTC | Sage (1ef1e4f8) | row 279 #1784 landed (mos-1269) | Before: stopRun returned unknown for a live run when a sleep overshot the deadline. After: LANDED ceadad993 as a carry of cad927137 onto 343b6dd76 (range-diff 1:= 2:=, patch-ids 6d8ae7f4/e484c297 equal, Dewey carry c30410). Sage gate on cad927137: business 209/0/0, cli 794/0/0, discord processing + processing-stop 9/0/0. Correction: the first discord run wrote to a wrong out path and ran nothing; the rerun is the result. Overlap sweep clean (13, 257). Note rev 3144, done rev 3145. No live action; LIVE stays 86f95bb27.
2026-10-22 06:37:43 UTC | Sage (1ef1e4f8) | row 194 b16 third red (mos-1269/mos-1270) | Gate on ceadad993 + 9cdc47d04: business 228/0, cli 826/0 (T9a passes), control-board 124/0, bus 705/1 at grant-executor.test.mjs G13 (timed out 10044 ms); conversation, webui and scripts/test-*.sh never ran. runb8 green 436/0/0, 17/17 mutants. Correction to sage-2524 (mos-1270): the stuck wait is the first until at :733, the holding file after decision.resolve, so the executor never started the install and hosts.revoke at :734 never ran; it is not a revoke-path hang. Ruling: a diagnosis run, 30 base + 30 candidate alternating under cli load, no gate retry.
2026-10-22 07:51:04 UTC | Rocko | row 280 #1785: before, any TCP connect could accept a port our sshd failed to bind; after, attempt-local exact log proof plus liveness and connect, non-ephemeral picker, bounded scratch remote sleep cleanup. Both occupied regressions red on old acceptance; CRLF build failure captured and fixed under sage-2535. Committed source b765f5cb8, packet d6c924764, manifest27806c0579c76d0999d2e6be4180c3867570df0268af9d7e0d7b971af799e6f5. Repeats07:29:03Z–07:48:38Z grant1230/1230,host780/780,CLI1588/1588; final M1/M2 four assertion kills,0skips/timeouts,sweepclear. Sage publishes/gates; Duckworth reviews.
2026-10-22 08:00:02 UTC | Sage (1ef1e4f8) | row 280 #1785 landed | before: the bus tests' scratch sshd helper returned a port on a connect alone, and 194 b16's gate hung at G13 (base hung too, honker-0127). After: b0fd28b6f on refactor, a ff from da5d3651d (b765f5cb8 + Rocko slim packet; full evidence d6c924764 on rocko/row280-da5d-20261022). Duckworth approve rev 3153 (manifest 27806c05). Sage gate bus 656/0/0, ge 41/0/0, hr 26/0/0 (c30431). Rocko repeat 2010/2010 under cli 1588/1588, no timeout; M1/M2 killed 4/4. Two build-time slips recorded in Rocko's packet: a runner root path (no test ran) and the CRLF sshd log line. Overlap 13/257 clean. Note rev 3154, done 3155, #1785 closed c30432, P19 511 comment 623. 194 b17 rebases on b0fd28b6f (sage-2542).
2026-10-22 08:38:11 UTC | Sage (1ef1e4f8) | row 194 #1692 landed | before: 194 b16 9cdc47d04 went red three times (stopRun deadline, then the bus test sshd port race), fixed by rows 279 and 280. After: fd6dd327c on refactor, a ff on e122aad8e. b18 is b17 3f451772d rebased after my doc and queue push blocked a ff (16 =, series patch-id 64303253f8e6, packages equal b17). Webby approve rev 3160 on manifest 29f131ab. Honker gate on fd6dd327c: runb8 503/0/0, mutants 17/17; 18 packages 3668/0, ten scripts rc=0, Docker up, env skips named. The secret scan hit three fake_*_token fixtures in bus tests (false positive). Note 3161, done 3162; #1692 closed c30435; P19 414 comment 625. LIVE still 86f95bb27.
2026-10-22 08:42:04 UTC | Honker (Claude Opus 5.5, ecd17786) | row 194 #1692 agent lifecycle build | before: no Console or CLI lifecycle for agents (create, launch, terminate, regenerate, delete) and no per-agent hire/fire veto. After: 16 commits landed as fd6dd327c (b18, a rebase of b17 3f451772d on e122aad8e: 16 =, series patch-id 64303253f8e6, packages equal b17). Manifest 53 paths, digest 29f131ab. Final run on fd6dd327c: runb8 39 files whole 503/0/0, mutants 17/17 killed, restores matched; gate.sh merge onto e122aad8e, 18 packages 3668/0, ten scripts rc=0, Docker up, four env skips named (webui esbuild pin, queue render outside canonical root, test-task live provider x2); sweep clean. Corrections: b16 went red at G13; my diagnosis (60 runs, base hang on port 38927 sshd bind EADDRINUSE) placed it outside 194, fixed by row 280; the g13diag load kill missed its process group and overran about 8 minutes; one runb8 launch failed rc=126 on a lost exec bit before any test ran; the b17 runb8 was stopped unscored when refactor moved. In-review revs 3156/3157, Webby approve rev 3160.
2026-10-22 09:43:34 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 236 #1742 Gitea grant landing, and row 281 filing | 236 landed bd5f11ded at 09:37:31Z, a fast-forward on be2b008cc: 08840f3c1 rebased past 194 (commit 1 unions only, 2-7 =), e40b74beb for two stale out-of-manifest pins, bd5f11ded for a gitea-not-configured raise test. Dewey approve rev 3168 on manifest 0b00269b (34 paths). Megavolt gate under my slot: 2c bus 131, cli 224, route 28, docker 1; whole bus 765, business 228, cli 832, ten scripts rc=0; mutants 40/40; skips named, env only. Overlap with 13 and 257 clean. Note 3169, done 3170; #1742 closed c30451; P19 436 comment 639. Correction: the first whole run on ce74477e1 went red 762/764 on the two stale pins; that run is void. Follow-up per mos-1271: row 281 (#1786), a fire or delete revokes host and Gitea grants, Megavolt owns, Dewey reviews; brief docs/plans/2026-10-22_fire-revokes-grants.md; DEFERRED entry points at it.
2026-10-22 10:30:51 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 274 #1779 FED R3 broker user principal landing | 274 landed 4c485c63b at 10:28:26Z, a fast-forward on 47a35bf43: six Morgana commits d20c92bf6..4c485c63b. r5 f58cfc361 moved covers, LEVELS and the all marker into business/src/vocabulary.mjs so the broker keeps its sandbox import closure (M16/M16b/M16c). r6 4c485c63b pins USER_VERBS as the ruled seven and widens the R3 off-list set. Dewey approve rev 3178 on manifest 00047b52 (13 paths), comment 30487. Morgana gate under my slot, 10:09-10:22Z: 23 touched files 0 fail, bus 787, harness 151, business 228, cli 832, seat 167, ten scripts rc=0, mutants 22/22; skips named, env only (test-queue 1, test-task 2). Overlap with 13 and 257 clean. Note 3179, done 3180; #1779 closed c30491; P19 414 comment 646. Corrections: the r5 gate on f58cfc361 went red (M3x/M6x survived the R3 off-list set), void; my gate comment 30490 said 24 touched files, the count is 23; the kit comment guessed human-required for the M3x wall, the run code is not-holder.
2026-10-22 10:57:34 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 256 #1761 the human deletes a creative instance, landing | before: row 194's deleteRefusal refused every END_PROTECTED definition for every actor, so nobody could delete a creative instance; the human could only delist one with launch: false. After: 256 landed f12441d4f, a fast-forward on 0165be266 (the 274 landing plus its docs and queue commits): four Honker commits b5f1e09d6..f12441d4f, patch-id b28e63746f8c. One protectedRefusal predicate serves delete and the listing's per-viewer endProtected; any non-human actor gets organization-creative-human-only, fire unchanged. Rebased twice (past 236, then past 274), each range-diff 4/4 =, carries checked by Webby (webby-0126, webby-0128). Webby APPROVE on manifest-r3 01eb75aa (10 paths), comment 30519, posted by Sage as jarvis because Webby has no token; review request c30518 (Sage, revs 3183/3184) after Honker's request failed (no token, revs 3181/3182). Gate by Honker under Sage's slot 10:33Z-10:52Z on merge tree ced6bf5f8: touched files 31/0/0, 18 packages 3757/0 (bus 788, business 231, cli 832, webui 335), ten scripts/test-*.sh rc=0, mutants M1, M2, M3, M4a, M4b killed with blob-checked restores. Skips, named and environment-only: webui editor rebuild byte check (esbuild 0.28.1 installed, pin 0.28.2), test-queue non-canonical root, test-task live provider x2. Overlap with in-review rows 13 and 257 clean. Order change sage-2594: 256 landed before 277 because 277's gate had not run. LIVE is still 86f95bb27. Queue note rev 3186, done rev 3187; #1761 closed c30520; P19 416 comment 649.
2026-10-22 11:19:28 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 277 #1782 the owner sees a thread agent's claims, landing | before: a thread run's role.claim wrote thread_claims only, and the agents verb read role_claims only, so the owner had no way to see a thread agent's claim (PRD :1083-1084). After: 277 landed 62f53b1e0, a fast-forward on 46dedf925 (the 256 landing plus its docs and queue commits): seven Morgana commits 1d5d00939..62f53b1e0, range patch-id 7f0533520bd2. A bus verb thread.claims serves human and reader sessions; mosaic agents adds thread lines without --tester or --json, --json keeps the agents array, the tester path makes one call, a refused thread.claims prints the role lines and the refusal code, and an error with no bus code still throws. Rebased onto 0165be266, then onto 46dedf925 after 256 landed first (sage-2594); both range-diffs all =, manifest e281aa2c unchanged (duckworth-0126). Duckworth APPROVE comment 30539, recorded rev 3192, posted by Sage as jarvis; review request c30538 (revs 3190/3191) after Morgana's request failed (no token, revs 3188/3189). Gate by Morgana under Sage's slot 11:01:23Z-11:13:56Z: 7 touched files 0 fail (thread-claims 5, message-filter 6, user-principal 22, format 4, tester 11, other-tree 4, host 43), bus 793, cli 836, harness 151, business 231, seat 167, ten scripts/test-*.sh rc=0, M1-M9 killed on committed bytes. Skips named, environment only: test-queue non-canonical root, test-task live provider x2. Overlap with in-review 13 and 257 clean. Duckworth N1 (M8 dies on a rethrown error) to DEFERRED; 0123 N3 per-session claims binds 264a r7. Note rev 3193, done rev 3194; #1782 closed c30540; P19 506 comment 652. Also landed as doc-only picks: Honker's 256 BUILD.md gate results (80620a3a0 as b83173305) and Megavolt's 236 BUILD.md runs (9c6e1a165 as 01ea1fb11). LIVE is still 86f95bb27.
2026-10-22T11:37:16Z | Gander row278 #1783 implementation prepared, verification held | Before: owner thread claims were visible through row277 but absent from the Console Agents view. After: prepared a separate four-field table, a Console-owned read adapter, section-local refusal handling and an explicit tester refusal before busRead. Ten tests and six unique check definitions are prepared but unrun. Base moved from980cec88c to6af85df4b under sage-2625 on new gander/row278-b; no candidate path overlapped. No Node process, syntax check, test, browser, queue command, push helper or live action ran. Next: Sage CLEAR for the queue claim and gate; Webby reviews the verified manifest.
2026-10-22T12:09:45.285844+00:00 | Gander row278 run1 void, loader fake repaired | First new-file run stopped3/1/0 because the fake omitted BusReadError imported by base chat.mjs. Sage-2638 places the first-own-test red and permits the assertion-preserving export; test-only commit964b6b184. Runtime bytes unchanged. All run1 results void, no timeout or run leftovers. Run2 must restart every step after busy CLEAR; a second red goes to Sage then Mos before any change. No product acceptance or push claimed.
### 2026-10-22 | Gander | row278 gate run3
Fresh gate on3e4c77ac6 under sage-2648: bus793/0, WebUI345/0/1 (esbuild version skip), all ten shell files425/0, touched files15/0, M1-M6 killed6/6. No timeout; sweep0 run-created leftovers. Runs1/2 void. Seven-path manifest8fa859a0 goes to Webby for final review. No landing or live change.
2026-10-22 11:29:09 UTC | Rocko | V502 carry/rerun on980cec88c: source407c8d7be and packet04a86cf33 match approved8e3c6a79b/9bf4683c6 patch-ids, no conflicts. Dewey0834 carry conditioned on rerun, satisfied under sage2616: stop-run13,processingHTTP6,bustasks9 allgreen (Node28/28),Python4/4,0skips/timeouts. Manifest5758d699,source5/5restored,cwdsweep[]. Doc rerun packet6af85df4b explicitly pushed to rocko/vikunja502-980c-20261022; no live action.
2026-10-22 11:30:17 UTC | Sage | Vikunja 502 (row 191 follow-up, no queue row) LANDED 6af85df4b, a fast-forward from 980cec88c. Before: the processing failure diagnostics and cleanup in stop-run, processing HTTP and bus tasks tests were Dewey-approved on the pre-194 base only (8e3c6a79b/9bf4683c6). After: Rocko's picks 407c8d7be/04a86cf33 carry the same patch-ids (37135ebe/0dbcda54), plus a doc-only rerun record 6af85df4b. The rerun on the new base was green: stop-run 13/13, processing HTTP 6/6, bus tasks 9/9, Python 4/4, 0 skips. Dewey carried the approve (dewey-0836). mosaic-task.mjs is unchanged, so N7 (runs suite) didn't trigger. Overlap with in-review rows 13 and 257 was clean. LIVE is still 86f95bb27.
2026-10-22 13:10 UTC | Sage | Rows 278 and 281 LANDED. Before: refactor at 6af85df4b (V502). Row 278 (#1783, Gander, Webby): the Console Agents view shows thread claims in their own section. It landed as 385a7194c at 12:39Z, ff, 9 commits, manifest 8fa859a0. Run 3 was green: bus 793, webui 345/0/1, ten shells 425/0 under the 3x limits (mos-1275), M1-M6 killed. Runs 1 and 2 were void, on fixture fixes 964b6b184 (placed by Sage) and 9696d1fcb (ruled by Mos, mos-1275). Webby approved (c30588, rev 3200). Done rev 3202, #1783 closed c30589. Row 281 (#1786, Megavolt, Dewey): a fire or a delete revokes the instance's host and Gitea grants. It landed as e479521d3 at 13:06Z, ff, 17 commits, manifest b73d4c81. Slot on 7979fb41e: bus 819, business 231, cli 837, dewey-bus 76 with the Docker gitea-grant test, shells green, M1-M22 killed. The M1 hang was fixed at its cause in b4a (mos-1273). Dewey approved (c30614, rev 3207). Done rev 3209, #1786 closed c30621. Skips on both: test-task's row 244 live-provider pair and test-queue's canonical-root check, plus webui's esbuild rebuild test on 278. After: refactor at e479521d3. DEFERRED moves the grant gap to Done and opens Dewey's N1/N2 text gaps. LIVE is still 86f95bb27.
2026-10-22 14:18:23 UTC | Sage | Row 264a LANDED (#1769, Gizmoduck, Duckworth): sessions per thread, schema 3k-3l. Before: refactor at e479521d3 (row 281). It landed as 84f53540d, ff, 21 commits, manifest 257408e9 (Duckworth approve rev 3214, comment 30640). Gate r13 on 2cc34aef8: 20 touched files, bus 827, cli 837, webui 345/1 named skip, ten shells under 3x limits, mutants M1-M4 killed on assertions. Corrections on the way: the kit stopped on first red, so four reds surfaced one at a time. r10 later3l missed a trigger (mos-1276). T2 had failed since r2 because refusals log action.refused (mos-1277, my sage-2257 wording). threads.test:341 kept the old trigger text (mos-1278). M2 was caught by the store trigger before any assertion, so it now drops the trigger too (mos-1279, four files with the SCHEMA_SHA256 pin). Deferred: actor_run pin binds row 269, esbuild skip to a later gate, session-scoped user reply binds row 272. No live change; LIVE is still 86f95bb27.
2026-10-21 21:24:47 UTC | Gander row253 #1758 design r1 | Investigation began21:12Z under sage-2162; recorded now. Before: case2 installer still renders tester TCP and case1 acceptance requires it. After: proposed design covers socket marker migration, actual configured dataRoot within unchanged hardening, target-uid metadata queries, kernel-peer acceptance observation and explicit same-uid residual. Initial inventory saved; no passing final static-check claim, product edit, test, host or live action. Duckworth design review precedes code.
### 2026-10-21 row253 implementation prepared, gate pending
Gander: after Duckworth-0100 design approval with C1-C5, prepared the two host-script socket migrations, fake fixtures, updated retirement docs and 33 unique change checks. Static syntax/diff and source inventory checks pass; behavioral tests have not run. Sage-2191 queues the requested full scope and will send CLEAR. No host, unit or live config changed.
2026-10-22 02:13:30 UTC | Gander row253 C4 committed candidate, gate pending | Sage-2335 explicitly directs committing runnable source and tests before the whole gate. The repair implements the approved after-uid supervisor with separate inner/outer bounds; the static packet is b64aa316a. Current repaired bytes have run no syntax check, test, probe or host experiment. Earlier static syntax results apply only to the old build. Results from 8a8df4171 are historical and do not carry. Duckworth static verdict and Sage CLEAR precede all gate execution. F5 is row276 / #1781 in DEFERRED.md.
2026-10-22 14:45 UTC | Gander row253 focused27 on finalaca16404c base | Fresh0604d91fc / manifest573c51cf:27 whole files604/0/0, no timeout/retry, case134/0 in99.281s, case243/0 in66.418s, stack-release includingT46221/0 in583.453s. N9 sandbox bind ran and passed, no focused skips. Seven source blobs unchanged; sweep0 run-created leftovers, pre-existing infrastructure receipts retained. Whole gate and M1-M40 await separate Sage CLEAR after Duckworth rebase check. No live action.
2026-10-22 14:46 UTC | Gander correction to the row253 focused27 entry immediately above | stack-release whole was218/0/0, not221/0. The27-file sum604/0/0 and duration583.453s are unchanged; raw files/results.json and SUMMARY.json give the exact per-file totals.
2026-10-21T16:04:23Z | Q195/Q196 writing editor design, #1752 / Vikunja #448 | Before: row 154 edits Markdown in a separate Docs view, with no selected-word thesaurus or document pins. After: design draft on gyro/editor-design compares three editor approaches, records WordNet source/license/size, folds the pane/pin model and specifies guarded context/proposal APIs, DesignCraft handoff and seven proposed briefs. Q196 changes the basis to a Word-like interface. Verification so far: editor/provenance checks 10 pass, 1 explicit rebuild skip (pinned esbuild absent); brief fields, unique headings and 11-line checklist checked. Product, queue and live state unchanged. WordCraft scratch evaluation is being incorporated before publication. Review and Jason choices remain open.
2026-10-21T16:22:36Z | Q195/Q196 design verification, #1752 / Vikunja #448 | Folded Fenton 7caf8008b and Louie 880ef19e0. Recommend bounded O3: Word-like DOM editor plus isolated headless DOCX export, with explicit feature cut and semantic loss checks. WordCraft web 44,179,239 bytes, identical twice; measured comment/footnote conversion limits recorded. Seven briefs and 12-line checklist validated. Fresh editor/provenance: 10 pass, 0 fail, 1 explicit rebuild skip (esbuild 0.28.2 absent). CI queue guard identity gyro/purpose push returned documented degraded-pass (exit 0), CANNOT_ASSERT credential-unresolvable, scratch audit gyro-q195-ci-queue-audit.jsonl; no fallback or CI-green claim. Documentation-only candidate for Mos/Fenton review, no source, queue or live changes.
2026-10-22T16:06:34Z | Q195/Q196 design review fixes, #1752 / Vikunja #448, mos-1300 / fenton-0004 | Before: draft 105811400 had owner-only thread wording, stale source positions, two mismatched seat/model assignments, an assumed library save boundary and 12 owner questions. After: grant-aware pin validation and stale/revoked access redaction; references refreshed against aca16404c, including public/bus.js and README offsets; Sage assigns model-only seats; unimplemented library.file.save/C2-11b brief blocks book editing; tabs explicitly cover 62-76rem; J7 stricter context choice recorded; pronouns removed; six owner questions. Static docs/source-anchor checks only, no builds, product suites, MVP gate slot or live action. Fenton re-check pending; O3 and both companion folds retained.
2026-10-22T16:09:56Z | Della mos-1303 / Vikunja 460 Q217/Q222 revision | Before: Q1/Q2 open and Q3-Q7 phrased as Jason questions. After: Jason-created InMotion/woltje.com ingress recorded; per-source mailboxScope defaults none with human-only CLI then Console writer and named default/change/agent-refusal tests. Q3-Q7 settings/defaults/owners documented; no new external decision required for default development, conditional provider/spend/exposure list retained. PRD/grant/data.delete/live gates preserved. Docs only, no product suite or live operation. Earlier uncommitted append-only PRD review session receipts are preserved in this docs commit. Ludwig reviews before Sage landing.
2026-10-22T16:12:26Z | Della ludwig-0035 / mos-1303 / Vikunja 460 focused corrections | Verified REQ-WAIT-2/3 at db1e7732a: Jason approval is required to route mail.access out of hard waits. Restored explicit Jason-approved PRD gate; no Q211 waiver inferred. Named tunable parser/attachment settings, defaults and schema/test review ownership of resource bounds; E04 checks changed values and bound refusals. Clarified Q8 extraction stays Jason choice. Docs-only checks, no product/live action; focused review before Sage handoff.
2026-10-22 13:06 UTC | Megavolt | Row 281 (#1786) build. Before: a fire or delete left host_grants/gitea_grants rows, the host line and the Gitea token live. After: step G in broker.mjs revokes them in a detached job per agent (the host line first, then Gitea local-first with an admin preflight); a refusal is grant.revoke.refused plus a human message; open grant decisions are in the set; a raise for a fired agent refuses instance-fired. Tests T1-T14 (bus fire-revokes-grants, lifecycle-revoke, grant-executor, gitea-grant; cli lifecycle). Gate on 7979fb41e: base 121/121 + 24/24, 29 anchors, M1 exit check rc1/17, M1-M22 22/22 killed, dewey-0840 files 76+14+23, bus 819, business 231, cli 837, ten shells rc 0 under the mos-1275 limits; skips: test-task row 244 live-provider pair, test-queue canonical-root check. Corrections: the first base run on c174f37ac went red on five first-run test defects (b3, test only); the first mutant run on 06ee5ed6b hung on M1 because T6c's held loop never ended (b4a fixed the teardown, and the runner now has timeout -k 10 180 and an exit check without --test-force-exit).
2026-10-22T16:36:30Z | Della row224 C2-05b #1725 preparation | Before: C2-05a queries exist at 4f3fbc25a but no broker lookup verbs or typed tools. Scope: broker/tools plus Sage-authorized runtime/bundle integration, tests and docs. Existing human-only library reads retained; five agent queries require a live holder and trusted configured source. Code/docs only under row253 gate; no Node/test/push/claim. Runtime ownership coordinated with Rocko219/Huey272.
2026-10-22T16:52:55Z | Rocko | Row219 prepared under sage2793/2807; explicit creative thread/default-session selection, narrow metadata route/picker U1–U5,22 static mutant sites; unrun while253 holds.
2026-10-22T17:42:16Z | Sage | Row 253 (#1758, Gander, Duckworth) LANDED 48e450ce8. Before: refactor at aca16404c; case-sudo calls had no supervised session check and the M21 survivor hid behind a fast-exit fixture (mos-1297). After: a fast-forward of 22 commits; manifest 591020a1 rechecked with 0 mismatches; run 5 whole gate green with M1-M40 killed, the M38 PID equals SID assertion added under mos-1298; Duckworth APPROVE rev 3252, note rev 3253, done rev 3254; #1758 comment 30701 and closed; P19 416 comment 719. N1 (fast child reported as exit 85) is DEFERRED as Vikunja 532.
2026-10-22T18:11:57Z | Sage | Row 275 (#1780, Morgana, Duckworth) LANDED 3410df7c3. Before: refactor at 48e450ce8; viewers had no verb list of their own and board reads were not refused to users. After: a fast-forward of 3 commits; manifest 39847b15 (4 paths) blobs match; gate 17:52:12Z-18:05:00Z green (whole files 7/22/6, bus 834, cli 861, harness 151, business 231, seat 167, 10 scripts with 3 named skips), M6 and M10 killed; Duckworth APPROVE rev 3260 (comment 30717); note and done by sage-2897; #1780 comment 30728 and closed. N1 (board refusal test as a loop over BOARD_VERBS) is non-blocking.
-1
View File
@@ -22,7 +22,6 @@ RUN npm ci --ignore-scripts
# runtime adapters.
COPY contracts /opt/mosaic/contracts
COPY src /opt/mosaic/src
COPY packages/processing-control/src/state.mjs packages/processing-control/src/entrypoint.mjs /opt/mosaic/processing-control/
COPY adapters /opt/mosaic/adapters
RUN chmod 0555 /opt/mosaic/contracts /opt/mosaic/contracts/* \
&& chmod 0555 /opt/mosaic/src /opt/mosaic/src/*.sh \
+4 -24
View File
@@ -75,8 +75,7 @@ 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. 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_EXTENSIONS` | `pi` only: extension files loaded with `-e`; a missing one exits 2. |
| `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. |
@@ -87,8 +86,7 @@ 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 as a file: `pi` with `--system-prompt <path>`
and `claude` with `--system-prompt-file <path>` (row 213). Neither adapter loads a
`prompt.md`, which they pass with `--system-prompt`. 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
@@ -106,23 +104,5 @@ 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. 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.
64 KiB together, since `claude` passes `prompt.md` as one argument and Linux
caps one argument at 128 KiB.
+3 -10
View File
@@ -27,11 +27,6 @@ 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
@@ -61,10 +56,7 @@ fi
# Flags:
# -p one-shot: print the answer and exit
# --output-format text stdout is the answer only
# --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
# --system-prompt the generated prompt replaces the default
# --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
@@ -84,9 +76,10 @@ 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-file "$PROMPT_FILE" \
--system-prompt "$PROMPT_CONTENT" \
--model "$MOSAIC_MODEL" \
--effort "$MOSAIC_THINKING" \
--restricted \
+4 -14
View File
@@ -8,17 +8,7 @@
set -eu
[ -n "${MOSAIC_SYSTEM_PROMPT_FILE:-}" ] || { echo "pi adapter: MOSAIC_SYSTEM_PROMPT_FILE is required" >&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; }
[ -r "$MOSAIC_SYSTEM_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.
@@ -104,8 +94,7 @@ 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, given
# as the file's absolute path (checked above), never its text
# --system-prompt replace the default prompt with the generated one
# --no-* no ambient context/skills/extensions/templates/themes
# EXT_FLAGS the explicitly provided extensions only (-e)
# SESSION_FLAGS ephemeral | persistent | forked (per env)
@@ -114,6 +103,7 @@ 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 \
@@ -128,7 +118,7 @@ set -- \
--provider "$PI_PROVIDER" \
--model "$PI_MODEL" \
$THINKING_FLAG \
--system-prompt "$PROMPT_FILE"
--system-prompt "$PROMPT_CONTENT"
# One-shot mode appends -p and the request (both safely quoted);
# interactive mode appends nothing - clean TUI.
[ "$INTERACTIVE" = "1" ] || set -- "$@" -p "$MOSAIC_REQUEST"
-10
View File
@@ -20,18 +20,8 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
| Morgana | Builder, MVP T1 row 193 (human users and RBAC) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Honker | Builder, MVP T1 row 194 (agent lifecycle) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Gander | Builder, the human boundary across uids (P19 333, #1694) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
| Herb | Builder, a second gate lane (#1695) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
| Bushroot | Builder, retire the control board writer on 7331 (#1696) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1008 | none, T3 only |
| Megavolt | Builder, MVP T1 item 6 (agent credentials and git identity, #1697) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Quackerjack | Builder, federation R5 and R2 (#414) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1023 | none, T3 only |
| Gizmoduck | Federation design (#414) and builder of FED R1 | T3 session (Claude, Opus 5.5, thread 9312a896) | none, T3 only |
| Webby | Reviewer: Case 2 designs, rows 192 and 194 | T3 session (Claude, Opus 5.5, thread 03086006), started by Mos (mos-0981) | none, T3 only |
| Duckworth | Reviewer: federation designs, row 193 | T3 session (Claude, Opus 5.5, thread ffceafec), started by Mos (mos-0977) | none, T3 only |
| Della | Builder, email plan (Vikunja 460) and row 224 (C2-05b) | T3 session (Codex, gpt-6.1-sol, thread 66735a7f), started by Mos | none, T3 only |
| Scrooge | Builder of row 252 and reviewer of row 272 | T3 session (Claude, Opus 5.5, thread 629b6cd3), started by Mos | none, T3 only |
| Huey | Builder, row 272 (T1 sessions) | T3 session (Codex, gpt-6.1-sol, thread cabd45ca), started by Mos | none, T3 only |
| Louie | Builder and research, editor evaluation and row 174 | T3 session (Codex, gpt-6-luna, thread bfd13d49), started by Mos | none, T3 only |
| Ludwig | Builder and reviewer, post-v1 email PRD and row 258 (E01) | T3 session (Claude, Opus 5.5, thread 8615b2a6), started by Mos | none, T3 only |
| Sage | Project lead: coordination, review, integration; earlier DYOR strategy records retained | T3 session (Claude Code); Pi launcher `zai/glm-5.3:high` retained | `agents/sage/launch.sh` |
Each script supports `--check` and `--fresh`. Normal launches resume the agent's
-8
View File
@@ -1,8 +0,0 @@
# Bushroot
Builder seat, Claude Haiku 5.5, started by Sage under mos-1008 item 2 (a low-level model under Q177) for the row
that retires the control board writer on 7331 (#1696).
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `bushroot/*`
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
-161
View File
@@ -1,161 +0,0 @@
# 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
@@ -1,388 +0,0 @@
# 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
@@ -1,287 +0,0 @@
# 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
@@ -1,249 +0,0 @@
# 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
@@ -1,173 +0,0 @@
# 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
@@ -1,91 +0,0 @@
# 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
@@ -1,191 +0,0 @@
# Row 251 build: T27 lists no identities right after `ssh-add -t 1` (Vikunja #453)
Brief `docs/plans/2026-10-21_t27-ssh-add-lifetime.md` (rev 2931), Mos ruling mos-1185 item 2, assigned in
sage-2075. Owner Darkwing, reviewer Dewey, gate owner Sage. Branch `darkwing/row251` from 3b0aef995,
claimed in-progress at rev 2938.
## Step 1: the failure names its cause (e5e4b2e5a)
T27's two lists now carry a message that prints only when they fail. It gives the time from addKey's
start to its resolve and to each `ssh-add -l` call, the hrtime at each with how far into its second it
fell, `/proc/uptime` at each, and whether the agent process and its socket were still there. The checks
themselves did not change. The commit holds nothing else.
## Step 2: the cause
ssh-agent counts a key's lifetime in whole seconds of CLOCK_BOOTTIME, at the add and at the reap. I read
OpenSSH 10.5p1, the version on dragon-lin (`ssh -V`: OpenSSH_10.5p1), at tag V_10_5_P1 (ssh-agent.c
sha256 873691c4, misc.c sha256 ee467f43):
- `misc.c:1790-1798` and `:1828`: `monotime()` returns `ts.tv_sec` of `clock_gettime(CLOCK_BOOTTIME)`,
falling back to MONOTONIC. The fraction of the second is dropped.
- `ssh-agent.c:1315`: the `-t` constraint sets `*deathp = monotime() + seconds`. Lines 1410 and 1598
do the same for the agent's default lifetime.
- `ssh-agent.c:1064`: the reaper drops a key when `now >= id->death`, `now` being `monotime()` again.
- `ssh-agent.c:2650`: the main loop runs `reaper()` after every poll, before it reads requests. A list
that arrives after the boundary sees the key gone.
So a key added at second s plus a fraction f dies at s + N, and `-t N` keeps it N - f seconds, between
N - 1 and N. With `-t 1`, an add late in a second leaves the key milliseconds to live. That matches
Sage's probe in the brief: 8 of 80 `-t 1` adds missed, each one made at 0.9947-0.9973 of a monotonic
second and finished at 0.998-0.0008, and none earlier in the second missed. It also matches proof 5's
failure, where the first list ran 31.9 ms into the test and found no identities.
Node's hrtime is CLOCK_MONOTONIC. BOOTTIME differs from it by the time the host spent suspended. On
dragon-lin the difference read 0.000001 s at 19:09Z, so a monotonic second boundary is a BOOTTIME one
here.
`addKey` and the agent start are not the cause. `addKey` passes the lifetime through as `-t`
(`host-run.mjs:90`), and the agent starts with no default lifetime (`:175`). Production adds the key
with `timeout + 30` (`:180`), where losing up to a second makes no difference. The only caller that
can't afford the lost fraction is T27, which asks for 1 s. The fix belongs in the test's lifetime choice,
and `host-run.mjs` doesn't change.
The slot confirms this against T27 itself (below), not just from the source.
## Step 3: the fix (this commit)
T27 adds the key with `-t 2`. The agent then keeps it more than 1 s and at most 2 s. The first list
runs milliseconds after the add, so it has over a second of margin even under load. The sleep before
the second list grows from 2.5 s to 3.5 s, which keeps the old 1.5 s margin past the new 2 s ceiling.
Both checks stay: listed after the add, gone after the lifetime. A comment above the add says why the
lifetime is 2.
I considered `-t 3` with a 4.5 s sleep for a 2 s floor. One second of floor is far above anything the
first list has needed (31.9 ms in proof 5), and the concurrent-suite run below tests it under load, so I
took the smaller change.
## Base
The branch stays on 3b0aef995. By 19:15Z origin/refactor was at 6096b051b, after rows 66 and 243
landed. `git diff 3b0aef995 6096b051b` touches neither `packages/bus/src/host-run.mjs` nor
`packages/bus/tests/host-run.test.mjs`, so there's no rebase (sage-2084). Row 243 did change
`packages/bus/tests/process.test.mjs`. My slot's bus suites run that file as it was at 3b0aef995, and the
gate rerun on Sage's slot runs it as landed.
## Prediction for part A on e5e4b2e5a (written before the slot)
The hook starts `ssh-add -t 1` at the given fraction of a monotonic second. From the brief's probe, the
agent stores the key about 3 ms later. The first list reaches the agent a few ms after addKey resolves:
Node's exit event, the mark, and `ssh-add -l`'s own start. A red needs the add to land before the second
boundary and the list to land after it.
- 0.990: green. The add and the list both land before the boundary.
- 0.993, 0.995, 0.996, 0.997: red expected. The add lands before the boundary, the list after. At 0.993
and 0.997 I'm less sure, because the list (at 0.993) or the add (at 0.997) can land right at the
boundary. A green at either is in line with the cause, and so is a red.
- 0.998: green. The add lands after the boundary, so the key gets almost a full second.
Every red should show "first list got" an answer with no SHA256 line. It should also show addKey
resolving before a second boundary and the first list starting after it or within a few ms of it, with
the agent alive and its socket present.
Under sage-2084's rule, on e5e4b2e5a only a red at 0.990 or 0.998 counts toward the stop, and two such
reds stop the run. On the fix commit any red counts, and two stop it. In parts B to D, the first red stops
the run.
## Slot plan
All scripts are in `/mnt/storage/scratch/darkwing-251-out/`, launched from `/`, each behind the busy
checks and chained with `&&`, TMPDIR under `/mnt/storage/scratch/tmp`.
1. Confirmation. `align-hooks.mjs` is a load hook, outside the repo, that serves `host-run.mjs` with
`addKey` wrapped to start `ssh-add` at a chosen fraction of a monotonic second. T27 runs unchanged
under it, alone (`--test-name-pattern`). It runs at fractions 0.990, 0.993, 0.995, 0.996, 0.997 and
0.998, three times each:
- on e5e4b2e5a (step 1, `-t 1`). Expected: some first lists fail, and the step 1 message shows the
add's resolve and the list on either side of a second boundary, with the agent alive and the
socket present. These reds are the point of the run.
- on the fix commit (`-t 2`). Expected: all 18 pass.
2. `host-run.test.mjs` whole, once, on the fix commit.
3. The bus suite whole, three times in a row.
4. The bus suite whole in two clean worktrees at the same commit, started together.
## Slot results
Sage's CLEAR came in sage-2154 at 20:58:55Z. `slot-251.sh` ran parts A and B from 20:59:22Z to 21:01:33Z,
on node v26.8.1 and OpenSSH_10.5p1, with TMPDIR `/mnt/storage/scratch/tmp/r251`. At the start, BOOTTIME
minus MONOTONIC read 0.000001 s, then 0.000000 s twice (dewey-0769 N1). So the hrtime fractions below
put the agent's second boundary in the right place.
### Part A: T27 alone under `align-hooks.mjs`, 3 runs per fraction
The fraction is when the hook started `ssh-add`. The hook logged each start within 0.0001 of its target.
| fraction | e5e4b2e5a (`-t 1`) | 68f2dfd4b (`-t 2`) |
|---|---|---|
| 0.990 | 3 pass | 3 pass |
| 0.993 | 3 pass | 3 pass |
| 0.995 | 3 red | 3 pass |
| 0.996 | 3 red | 3 pass |
| 0.997 | 3 pass | 3 pass |
| 0.998 | 3 pass | 3 pass |
e5e4b2e5a went red 6 times out of 18, every time at a fraction the prediction named, so none of them
counted toward the stop. 68f2dfd4b passed all 18. The fix runs used 68f2dfd4b, whose test blob is the same
as c19b4167e's (280132c7).
Every red failed on the first list and got `"The agent has no identities.\n"`, with the agent alive and
its socket present. The step 1 message gave these hrtime fractions. "addKey start" is the mark before the
hook's wait, so its fraction is earlier than the ssh-add start.
| run | addKey resolved | first list mark |
|---|---|---|
| 0.995 rep 1 | 0.9992 (+317.6 ms) | 0.9993 |
| 0.995 rep 2 | 0.9991 (+846.5 ms) | 0.9991 |
| 0.995 rep 3 | 0.9988 (+845.0 ms) | 0.9988 |
| 0.996 rep 1 | 0.9999 (+839.8 ms) | 1.0000 |
| 0.996 rep 2 | 0.0004 (+845.0 ms) | 0.0004 |
| 0.996 rep 3 | 0.0002 (+835.9 ms) | 0.0002 |
At 0.995 the add finished about 4 ms in, before the boundary, and the list mark came just before it. The
`ssh-add -l` process then reached the agent after the boundary. At 0.996 the agent stored the key before
the boundary, and addKey resolved just after it. In both cases the key's whole second ended between the
add and the list, which is the cause step 2 gives. The two edge fractions came out green on the first
build. At 0.993 the list still reached the agent before the boundary, and at 0.997 the add landed after
it, so the key got almost a full second. The prediction allowed either result at those two.
### Part B: `host-run.test.mjs` whole on 68f2dfd4b
24 tests, 24 passed (21:01:18Z, 14.0 s).
### The stop before C1, and the resume
At 21:01:33Z the script stopped before C1, with exit 4 and no red. My own `busy.sh` matched two `sleep 60`
processes, mine, in `darkwing-r251/packages/bus`. Part B left them. They come from T27 "the start sweep
keeps a live bus dir and kills only a matching live process" (`host-run.test.mjs:528` and `:536`). Its
`t.after` SIGKILLs the `/bin/sh -c 'sleep 60; :'` parent, but the `sleep` child lives on for up to 60 s.
That code is outside this row's diff, so the row leaves it alone, and it's recorded below as a follow-up.
Sage confirmed (sage-2159) that the right move is to resume only the scope that hadn't run.
`slot-251-cd.sh` runs C and D as `slot-251.sh` has them, with the same two busy checks. Before each check,
it waits up to 75 s for those orphans to exit: my uid, my trees, a command line of exactly `sleep 60`.
It logs them as it waits. Any other process still trips the checks.
### Parts C and D: the bus suite whole on 68f2dfd4b
| run | start | tests | pass | fail | time |
|---|---|---|---|---|---|
| C1 | 21:02:48Z | 598 | 598 | 0 | 21.6 s |
| C2 | 21:04:06Z | 598 | 598 | 0 | 21.4 s |
| C3 | 21:05:26Z | 598 | 598 | 0 | 22.1 s |
| D1, darkwing-r251 | 21:06:44Z | 598 | 598 | 0 | 24.2 s |
| D2, darkwing-r251b, TMPDIR `r251-b` | 21:06:44Z | 598 | 598 | 0 | 24.2 s |
The script waited 33 s before C2, 34 s before C3, 33 s before D and 32 s before the sweep. Each time it
was waiting for that run's `sleep 60` orphans (two per suite, four after D), and nothing else. After the
wait, the sweep of my trees, the out dir and TMPDIR found 0 processes left (21:08:04Z). Sage confirmed
(sage-2159, sage-2160) that the short gitea-api and `node --check` runs they made during the slot weren't
stops. None of my busy checks refused on any of them.
These runs used the bus files as they were at 3b0aef995. Since then, origin/refactor (666173292, then
7247d80c7 and c3d88840c with row 196) has changed `broker.mjs`, `runtime.mjs` and `server.mjs`, which T27's
file imports, along with other bus files. None of those commits touch this row's two paths or
`host-run.mjs`, and `git merge-tree` of c3d88840c with 68f2dfd4b is clean. The gate rerun in
a clean worktree on Sage's slot runs T27 against them.
### Follow-up, not in this row
T27's start-sweep test leaves the `sleep 60` child of each planted shell running for up to 60 s after
the test. Killing the process group (`process.kill(-pid)`; both shells are spawned `detached`) or
`exec sleep 60` in the shell would end the child along with its parent.
-113
View File
@@ -1,113 +0,0 @@
# Row 273 (#1778): activate --send and --no-sso
Darkwing, 2026-10-22. Brief: `docs/plans/2026-10-22_activate-send-nosso.md` (blob in f0d0b31e9), approved by
Mos in mos-1246 (F39). Base: origin/refactor 0042f713d. Reviewer: Filbert. Gate owner: Sage.
## What changed
Only `scripts/stack-release.sh` and its two node test files.
- `activate --send` puts `--send` into the run's Console flags after `--sso` and before the tester
flags, where webui-service.sh renders it. It refuses `send-needs-sso` unless those flags hold `--sso`,
given or carried. The plan line is `send: on; --send was given`, or `send: carried` when the pointer
already holds it. `check_target` lets it name the pointer's copy only when the pointer holds no `--send`,
so a second `--send` on the same copy refuses `already-active`.
- `activate --no-sso` refuses `sso-not-on` when the pointer holds no `--sso`. Otherwise it takes the
Console flags from a rollback set (below) and keeps the pointer's tester flags. `send_for_target`
drops `--send`, whether it came from the set or the pointer. It is the only place that does, so one
mutant there (M3) shows the drop is tested. The plan prints `sso: off; Console flags from <set>/<file>; restores <flags>`
and a send line, `send: dropped; --no-sso drops --send (F39)` or `send: off; --no-sso was given and the
pointer holds no --send`. The steps log gets `drop --send; --no-sso was given` and
`turn off --sso; the Console flags come from <set>/<file>`.
- Usage errors: `--send` with `--no-send`; `--no-sso` with `--sso`, `--send`, a proof pair or
`--no-proof`; either flag on any verb but activate; either flag under `--units external`.
`--no-sso --no-send` is allowed, since the brief doesn't forbid it and both drop the same flag.
- The set read that `sso_base` did inline for a rollback to a Console without `--sso` moved into
`set_flags`, which both paths call. Its three messages are unchanged, and T57 still pins them.
- `from_log` has a new mode, `sets`: the activation of each ok activate and rollback line, newest first,
stopping at the adopt line, which took no rollback set.
## The consequential choice: which set --no-sso reads
The brief says the flags come from "the rollback set of the activation that turned `--sso` on", and that
"a set whose Console already ran with `--sso`" refuses. Each set holds the Console unit from before its
run. So the activation that turned `--sso` on is the newest one whose set holds a Console without
`--sso`: every activation after it ran with `--sso`, and so did every set it left.
`no_sso_base` walks the log's sets newest first and stops at the first set whose Console has no `--sso`.
It refuses `sso-off-flags` at the first set it can't read (missing, failing its SHA256SUMS, or with no
argv the script can parse), since it can't tell whether that set was the one. If every set ran with
`--sso`, it refuses with `the Console in <set>/<file> already ran with --sso, and the log names no
earlier activation`. A log with no set at all refuses with `the log names no activation with a rollback
set`.
The tradeoff: a later same-copy re-pin that carried `--sso` (a `--no-send`, or a re-pin onto a newer
copy) doesn't break `--no-sso`, which the phase 2 window needs if a re-pin lands between step 5 and a
rollback. The cost is that "a set that ran with `--sso` refuses" now applies only when no older set
without `--sso` exists. A tampered set on the walk still refuses, even one newer than the set the walk
would have used. Filbert, please rule on this reading.
## Filbert's early look: B1 and N1 (sage-2315)
Filbert accepted the walk. Sage ruled both findings into this row.
- B1: row 200's rollback to a copy whose Console has no `--sso` took the flags from the set and kept
the pointer's `--send`. From an `--sso --send` pointer, which this row's `--send` can now make, that
gave a Console with `--send` and no `--sso`, which F39 b forbids. `send_for_target` now drops `--send`
whenever the run drops the pointer's `--sso`, so it is still the one place that drops it. The plan
line reads `send: dropped; the run drops --sso, and --send needs it (F39)`, and the steps log gets
`drop --send; the run drops --sso`. Row 200's sso line now names only the tester as following the
pointer.
- Changed existing assertion, for the review to approve by name: T57 item 3 (sage-1389), "the
plain case", ends with `assert.deepEqual(pointer(g).console,[...noSend(gbase),...pair(f)])`. It
used to expect `'--send'` at the end. The case seeds a legacy `--send` pointer, turns `--sso` on
(which carries `--send`), then rolls back to B, whose Console has no `--sso`. A carried `--send`
that ends without `--sso` breaks F39 b whatever its source, so the old pin asserted the wrong
outcome (Sage accepted this in sage-2321). Row 200's two sso plan strings also lose "--send and"
(`ROLLBACK_PLAN` and the round 3 named-instance case), since `--send` no longer follows the pointer
on that path.
- N1: the walk skipped an ok line with no activation. Skipping it could land on an older set that
isn't the right one, so it now refuses `sso-off-flags` with `an ok line in the log names no
activation`. The walk reads the log through `mapfile`. That gives no entry for an empty log, so a
log with no set still gets its own message.
## test-release.sh
The window plan says test-release.sh "gets cases for both". The brief lists it only as a file to run
whole. test-release.sh is release.sh's Docker selftest and doesn't call stack-release.sh, so the cases
live in the node tests and test-release.sh runs unchanged.
## Tests (T58 in stack-release.test.mjs)
- T1: `--sso --send` from B onto C1 with a tester port: plan, pointer, unit and steps. `--send` beside a
carried `--sso` re-pins C1 once; a second `--send` refuses `already-active`; C carries it.
- T2: `--send` without `--sso` refuses on another copy and on the pointer's copy, also when the pointer
carries `--send` alone; `--plan` lists the refusal.
- T2 (sage-2315 B1): `--sso --send` onto C1, then a rollback to B, whose Console has no `--sso`. The
pointer and the unit end with neither flag, and the plan and steps name the drop.
- T2 and T4 usage: every forbidden pairing and verb, checked against the full usage text.
- T3: from an `--sso --send` pointer, `--no-sso` on the same copy gives back the pre-`--sso` ExecStart
line byte for byte, without `--send`. A second case walks past a `--no-send` re-pin that ran with
`--sso` and keeps the pointer's tester port (7334), which differs from the set's (7333). A third
(sage-2305 a) turns `--sso` on, then re-pins twice with `--send` and `--no-send`, so the two newest
sets both hold `--sso`. `--no-sso` then gives back the arrival's ExecStart line byte for byte.
- T4 (sage-2305 b): two sets, both edited or written to hold `--sso` with sums intact, refuse
`sso-off-flags` through `refused()`.
- T4: `sso-not-on`; an ok line with no activation; a set that fails its sums, ran with `--sso`, has no
argv or is missing; a log with no set. Each runs through `refused()`, so nothing stops and nothing else changes.
- T5: stack-release-external.test.mjs adds `--send` and `--no-sso` to the row 232 usage list.
- T55 test 5 and the T57 usage test match the new usage line.
## Mutants
| # | Change | Expected killer |
|---|---|---|
| M1 | `set_flags` skips the SHA256SUMS comparison when `--no-sso` is given | T4 tampered set |
| M2 | `no_sso_base` skips the `sso-not-on` refusal | T4 `sso-not-on` |
| M3 | the `--no-sso` branch of `send_for_target` keeps `--send` | T3 |
| M4 | `--send` skips the `send-needs-sso` refusal | T2 |
| M5 | parse_opts drops the `--send`/`--no-send` rule | T2 and T4 usage |
| M6 | a rollback to a Console without `--sso` keeps `--send` (B1) | T2 (sage-2315 B1) |
| M7 | the walk skips an ok line with no activation (N1) | T4 no-activation line |
Runs and results go in the round's notes once Sage clears the slot.
-8
View File
@@ -1,8 +0,0 @@
# Della
Builder seat, Codex gpt-6.1-sol, started by Mos for the email plan (Vikunja 460). Owns row 224 (C2-05b) under Q220.
No launcher in this directory. The seat runs as a T3 thread (thread 66735a7f) and works in its own worktree. It has no
credentials of its own; it pushes only its own `della/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Registered by Sage on 2026-10-22 so the queue can name the seat as
row owner under Q220 (mos-1301).
+100
View File
@@ -0,0 +1,100 @@
# Row 196 build evidence
Base 4d26003f7. Build authorized sage-1371, design approved duckworth-0009/0015.
No live units, live ports, credentials, real second uid or live state touched.
Single test files only so far. Suites and mutant batches require Sage's slot.
## Cleanup ruling, sage-1382 (2026-10-20T19:39:30Z)
The first scratch test proved Node/libuv unlinks a final-path replacement before
JavaScript close callbacks. Evidence: evidence/tester-cleanup-blocker.txt.
Duckworth's option 1 (523a6bddf) was then verified in a standalone scratch test:
bind temporary.sock, record dev/ino, rename to tester.sock, connect successfully,
replace final inode, close. Temporary name absent and replacement survived.
Sage chose option 1. The code binds a unique temporary name in the checked 0700
parent, chmods/verifies the socket, records its dev/ino and renames to tester.sock.
The temporary basename has the same length as tester.sock. libuv remembers the
removed temporary name. The close callback lstat/checks the final dev/ino before
unlink. The final-path replacement test was RED with direct bind and GREEN after
rename. It also asserts no temporary entry after bind or close. M24 will bind the
final path directly and must fail this test.
Residual: a hostile same-uid process can replace a path between lstat and unlink
(or between the final-path check and rename), or recreate the temporary name
before libuv closes it. The checked private parent excludes other uids, not the
instance's own uid. These same-uid races are outside this row's guarantees.
## Development checks
Evidence files retain the red/green and diagnostic output. The initial concurrent
peer test exposed that Node pauses a Socket passed through spawn stdio. The caller
now resumes it after helper exit, with bidirectional post-helper traffic asserted.
No product commits until applicable suites pass. Final candidate and counts follow
after implementation and authorized verification.
## Gate stop, 2026-10-20T19:57:01Z
Gate run 1 stopped at bus 463 pass/1 failure. The row164 reviewed byte pins needed
the intentional row196 human-walk/nonce and Console initial-spawn nonce changes.
Only those two pins were updated, retaining human-cli and CLI transport pins,
literal path recovery and channel plug-in import bans. Focused test passed.
Run 2: bus 464 pass/0 fail. WebUI stopped at two failures in tester-browser.test.mjs:
it still formed TCP URLs from the now-Unix listener address. Script counted 269
passing top-level cases before interruption and 2 failures. No final WebUI summary.
CLI and mutants did not run. Node24 peer 3/3 and principal 4/4 passed separately.
Sage received clear (gander-0023). The failures are fixture migration omissions,
not timing failures from Launchpad's overlapping single T56 run. No product commit.
Focused repairs: the browser fixture fulfills synthetic-origin requests through
CDP from the scratch Unix listener, with no TCP listener, proxy or product relay.
The two tester browser tests pass. Self-review also found a FIFO receipt could
block open before fstat; O_NONBLOCK now makes it refuse promptly (RED timeout,
then interpreter 5/5 GREEN). Missing/non-regular helper now refuses construction
before opening broker.sock (RED missing refusal, then peer reader 4/4 GREEN).
Renewed slot requested in gander-0024; no suite or mutant rerun yet.
## Manifest preparation while the slot is occupied
sage-1409 accepts the CDP fixture repair and startup refusals. MANIFEST.md gives
each its own item, including the old/new human.mjs and WebUI bus.mjs sha256 values
and rationale for the row164 re-pin. PRODUCT-FILES.json records current product,
test and operator-document hashes, explicitly as an uncommitted draft.
Self-review also found helper shutdown returned before child close. The focused
test was RED (evidence/peer-shutdown-red.txt), then GREEN after reader.close awaits
all killed helper close events and server.close awaits reader.close. M27 removes
the wait. M25 removes FIFO nonblocking open and M26 removes helper preflight.
All three are prepared but unexecuted, reported to Sage in gander-0025.
After this change, individual Node 24 files pass: peercred 5/5 and principal 4/4.
Node 26 peer-transport is 3/3. Their logs retain the full results. No suite or
mutant batch ran during the hold. sage-1416 puts Gander after Launchpad, Darkwing,
Rocko and row 192; Sage must send go before the renewed batch starts.
The consumer audit found one additional conversion defect in the files already
changed: proof insertion stopped at --tester-port but not --tester-socket.
Pointer argv therefore differed from rendered unit argv when conversion also
supplied a proof pair. The extended row196 release case reproduced the exact
order mismatch, then passed after adding the socket flag to that insertion
boundary. The proof-file no-read check still passes. M28 restores the defect.
No batch ran. Shell syntax and diff whitespace checks pass. Final prepared
mutant count is 28, pending Sage's go.
## Static review completion, sage-1425
The changed verifier path now has a real Client refusal test. A completed proof
refusal reaches the client as human-required; helper failure is peercred-required.
No broker request dispatches. peer-transport is 4/4. Client code is unchanged:
an abrupt close, timeout or transport error without a response is outcome-unknown.
The scratch base/candidate comparison returns human-required for both. It does
not reproduce Quackerjack's exact failing instance; the manifest names this limit.
Review also found release did not check tester path length until after service
stop. The focused long-path activation fixture reproduced it, then passed with
the new preflight. Existing unsafe parent type/uid/mode checks also precede stop;
the fixture verifies no start/stop calls for unsafe mode. M29 removes the check.
Legacy tester-port-invalid wording is restored to avoid an unrelated contract
change. Review covered all changed trust paths and consumers; no further code
changes are planned before the batch. M1-M29 remain prepared and unexecuted.
+332
View File
@@ -0,0 +1,332 @@
# Row 196: the human boundary across uids and humans
Gander, 2026-10-20. Revision 3, recording approved r2 and N5. Gitea #1694, P19 333.
Base: `96f243faf`, branch `gander/row196`. Design approval latest-by 2026-10-22, landing
latest-by 2026-10-24 and before federation R3. This document authorizes no live action.
## 1. Outcome and authority
Other Linux users must not reach the tester's data or writes. A bus human proof must describe
the process that actually connected. Console user calls must also pass the human walk and
come directly from this bus tree's Console. Row 193 and federation R3 use that one verifier.
This is not isolation from a hostile process on the instance's own uid. Keeping the human
terminal CLI means that an unmarked process on that uid still has the owner's authority.
Process ancestry cannot prove that a process was never an agent. Sections 4 and 8 state this
limit without treating it as an enforcement guarantee.
Inputs: COMMON at `53bb59462`, the row brief at `96f243faf`, Q175 sections 1, 6 and 8,
federation r2 at `60b9b112a` section 3.6, Duckworth's B1-B5 at `b402197ff`, row 190's
DESIGN.md and the row 190b brief. Q175-Q178 and the MVP plan set the order. Row 190b is on
hold pending the host decision. This row does not assume that a separate uid is already live.
Sage's `sage-1343` accepts the design direction and requires Duckworth to rule on the peer
credential implementation. `sage-1345` keeps the control board inventory-only. Morgana's
`morgana-0003` supplies the principal contract in section 4. Sage registers this session.
Mos accepted removal of direct tester browser access in mos-1015, relayed by sage-1363.
Agents test through the private socket. Any later browser view is an authenticated relay
behind SSO, designed and reviewed in its own row. This row adds no relay.
## 2. Inventory from code, not a live probe
"Same uid" includes untrusted local processes. "Other uid" excludes root, which is outside
this boundary. SSO below means Q175 is active on that instance, not merely installed.
| Entry | Same uid today | Other uid today | Effect of SSO and this row |
|---|---|---|---|
| Main Console, default 7332, bare serve default 7330 | Loopback reads. With preview/send, matching Host, header and origin can write as owner | Same TCP reach and header forgery | Q175 SSO verifies login and refuses non-public Host except GET /healthz. This row does not change public ingress |
| Tester TCP, default installation 7333, optional per instance | Reads and the tester instance, route, name and chat writes | Identical reach, no uid check | Q175 deliberately leaves tester separate. This row replaces TCP with a private filesystem Unix socket |
| `<dataRoot>/bus/broker.sock` | Connects, then needs cap or proof. Claimed human PID is not the socket peer | Normally refused by bus/ 0700 and socket 0600 | SSO changes nothing. Add peer uid and PID binding, retain file checks |
| Human wrapper to tree's human-cli.mjs | Owner if CLI path, nonce and ancestry pass. Agent ancestors refuse | Cannot connect to protected socket, and proof uid refuses | Preserve terminal owner path, add peer binding. SSO does not authenticate terminal users |
| Tester CLI to broker.sock | Tester if its proof passes and bus.tester is exactly true. T3 agents are allowed, managed runs are not | Filesystem modes normally refuse | Same tester policy, plus peer binding. No owner/user upgrade |
| `<dataRoot>/bus-host/launch.sock` | Requires run cap and launcher checks | bus-host/ 0700, socket 0600 | No human proof endpoint. Unchanged |
| `<dataRoot>/bus-host/git.sock` | Git capability protocol and broker authorization | bus-host/ 0700, socket 0600 | No human proof endpoint. Unchanged |
| Discord connector | Outbound Discord gateway/REST, no inbound local listener. Local process could tamper with same-uid files/process | No local port to call. File separation depends on uid and modes | Existing guild, user, channel, bot and webhook checks remain. Discord is not a new bus human transport |
| Row 190 additions | Separate configs, roots, unit names, wrappers and Console ports, on one uid | Console/tester TCP still reachable | Adds no new class of listener. Row 196 derives its socket per dataRoot. Row 190 alone supplies no hostile-process isolation |
| Control board, default 7331 | Unauthenticated transcript/board reads, POST /api/reply and /api/seen after request checks | Same loopback reach. A local client can supply matching request headers | Independent development service, outside instance SSO. Inventory only per sage-1345. Its fix needs another row |
Evidence: `packages/webui/src/{cli,serve,bus}.mjs`, `packages/bus/src/{server,runtime,human,
tester,store}.mjs`, `packages/cli/src/{launcher,git-socket,transport}.mjs`,
`packages/discord/src/{gateway,authorize}.mjs`, `packages/control-board/src/serve.mjs`.
Store checks bus/ with `safe(directory,true)`, including owner, symlink and mode checks.
The dataRoot check itself does not require 0700 today. Do not describe that as a checked mode.
## 3. D1: private tester HTTP socket
Add `serve --tester-socket`, a boolean selecting `<dataRoot>/console/tester.sock`. No arbitrary
path or new system config field. Create console/ as 0700. Refuse a symlink, wrong owner,
non-directory or any group/other bits on an existing console/. Refuse a pre-existing socket
or other entry rather than deleting it blindly. A stale socket needs an operator's diagnosis.
Refuse a path longer than Linux's 107-byte pathname capacity before binding.
Bind HTTP on the Unix path, set socket mode 0600, then verify type, owner and mode before
declaring ready. The private parent covers the bind-to-chmod interval. Cleanup only the owned
device/inode, as broker.sock does. Never open a TCP compatibility bridge or an abstract socket.
Readiness failure closes the main listener too, rather than leaving a partially started Console.
The tester handler keeps its fixed principal, header/cookie refusals and exact write list.
Use a fixed logical origin `http://mosaic-tester.invalid` for URL and same-origin checks. It
does not resolve that name. Clients send that Host and Origin plus the existing Fetch Metadata
when required. A Host string is not authentication. Unix socket access supplies that boundary.
The main listener still refuses x-mosaic-tester. No tester /auth route exists.
`bus.tester` remains Jason's switch. Socket reach never enables tester authority by itself.
Turning it off must refuse every tester bus call. This row adds no user or owner capability
to the tester. Tester reads retain today's scope, including any local Console reads that are
not bus calls. This row does not turn bus.tester into a separate HTTP read switch.
### Consumers and migration paths
| Consumer of 7333 | New path |
|---|---|
| Agent tester HTTP tools and curl probes in authorized window packets | `curl --unix-socket <dataRoot>/console/tester.sock http://mosaic-tester.invalid/...`, with the fixed Origin for writes. Add one repository helper for bounded tester HTTP requests so callers do not copy path/origin logic |
| `scripts/mosaic ... --tester` | Already uses TESTER_CLI and broker.sock, not TCP. Keep its verbs, output and switch |
| Browser tester page in row 176 and later window checks | Ordinary browsers cannot navigate a Unix HTTP socket. Replace automated tester HTTP checks with the socket helper. Browser visual QA uses scratch fixtures only. No local TCP relay is added |
| `webui-service.sh` render/install and installed unit | Carry --tester-socket for new copies. Refuse a target without support. Units running old copies retain their old flags until a window |
| `stack-release.sh` activate, plan, rehearsal and rollback | Add --tester-socket. Replace inherited --tester-port N only when explicitly requested. Refuse incompatible inherited flags before stopping units. Plan names the transport and every dropped pair. Rehearsal uses a scratch socket |
| Gate/window Console checks, including agents' console-check scripts | Extend checks to use scratch Unix HTTP and expect no tester TCP bind. Sage owns live packet updates, including Q175 phase 2's old --tester-port 7333 instructions |
| `docs/TOOLS.md`, webui/cli/bus READMEs and examples | Document socket invocation, fixed origin, loss of direct browser navigation and per-instance path. Historical window receipts stay unchanged |
Repository search found no non-test hardcoded 7333 caller in tools/ or the harness code. The
active windows and external seat commands still need Sage's consumer audit before re-pin.
The build manifest will record the full search, rather than treating this draft as that audit.
## 4. D2: one verified principal per bus request
Internal interface: `verifyPrincipal(request, connection) -> {kind,business,id}` or refusal.
Kinds are owner, user and tester. The connection metadata is supplied by the trusted server,
never deserialized from JSON. No role or authority is returned. The broker looks up live policy.
1. Read and validate the bounded envelope first. For human, tester and user proofs, read Linux
SO_PEERCRED on that accepted Unix connection before dispatch. Require a valid peer PID
and the bus uid. Refuse unavailable credentials, unsupported platform and errors. Cap
traffic keeps its capability protocol behind the validated 0700 directory and 0600 socket.
2. Require exactly one cap, human, tester or future user envelope. Human/tester/user proof.pid
must equal the kernel peer PID. Verify startTime, nonce and CLI path against that process.
Keep the second process read after the walk. Missing, changed or exited processes refuse.
3. Owner terminal: run the full existing human walk. Map the business to businesses[b].human.
No supplied human id, role or uid selects the principal. Keep today's terminal invocation.
4. Tester: run verifyTester, retaining its deliberate difference from the human walk. An agent
seat may be tester. A managed-run marker or registered managed ancestor refuses. Then check
bus.tester. The full human walk must never be weakened to admit a session user.
5. Session user: use the full human walk with the path-bound user-cli entry and its own nonce
variable, MOSAIC_BUS_USER_NONCE. Parameterize the nonce reader without admitting a human
or tester nonce as a user proof. Require its immediate parent to be the same tree's resolved webui/src/cli.mjs,
argv[2] `serve`. Check parent uid, startTime and ppid again before binding. Both child and
parent remain alive for the call. A different checkout or wrapper parent refuses.
6. For user calls, the Console puts its mapped access-file user id in the verified proof's
user field, as federation 3.6 specifies. The id comes only from claims Q175 verified for
this request's live session, never from an identity cached past that session. It never
takes that id from a form field or verb
args. Return kind user, proof.business and proof.user as id only after the provenance
checks. R3 then validates that mapping and the live grant. Bad access.json denies users,
not the terminal owner. The verifier does not implement access-file policy.
7. Owner Console calls keep the verified Q175 owner subject path. Under SSO only a subject
in console.sso.subjects reaches owner bus calls. They use human-cli and the same peer/walk
checks. A terminal owner still needs no SSO token. No anonymous main-listener fallback.
The bus verifies which process supplied the session identity. It relies on that Console's
Q175 implementation to have verified OIDC. It does not independently verify an ID token in
this option. A provider/sub field alone, from another process, is not enough.
The Console must spawn user-cli directly with its nonce already in the launch environment.
It must not use human-cli's existing self-respawn pattern, which makes the CLI's parent a CLI.
Apply the same initial-nonce spawn to Console human calls for consistent tests. Terminal
human-cli can retain its self-respawn since no Console-parent condition applies there.
Never strip agent markers from inherited environments to make a walk pass.
The server binds an internal cap for this request, dispatches, and disconnects in every
success/error/timeout path, including asynchronous task and web calls. It never returns the
cap or a reusable principal handle. A proof copied by another process has a different kernel
peer PID and fails. One verified CLI may make only its one request on its one connection.
The session may hold s.user for that request. No PID or principal cache survives the connection.
### Fit with rows 193 and R3
Row 196 owns peer metadata, proof/walk verification and the internal principal interface.
It supplies the Console provenance verifier and tests before R3 adds user-cli and user sessions.
R3 owns access.json mapping and the bindUser integration, using this interface. It must not
add a second ancestry implementation. Row 193 adds viewer and share policy to the same live
grant check. An owner id is the business human. A user id is the access-file id, with stored
owner string `user:<id>`. Neither row takes role from the proof.
No live grant, unknown subject, duplicate owner/user mapping and invalid access file all
deny user binding through R3's policy. Per-request authorization, access.reload and the
30-second recheck remain R3's responsibility. Share visibility uses that check in row 193.
Binding provenance is not a grant and never widens one. R3's user dispatch must run before
names falls through to #agent. R3 must not land before this contract and its tests are green.
Gizmoduck's gizmoduck-0015 and Gander's gander-0007 agree on this split. R3 keeps the reserved
user-name mutant. Row 196 tests kind separation even if a user id resembles the owner name.
### OIDC alternative
The bus could verify the signed ID token itself and map its issuer/sub. That reduces reliance
on the Console's subject assertion, but requires JWKS/provider handling, nonce or challenge
binding, replay handling, login and logout/session lifetimes in the bus. Reusing an ID token
on every request either creates a replay credential or expires a Console session with the
token. A bus-issued session needs a separate lifecycle design. It also leaves the terminal
owner and hostile same-uid limit intact. Recommend the Console provenance contract for this
slice. Duckworth approved this option in duckworth-0006, with the live-session condition above
and the requirement to carry section 8 R2 into P19 333 word for word.
## 5. D3: obtaining kernel credentials, O1 approved with conditions
All options must bind the accepted connection, not a PID a caller picked. None gets a silent
fallback to the current proof. Sage required this comparison. Duckworth selected O1 in
duckworth-0006, with the interpreter, proof-only and image conditions below. O4 remains the
fallback if Python is unavailable, and needs a revised build manifest before use.
| Option | What it proves and costs | Recommendation |
|---|---|---|
| O1 Python stdlib helper on inherited accepted socket | Node passes the accepted net.Socket as fd 3 using documented spawn stdio stream passing. Python socket(fileno=3) reads SO_PEERCRED and returns only pid,uid,gid on stdout. No socket reads, no shell, no credential values. Adds Python to bus hosts and any future bus image, with a process per admitted proof connection. Copies carry the helper and depend on the system interpreter | Selected by Duckworth, with conditions below |
| O2 /proc/net/unix plus inode and /proc/PID/fd scans | The table identifies Unix socket entries, not an authenticated peer PID for each accepted connection. Finding a process holding an inode is racy, may have several holders and can be unreadable. The server endpoint's inode is not the client's inode | Reject as a replacement for SO_PEERCRED. No claimed proof PID may choose the scan target |
| O3 plain Node fd passing or abstract sockets | A private inherited IPC channel can authenticate a child that this parent spawned. The bus did not spawn the independently started Console or terminal CLI. Moving the bus into the Console's child tree changes lifecycle and ownership. An abstract name supplies no filesystem mode boundary and no peer uid check by itself. Node's public net API supplies no SO_PEERCRED accessor | Reject as a drop-in. A larger bus-spawned transport design could use fd capabilities, but is outside this slice |
| O4 small compiled helper or Node-API addon | Calls getsockopt directly, with no Python runtime. A helper can use the same inherited fd. Adds compiler/build closure and platform artifacts to stack-copy and containers. An addon also needs a supported way to obtain the accepted socket fd and Node version coverage | Viable alternative if Python is refused. Prefer a small helper over undocumented Node handles |
For O1, pin the system interpreter `/usr/bin/python3`, resolving its system symlink to an
absolute ELF binary. Require root ownership and no group/other write on that executable and
its path components. Refuse scripts, PATH shims and any resolved path under an OS account's
home, including /home and /root. A pyenv shim can choose an interpreter before -I -S applies.
No PATH lookup, caller-selected interpreter or helper. Invoke the resolved binary with
`-I -S`, a minimal environment and the absolute helper path in the same pinned copy.
Activation preflight records the interpreter's resolved path, version and file identity/hash
in its gate evidence. The bus validates and pins the resolved executable at each startup
before opening broker.sock. The activation gate must report the same interpreter used by
that bus. Missing, unsafe or changed resolution during that activation refuses it, never
falling back to PATH. No secret values are recorded. Test system-owned interpreter checks
through injected metadata.
N5 decision: a later bus restart repeats the safety/version/support checks and accepts a
safe system interpreter installed by an OS upgrade. It does not compare against an old
activation hash. Before opening the listener, append a startup receipt with timestamp,
copy path, resolved interpreter path, version and file identity/hash to
`<dataRoot>/bus/peercred-starts.jsonl` (0600 in the checked 0700 bus directory). Refuse an
unsafe receipt file or failed append. Never edit the old activation evidence. The startup
receipt identifies the binary now in use, so the old receipt remains a historical fact.
If an upgraded interpreter fails the checks, startup fails closed until a safe supported
system interpreter is restored by the operator. There is no shim or O4 auto-fallback.
Operator re-pin after an interpreter change: in the next authorized window, prepare the
next reviewed copy with `scripts/stack-copy.sh copy NEW`, then run
`scripts/stack-release.sh activate NEW BUSINESS --tester-socket` with that instance's existing
flags and selector, first with --plan. NEW must differ from the active copy because activate
refuses already-active. Record the interpreter in the new activation evidence and compare it
with the bus startup receipt. A routine OS update alone does not authorize a re-pin or unit
restart. A bus restart does not rewrite config, pointer or activation history.
Preflight helper and SO_PEERCRED support too. The helper lives under packages/bus/src so
stack-copy's closure includes it. Copies remain immutable. Document Python >=3.10 on bus
hosts. Today's Containerfile builds the worker image and runs run-agent.sh, not the bus.
It needs no Python change for this row. A future bus-host image must explicitly include
the pinned system Python or use reviewed O4. No install through the node_modules symlink.
Pause each proof connection while reading credentials. Limit outstanding helpers
to 16, timeout to one second, output to one strict JSON record of at most 256 bytes. Refuse
nonzero exit, extra output, malformed integers, missing fd and timeout. Validate pid, uid and
gid as safe integers, pid >=2 and uid/gid >=0. Only pid and uid select the principal. gid is
validated but grants no authority. Kill and reap helpers on shutdown. Cap envelopes never
spawn this helper. Reading the proof envelope first is sound because SO_PEERCRED describes
the peer at connect time. Keep the accepted Socket object owned by its connection closure,
not a shared current-fd variable. Only the helper's duplicate fd is closed when it exits.
References: [Node stdio stream passing](https://nodejs.org/api/child_process.html#optionsstdio),
[Node Unix socket support](https://nodejs.org/api/net.html#ipc-support),
[Python socket and getsockopt](https://docs.python.org/3/library/socket.html),
[Linux Unix credentials and permissions](https://man7.org/linux/man-pages/man7/unix.7.html),
[Linux proc network table fields](https://man7.org/linux/man-pages/man5/proc_pid_net.5.html).
These support the APIs. The connection-binding behavior still needs T2 on supported Node versions.
## 6. Migration and rollback
No config schema or store schema changes. Old copies and units keep working until each
instance re-pins. No new verifier is loaded into a running old broker. Do not pair a new
Console with an old bus and call it protected. Same-tree paths intentionally refuse mixtures.
The authorized window must pin bus, Console and human wrapper to one candidate. New tools
offer `activate ID BUSINESS --tester-socket`. Plan must show the old tester-port pair removed
and the new socket flag added, and tell Jason that direct tester browser navigation is lost.
Mos accepted that change in mos-1015 (sage-1363). Agents test through the socket. A later
authenticated browser relay behind SSO belongs to its own row. Without the explicit
conversion, a candidate that no longer
serves tester TCP refuses inherited --tester-port before stopping anything. New serve refuses
--tester-port with a migration error. It never silently changes that flag's meaning.
Rollback to a copy lacking --tester-socket drops tester enablement and reports it, as today's
tools drop unsupported flags. It does not turn the socket back into TCP. The old main Console
and owner CLI can run with their known old limits. A failed activation's recovery may restore
the old saved unit, including its TCP listener. Report that recovery as restoration of the old
security state, never as a hardened deployment. After rollback, R3 and second-human access are
not accepted until prerequisites are restored. Older operator tools cannot manage the new
flag safely. The window must use the reviewed candidate tool blob for rollback as well.
The Q173 window supplies real uid-denial evidence and the final consumer audit. No second uid,
live socket, unit, config or secret is touched in this row's tests. Row 190b or a separate host
still owns cross-instance file isolation. It is not replaced by SSO or this socket change.
## 7. Acceptance tests and mutants
All fixtures use scratch roots and ephemeral sockets, no live 7332/7333, public origin or
provider. No real second uid. Foreign uid checks use injected metadata and name that limit.
Use short TMPDIR paths so the longest Unix socket path stays under 108 bytes.
| Test | Required evidence | Mutant killed |
|---|---|---|
| T1 private tester | HTTP over scratch socket reads and writes as tester only. parent 0700, socket 0600. No tester TCP listen. Wrong Host/origin and auth cookies refuse | M1 bind loopback TCP as well, M2 remove socket mode check, M3 accept owner cookie |
| T2 real peer binding | At least 8 concurrent proof connections from distinct scratch client processes, each with its own live proof, plus a copier racing one live CLI. Barrier the helpers so connections overlap. Each genuine call must bind its own peer PID and pass, the copier must refuse. Repeat on Node 24 and 26. A lower-level helper fixture exchanges bytes both ways on the accepted socket after the helper exits, and each real bus call receives its response after helper exit. Helper failure cases all refuse | M4 use proof.pid as peer PID, M5 continue after helper error, M20 give the helper another pending connection's socket |
| T3 uid/path refusal | Inject foreign peer uid, wrong file owner/modes, symlink parent, existing socket and long path. Inject interpreter script, user-owned ELF, home path, writable ancestor and missing executable. Refuse before dispatch, no repaired paths. Unsafe pid/uid/gid and extra helper output refuse | M6 ignore uid, M7 accept unsafe parent |
| T4 ancestry | Agent env, binary and registered launch above owner/user refuse. Missing/reused PID, changed parent and cycles refuse. Clean terminal owner works | M8 omit walk, M9 omit identity re-read |
| T5 Console parent | Direct CLI child from exact tree Console fixture works. Same CLI under shell, other copy, wrapper, agent ancestor or CLI respawn refuses | M10 remove direct-parent check, M11 accept any Console basename |
| T6 principal shape | No human id or role from request body. Exactly one proof. No returned cap. Disconnect on synchronous and async failure, timeout and close. Tester agent allowed, managed tester refused | M12 accept extra identity, M13 omit disconnect, M14 use tester walk for user |
| T7 consumer migration | Render/plan/activate/rehearse fixture carries socket flag in unit and pointer. Explicit conversion removes port. Unsupported/inherited mismatch refuses before unit calls. Rollback disables unsupported tester, never opens TCP | M15 keep inherited tester port, M16 restore TCP on downgrade |
| T8 gates and identity | bus.tester false refuses tester calls. SSO owner/user wiring uses only claims verified for that request's live session. Expired/logged-out or replaced session cannot reuse an earlier mapped id. User kind never becomes owner from its id. R3 integration maps subject, denies missing/invalid grants and owner overlap, broken access file leaves terminal owner working | M17 bypass tester switch, M18 turn user kind into owner by id |
| T9 isolation limits | Two scratch instances have different sockets and matching tree paths. Foreign-uid stand-in refuses. Record that no real uid boundary was exercised | M19 choose default instance socket |
| T10 interpreter and cap path | Activation evidence records the same pinned root-owned system ELF that the bus uses. Scripts, home paths and unsafe owners/modes refuse. Simulated OS replacement followed by startup accepts a safe replacement and appends its actual identity without changing the old receipt, refuses an unsafe replacement or unsafe/unwritable receipt. Cap calls complete without invoking the helper, proven with a helper spy that fails if called | M21 bypass interpreter safety, M22 invoke helper for cap traffic, M23 retain stale interpreter identity after restart |
Tests for new policy stay in the bus, WebUI and CLI packages. R3 provides the real user/grant
half of T8 in its candidate, with its own gate. This row tests the shared verifier's user
provenance without shipping placeholder user access. Mutants run on scratch copies,
restore the original bytes and verify hashes. Two failures in a gate run stop and are reported.
Single test files run freely. Ask Sage for the slot before any suite. Final acceptance is
Duckworth's approved manifest plus Sage's bus, WebUI, CLI and scripts/test-*.sh gate.
## 8. Closure, limits and review decisions
R1. Same uid remains trusted OS authority. A hostile process may detach, clear its markers,
modify argv, read same-uid pipes or capabilities, edit policy files, or interfere with the
Console where host policy permits. SO_PEERCRED proves the connecting process identity at
connection time. It does not prove human intent, protect memory, or track a socket passed to
another process. Ordinary agent children fail the walk. Hostile detached agents are not
claimed contained. Separate uids narrow cross-instance reach, not reach within one instance.
R2. The Console provenance option trusts verified code and process metadata, not an independent
bus-side OIDC exchange. That trade must be approved, not hidden behind the phrase zero trust.
R3. A Unix tester endpoint removes direct browser navigation. No bridge is in this design.
The 7331 development board remains a separately reported reachability and impersonation risk.
P19 333 can record the following after the implementation gate and authorized re-pin:
"Tester ingress is private to the instance uid. Bus human proofs are tied to the actual
Unix peer and retain the human walk. Session-user calls require the same-tree Console as
direct parent. Q175 phase 2 separately protects main Console ingress with verified OIDC.
Hostile processes on the instance uid, root, and the independent development control board
remain outside these guarantees. Cross-instance file isolation requires the uid/host gate."
Duckworth requires that P19 333 also carry R2 word for word:
"The Console provenance option trusts verified code and process metadata, not an independent
bus-side OIDC exchange. That trade must be approved, not hidden behind the phrase zero trust."
Do not mark the historical task fully closed on a design or commit. Closure needs live
Q175 phase 2, this row's re-pin and the recorded residual accepted through Mos. A promise of
hostile same-uid isolation would require separate OS authority for agents and a new design.
Review receipt: duckworth-0006, notes at `86be181a2:agents/duckworth/work/row196/REVIEW.md`.
Q1 selects O1 with the conditions incorporated in section 5. Q2 approves Console provenance
with the live-session and unchanged P19 R2 conditions. B1 adds concurrent T2, M20 and socket
survival checks. N2's detached-Console limit remains in R1. N3's gid validation and N4's
operator window warning are included. Mos's tester-access ruling is recorded above. No
product code begins before design approval.
Duckworth approved revision 2 in duckworth-0009, review at
`cb6d840df:agents/duckworth/work/row196/REVIEW-r2.md`. N5 permits either restart policy.
Revision 3 selects safety revalidation with a new append-only startup receipt, and gives the
operator a re-pin path that respects the existing already-active refusal.
+121
View File
@@ -0,0 +1,121 @@
# Row 196 review manifest
Status: draft, not an acceptance claim. Candidate product files are uncommitted.
Base: 4d26003f731a6eec439df6a48cacc3ef482a59c8. Design r3 approved by
duckworth-0009/0015 and sage-1371. No merge, deployment or live re-pin requested.
Current path hashes are in PRODUCT-FILES.json, marked as an uncommitted draft.
Final candidate commit, exact changed-file hashes and gate totals follow after
the renewed suite slot. Sage owns that slot and the final gate. Duckworth reviews
this manifest before Sage's gate.
## Review items
| Item | Change and acceptance evidence | Mutants |
|---|---|---|
| Kernel peer proof | `bus/src/peercred.py` obtains SO_PEERCRED from inherited fd 3. `peercred.mjs` owns the accepted Socket in each connection closure, bounds helpers and validates all three safe integers. `peercred.test.mjs` overlaps eight distinct callers plus a copier and checks traffic after helper exit. | M4, M5, M20 |
| Interpreter trust | `bus/src/interpreter.mjs` resolves fixed system Python, checks root ownership, safe ancestors, ELF and version, and refuses home paths or shims. Startup appends actual identity; activation compares the fresh receipt. `interpreter.test.mjs` covers safe replacement and unchanged older receipt. | M21, M23 |
| Startup FIFO refusal | Receipt opens include O_NONBLOCK before fstat, avoiding an indefinite FIFO open. `interpreter.test.mjs`, `non-regular startup receipt refuses promptly without waiting on a FIFO peer`, was RED by timeout, then GREEN. Evidence: `evidence/interpreter-fifo-red.txt`, `evidence/interpreter-green.txt`. No live receipt was used. | M25 removes O_NONBLOCK |
| Missing helper refusal | Reader construction refuses missing, symlink or non-regular peer helper before opening the broker socket. `peercred.test.mjs`, `missing or non-regular peer helper refuses at construction before opening the bus`, was RED, then GREEN. Evidence: `evidence/peer-helper-red.txt`, `evidence/peercred-green.txt`. | M26 removes helper preflight |
| Helper shutdown | Reader close kills and awaits helper close events; server close awaits it. `peercred.test.mjs`, `shutdown waits for killed helper close before returning`, was RED, then GREEN. Evidence: `evidence/peer-shutdown-red.txt`, `evidence/peercred-green.txt`. | M27 returns without waiting |
| Shared principal | `bus/src/principal.mjs` checks kernel uid/PID, retains the human walk, separates nonce kinds and requires the exact same-tree Console serve direct parent for users. `principal.test.mjs` supplies process-table refusals. User id is provenance only; R3 owns mapping, grants and user dispatch. | M6, M8-M12, M14, M18 |
| Request lifetime | `bus/src/server.mjs` runs the helper only for proof envelopes and releases transient caps for sync/async success, refusal, timeout and close. `peer-transport.test.mjs` tests cleanup and fails if a cap request invokes the helper. | M13, M22 |
| Private tester ingress | `webui/src/tester-socket.mjs` owns 0700 console directory and 0600 socket; `serve.mjs` preserves tester policy and refuses sign-in headers/cookies. `tester-socket.test.mjs` covers unsafe paths, origin/Host, instance separation and HTTP access. | M1-M3, M7, M19 |
| Owned inode cleanup | sage-1382 selects temporary bind, rename to final, conditional final dev/ino cleanup. `tester-socket.test.mjs` asserts temporary name absent after bind/close and replacement final inode survives close. Scratch proof and remaining same-uid race are in BUILD.md. | M24 binds final directly |
| Tester policy | `bus.tester` remains Jason's switch. Existing `tester.test.mjs` verifies refusals. No user widening, new browser relay or control-board change. | M17 |
| Consumer migration | WebUI CLI, service rendering, release plan/activation/rehearsal and CLI Unix HTTP helper carry explicit --tester-socket. Inherited TCP on a new target refuses before stop without explicit conversion. Unsupported rollback disables tester and reports it. Focused service/release/tester HTTP files cover these paths. | M15, M16 |
| Proof pair ordering | Conversion plus an explicit proof pair keeps pointer and unit argv in the same order: preview, proof pair, tester socket. The focused row196 release test was RED with the old insertion scan, then GREEN after including --tester-socket in its stop list. The proof-file no-read check also passes. Evidence: `evidence/release-proof-order-red.txt` and `evidence/release-proof-order-green.txt`. | M28 omits socket from the insertion boundary |
| Path preflight before stop | Activation checks the final Unix pathname byte length and any existing parent directory's type, owner and mode before stopping units. The row196 release case reproduces a too-long path reaching service stops, then verifies length and unsafe-parent refusals leave start/stop calls unchanged. Evidence: `evidence/release-path-red.txt` and `evidence/release-path-green.txt`. | M29 removes the preflight |
| Browser fixture migration | Test-only CDP interception fulfills synthetic-origin requests from the scratch Unix listener. No TCP listener or product relay. `tester-browser.test.mjs` is 2/2 after the fixture repair, accepted in sage-1409. Evidence: `evidence/tester-browser-focused.txt`. | Product transport removal is covered by M1; browser fixture itself is tested by its two cases. |
All package paths above are under `packages/`. Mutants are prepared in
`mutants.mjs` but none have run as of this draft. Their kill and restoration
receipts must be attached before acceptance. M25-M29 supplement design M1-M24.
## Separate row164 proof baseline update
Duckworth should review this hunk separately, as requested in sage-1409.
Only two baselines in `packages/bus/tests/channel-proof.test.mjs` change.
| File | Previous sha256 | Candidate sha256 | Why |
|---|---|---|---|
| `packages/bus/src/human.mjs` | `b995fc5cff3af8ade4c990d0e3bebf9d83829e00f034ebc9c3dcfd05f252b573` | `0c6d96abe4607cba0b02b0638a2fc217ff8135bac9a47761a0ef015fbe9d3079` | Read the distinct user nonce and parameterize the existing path-bound human walk for the shared verifier. |
| `packages/webui/src/bus.mjs` | `76a1b57858c4aa41071746713a196299264851c18322a6f9869d553f0362d421` | `934ab2f9b6bde72aafe7814758550c283eb25a4f3a23b284021b13718d67178f` | Supply the initial owner CLI nonce at spawn and map peercred-required to HTTP refusal. |
The human-cli and CLI transport hashes stay fixed. Literal shim path recovery,
forged-proof refusal and the channel plug-in proof-caller import ban remain tested.
Focused evidence: `evidence/channel-proof.txt`. These hashes must be checked again
against the final candidate before review.
## Consumer inventory and window
Tester tools use `packages/cli/src/tester-http.mjs` and the selected instance's
console/tester.sock. WebUI tester test fixtures and browser fixtures use scratch
Unix sockets. The stack-release gate and rehearsal carry --tester-socket and
compare the bus startup interpreter receipt. Package READMEs and docs/TOOLS.md
show the Unix path and explicit migration. Direct browser access is lost by
Mos's accepted ruling mos-1015; a future SSO relay needs its own row. Name that
loss to Jason in the authorized window. No candidate is live from this work.
The 7331 development control board is inventory only. Its reads, /api/reply
writes and reachability remain the separate boundary described in DESIGN.md.
Row 190b remains on hold; this work does not isolate hostile same-uid processes.
## Gate history and remaining evidence
Run 1: bus 463 pass, one byte-baseline failure. Run 2: bus 464 pass; WebUI
stopped at its second fixture migration failure, with 269 observed top-level
passes and no final summary. CLI and mutants did not start. Sage received clear
in gander-0023. No timing failures were attributed to Launchpad's T56 overlap.
The two browser failures were repaired and verified in a single file.
sage-1416 delays the renewed slot until Launchpad, Darkwing, Rocko and row 192
finish. No suite or mutant batch starts before Sage's go. Remaining: final
bus/WebUI/CLI suites, M1-M29 receipts, final changed-file hashes,
verified candidate commit/push, Duckworth
manifest review and Sage's gate. No approval is implied by this draft.
Node 24 focused reruns after the shutdown change are complete: peercred 5/5 and
principal 4/4. Node 26 peer-transport now passes 4/4, including client-visible
refusal codes. Full logs are in evidence/.
## Refused caller behavior and static review
The verifier path is changed by this row. A completed proof refusal returns an
explicit protocol error before closing the connection: `human-required` for a
rejected human proof and `peercred-required` for helper failure. The real Client
receives those codes in `peer-transport.test.mjs`, without broker dispatch. No
client code is changed. Abrupt close, transport error or timeout without a valid
response still reports `outcome-unknown`; this row does not convert that case.
Quackerjack's reported failed-proof close was not reproduced against this branch's
base. A scratch comparison loads HEAD's server and the candidate with the same
refusing authenticator and unchanged Client. Both return `human-required`.
Evidence: `evidence/refusal-baseline-comparison.txt`. This comparison does not
claim to reproduce Quackerjack's exact process ancestry or instance. The manifest
therefore claims the explicit codes only for the paths verified here.
Static review covered peer reader and shutdown, runtime startup and failure
cleanup, principal/walk dispatch, tester bind/close, Console spawn, Unix client,
release conversion/recovery/preflight, unit rendering and test fixture changes.
It found the shutdown, proof ordering and preflight defects recorded above.
Each has a focused test and prepared mutant. An unrelated change to legacy
tester-port-invalid text was also removed to preserve the existing contract.
No further implementation changes are planned before the authorized batch.
## Limits carried to acceptance
No real second uid or live instance is exercised. Foreign uid metadata is a
stand-in. The authorized window still owns real uid-denial and consumer audit.
Python is a new bus-host dependency; the worker Containerfile is unchanged.
Restarts validate the current interpreter and append a receipt; old receipts
are preserved. Interpreter changes use the next reviewed copy and preserve
the already-active refusal.
Hostile same-uid processes can read or alter same-uid state, detach or interfere
with pathname checks and Console provenance. Separate uids narrow cross-instance
reach, not reach within one instance. R3 must source the proof id only from
claims verified for that request's live session and reread grants per request.
P19 333 must retain this text unchanged:
The Console provenance option trusts verified code and process metadata, not an independent
bus-side OIDC exchange. That trade must be approved, not hidden behind the phrase zero trust.
+121
View File
@@ -0,0 +1,121 @@
# Row 196 Human Boundary Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** Bind human proofs to their actual Unix peers and make tester HTTP private to the instance uid.
**Architecture:** The bus reads kernel credentials through a pinned system Python helper only for proof envelopes. One principal verifier keeps the human walk and supplies Console provenance for federation R3. Tester HTTP moves to a private filesystem socket. Release tools migrate the flags explicitly.
**Tech Stack:** Linux, Node 24 and 26, Python >=3.10 standard library, existing Node test runner and shell release tools.
**Spec:** `agents/gander/work/queue-196/DESIGN.md`, approved r2 at 5fe259f16, with N5 restart policy added in r3.
## Global Constraints
- Work in the existing gander/row196 worktree. Plain node_modules symlink. No new package dependency.
- No live files, credentials, listeners, units, second uid or external authentication service.
- Helpers: proof envelopes only, at most 16 concurrent, one second timeout, 256-byte strict output.
- Root-owned system Python ELF at resolved /usr/bin/python3, never PATH or a home/shim path.
- console/ 0700, tester.sock 0600, Unix pathname at most 107 bytes.
- One principal and transient cap per request. R3 owns user access policy and user-cli integration.
- Mos accepted socket-only tester access. No TCP relay or control-board change.
- Single test files run freely. Ask Sage before any suite. Stop and report two failures in a gate run.
- Commit code only after applicable suites pass. Sage owns the final gate and manifest queue moves.
## Review Focus
- Connection overlap and disconnect: helpers must retain the correct Socket object and release capacity on every exit.
- OS interpreter replacement: restart records the actual safe binary, while old receipts remain unchanged.
- Long or unsafe instance paths: refuse before binding or stopping any service.
- Rollback/recovery: distinguish intentional tester disablement from restoration of an old saved TCP unit.
- CLI nonce respawn and session expiry: direct-parent provenance must survive correct Console spawning and refuse stale identity.
## Task 1: kernel credentials and interpreter evidence
Files: create `packages/bus/src/peercred.mjs`, `packages/bus/src/peercred.py`,
`packages/bus/src/interpreter.mjs`; tests in `packages/bus/tests/peercred.test.mjs` and
`packages/bus/tests/interpreter.test.mjs`, with subprocess fixtures under `packages/bus/fixtures/`.
Interfaces: `inspectInterpreter()` returns a validated immutable record of path, version,
device, inode and sha256. `recordInterpreterStart(directory, record)` appends the actual startup
identity safely. `createPeerReader({interpreter})` returns `{read(socket),close()}`. read resolves
to `{pid,uid,gid}` from that exact socket, or refuses. Test-only injected OS/process operations
do not become product flags or environment overrides.
- [ ] Write interpreter safety, receipt replacement and cap-independent helper tests (T3/T10).
- [ ] Run each new test file alone and record the expected missing-module failures.
- [ ] Implement system interpreter validation and startup receipt append with no-follow, owner/type/mode checks.
- [ ] Write T2's eight-process barrier plus racing copier, wrong-fd case and post-helper socket exchange.
- [ ] Implement fd-3 helper, strict result validation, bounded async reader and helper shutdown.
- [ ] Run `node --test packages/bus/tests/interpreter.test.mjs` and separately `node --test packages/bus/tests/peercred.test.mjs`.
- [ ] Confirm process/socket cleanup, eight distinct kernel PIDs, copier refusal and original receipt preservation.
## Task 2: principal provenance and bus dispatch
Files: modify `packages/bus/src/{human,tester,server,runtime}.mjs`, create
`packages/bus/src/principal.mjs`, add `packages/bus/tests/principal.test.mjs`, and extend
`packages/bus/tests/human.test.mjs`. Extend server/runtime test files after inspecting their fixtures.
Interfaces: `verifyPrincipal(request, {peer,...trustedPathsAndProcessReaders})` returns
`{kind,business,id}`. Human maps to the business human, tester to tester, user to its verified
Console proof id. No roles or grants. Server obtains peer metadata from Task 1 only after
envelope validation and only for proofs. Existing cap dispatch never invokes the helper.
Future user binding stays refused without R3's trusted integration.
- [ ] Write T4/T5/T6/T8 process-table tests, including changed ancestry, distinct nonce namespaces and kind confusion.
- [ ] Run the focused new file and confirm expected failure before implementing the verifier.
- [ ] Parameterize the nonce/path portion of the existing human walk and retain all marker/binary/launch checks.
- [ ] Wire peer metadata, interpreter startup receipt and one-request cleanup into runtime/server.
- [ ] Test cap requests with a failing helper spy (M22), and test async refusal/close cleanup.
- [ ] Run each affected bus test file alone. Record any base fixture changes needed for real peer identity.
## Task 3: tester Unix HTTP and Console nonce spawning
Files: modify `packages/webui/src/{cli,serve,bus}.mjs`, create
`packages/webui/src/tester-socket.mjs` and `packages/cli/src/tester-http.mjs`; add focused
`packages/webui/tests/tester-socket.test.mjs` and `packages/cli/tests/tester-http.test.mjs`.
Extend existing WebUI transport tests.
Interfaces: `startTesterSocket({dataRoot,...handlerOptions})` owns the private listener and
cleanup. `testerRequest({dataRoot,method,path,body})` uses the fixed logical origin and bounded
Unix HTTP. Console human child spawning supplies its initial nonce, preserving agent markers.
R3 reuses that spawn pattern for its user-cli.
- [ ] Write T1/T3/T9 socket permission, unsafe-path, wrong-origin and instance separation tests.
- [ ] Confirm the new focused files fail, then implement --tester-socket and refusal of --tester-port.
- [ ] Keep tester principal, write allowlist and cookie/header refusals intact. Add no browser relay.
- [ ] Write and implement the initial-nonce Console spawn test and the helper request timeout test.
- [ ] Run each new or changed test file alone, verifying cleanup and no tester TCP bind.
## Task 4: release migration and operator evidence
Files: modify `scripts/{webui-service,stack-release}.sh`, inspect
`scripts/stack-copy.sh` closure, extend `packages/cli/tests/stack-release.test.mjs` and its
render fixtures. Update `docs/TOOLS.md` and package READMEs with the implementation.
Interfaces: `activate ID BUSINESS --tester-socket` explicitly replaces inherited tester-port
flags. Pointer and unit argv agree. Candidate support and interpreter preflight run before
any unit stop. Activation gate evidence records the same interpreter as the bus startup receipt.
- [ ] Write T7 explicit conversion, inherited mismatch and unsupported-target refusal fixtures.
- [ ] Run only the affected test file, then implement flag rendering, plan output and rollback disabling unsupported tester.
- [ ] Preserve old-copy recovery behavior and report restored old security state accurately.
- [ ] Add rehearsal scratch socket and T10 interpreter gate identity comparison.
- [ ] Verify same-copy activation refusal is preserved. Document N5's next-reviewed-copy re-pin path.
- [ ] Run the affected file alone, shell syntax checks, and inspect remaining tester-port references by purpose.
## Task 5: mutants, approved suites and manifest
Files: add the row's mutation runner and receipts under `agents/gander/work/queue-196/`.
Append BUILD-LOG phase receipts. No SESSIONS or queue file edits.
- [ ] Exercise M1-M23 against isolated scratch copies. Record killer test, restored hash and cleanup for each.
- [ ] Run focused tests on Node 24 and 26, including the concurrent T2 case and socket lifetime checks.
- [ ] Send Sage the scope and estimated duration, obtain the suite slot, then run approved bus/WebUI/CLI suites.
- [ ] Let Sage own the scripts/test-*.sh gate. Stop on the repository's two-failure rule.
- [ ] Build the exact changed-path manifest with acceptance evidence, search results, limits and consumer audit.
- [ ] Commit/push the verified candidate only after applicable suites pass. Send Duckworth and Sage path, commit and hashes for manifest review.
Execution stays with Gander in this worktree. No new agent delegation. Duckworth reviews
the manifest independently. Push is only to gander/row196. No merge, re-pin or deployment
is authorized by this plan.
@@ -0,0 +1,186 @@
{
"status": "uncommitted draft; regenerate after final tests",
"base": "4d26003f731a6eec439df6a48cacc3ef482a59c8",
"files": [
{
"path": "docs/TOOLS.md",
"baselineSha256": "5bd1bc697cd56e57b00e60c0e80be72cd1043a55a6d2fc0b9e2ccdd0c5cf9802",
"candidateSha256": "7eba3f1ac2e5a159c9c764f1c3e4f6fdfaf06d2904cdc36338723d5ddd9044c8"
},
{
"path": "packages/bus/README.md",
"baselineSha256": "4377cc32aed37fa1bb645c0bbc3340eed4ec01740e67327449949474e35d3fdb",
"candidateSha256": "becff52b647f28079d2756b26a7e46e28f14b695f57b2dede779e7cb43af3d96"
},
{
"path": "packages/bus/fixtures/peer-client.mjs",
"baselineSha256": null,
"candidateSha256": "60720461a7994011a9a30594715eb2c992be699cd61c656e77cbd609ad05328e"
},
{
"path": "packages/bus/src/human.mjs",
"baselineSha256": "b995fc5cff3af8ade4c990d0e3bebf9d83829e00f034ebc9c3dcfd05f252b573",
"candidateSha256": "0c6d96abe4607cba0b02b0638a2fc217ff8135bac9a47761a0ef015fbe9d3079"
},
{
"path": "packages/bus/src/interpreter.mjs",
"baselineSha256": null,
"candidateSha256": "d5d98a9133a1b449acc48143fd559d890a470c6ea210b30b9b70e491e6cd855a"
},
{
"path": "packages/bus/src/peercred.mjs",
"baselineSha256": null,
"candidateSha256": "29c88655e8b9aed8db604573c9213fd9f3c40ef35ff04f4eda8fbe8f1b8a32d7"
},
{
"path": "packages/bus/src/peercred.py",
"baselineSha256": null,
"candidateSha256": "d1536a8f472fa5b3773adb10555529ed049ad63b647c0b44c00ce5f25ce09f85"
},
{
"path": "packages/bus/src/principal.mjs",
"baselineSha256": null,
"candidateSha256": "32ef39d80b523bdd586c29620d7a752c309c8e1aa70befc1414125468f3411dd"
},
{
"path": "packages/bus/src/runtime.mjs",
"baselineSha256": "fe70f4b6568fbfec85e58d34f3cb8c3624daba65487d73ad24e981f2121cb27a",
"candidateSha256": "e001bdba127fc6e7a6e95ef1864e661b2842f3c797169ab0a6ccaa5e2cd7da00"
},
{
"path": "packages/bus/src/server.mjs",
"baselineSha256": "8a1b8eac60a5885bbfd36f939b63443de303ed298bf9e1ff653a5a7bcbae76d2",
"candidateSha256": "a5788c0a0b2b78de5eb2111a47c475e100cbb0e2eca4b601e7f027cac2c56d7e"
},
{
"path": "packages/bus/tests/channel-proof.test.mjs",
"baselineSha256": "98f12f509f0e292fa0b0e3924834f9d0f285c08ea5c4d22057566c49e2bd6c68",
"candidateSha256": "e99133c3c957a1e856e404343721add2430e4a0d8c75452517aef04942af56ab"
},
{
"path": "packages/bus/tests/interpreter.test.mjs",
"baselineSha256": null,
"candidateSha256": "d19779b97e80ed3d62f58d0e798df46c23f45fa02b14fb4ac7a2ff4690a734e2"
},
{
"path": "packages/bus/tests/peer-transport.test.mjs",
"baselineSha256": null,
"candidateSha256": "a39af877a2d9143cc8d0584af0d603ca7ab651db8787e5816579762f17295887"
},
{
"path": "packages/bus/tests/peercred.test.mjs",
"baselineSha256": null,
"candidateSha256": "2c4c0bed41e546ce94535d8c4547b1cf5552a181183525dd1d52e2af1d64ccb6"
},
{
"path": "packages/bus/tests/principal.test.mjs",
"baselineSha256": null,
"candidateSha256": "cefbc0688d823c69196d94bb71467884acd639070721286d7e8e4e986c8240c1"
},
{
"path": "packages/cli/README.md",
"baselineSha256": "2c9f8fe7143aa80e59826ab53bd046de2b5e0a8f3e81fe4e3df77726bb54d50b",
"candidateSha256": "23f4c767a67b358025f0d035d2a676e14e335478c480bc44a5c303066c40709c"
},
{
"path": "packages/cli/src/tester-http.mjs",
"baselineSha256": null,
"candidateSha256": "4bd7bdf0898844684ebf9444b110d2c5b6fa874b2681183e54f1f62f97823192"
},
{
"path": "packages/cli/tests/stack-release.test.mjs",
"baselineSha256": "b01ff656962c6fdb5bc3554cfbb13645b7f25adae5f6cd9608af8de6d6091c6c",
"candidateSha256": "4c92b7cc7c4b942a1bbb9a9e24c7c1ef10e23d3f9d166bee98ed1b7f6b8de4b0"
},
{
"path": "packages/cli/tests/tester-http.test.mjs",
"baselineSha256": null,
"candidateSha256": "fbaa36e64129aee324341364bde831c379c098b97932cceadb7fcaa7909d4b20"
},
{
"path": "packages/webui/README.md",
"baselineSha256": "78c7b60d23cf20863a1cb66e16af2f9363d08acc79c5b4a344cd8af4fbe5bd44",
"candidateSha256": "8bf41140a0c013a3d77757b66e442d46c278cf8eb0f54ac26b4e477e8265279b"
},
{
"path": "packages/webui/src/bus.mjs",
"baselineSha256": "76a1b57858c4aa41071746713a196299264851c18322a6f9869d553f0362d421",
"candidateSha256": "934ab2f9b6bde72aafe7814758550c283eb25a4f3a23b284021b13718d67178f"
},
{
"path": "packages/webui/src/cli.mjs",
"baselineSha256": "4f988333ec6af225a023323dda85a159b81a77f408a8dd7bff798fe9a1f4b24b",
"candidateSha256": "fbe9db9352300fb969aa889e40323c61b1a51d01f63434964be0a8c530240e50"
},
{
"path": "packages/webui/src/serve.mjs",
"baselineSha256": "ba7d3e35b225cb4e17a4d2530a0f34d0a3734641e3982229a38207a36b3cd433",
"candidateSha256": "894bdb260af418bef7ca9dac5dd19541ca898d6f946ed12fa832bc3475234709"
},
{
"path": "packages/webui/src/tester-socket.mjs",
"baselineSha256": null,
"candidateSha256": "1c1fe0c795adac97f28502564a75f26d5666d9be5ae51169f7dd5bb7741fad16"
},
{
"path": "packages/webui/tests/browser.mjs",
"baselineSha256": "1e70a0175f1d660f6b71449a1423ece83fbe8332108960fa9aff6f7a5e48022a",
"candidateSha256": "2f3ea92cbe1ca115301ee1e3b8fc55a206842606237446b700881ee57a1b18cb"
},
{
"path": "packages/webui/tests/proof-cli.test.mjs",
"baselineSha256": "c23975b140ed977f3b98dbbec5de289e9f930cc42089193c4aa7c106338a68ba",
"candidateSha256": "105cf4936db0498d0c0025698c7ee33ddce9530b43f6c829b77afdbcb3075207"
},
{
"path": "packages/webui/tests/proof.test.mjs",
"baselineSha256": "c06a1e673ef6f3df8f0473692c828aeba853369d2b668e821f1d41e6340b6503",
"candidateSha256": "5f34d699a1bce6f46f286def1a93fdd4d863d06bec6839677fae1041be7153dd"
},
{
"path": "packages/webui/tests/service.test.mjs",
"baselineSha256": "424be2837a18a41eab56b0deb576e35b460ac16b8c371762ad44896ffc3aad7d",
"candidateSha256": "2304ced2473c25b0369b4bb9b1433c1be3ee9ab8a6f75c971487abcd48cb1d1b"
},
{
"path": "packages/webui/tests/tester-browser-fixture.mjs",
"baselineSha256": null,
"candidateSha256": "9215ff92df774ba1e83819b87f758b53c8f3d19c2824c916490165914f1cb9e8"
},
{
"path": "packages/webui/tests/tester-browser.test.mjs",
"baselineSha256": "3cc6def0c8cfb6cf7bb91a52df6e57f18071549588b35394e0083f7dc938dacf",
"candidateSha256": "89ffc5cbed1630a523080e8561d3678c1f781fb657199a5cd6cdb8189f082c73"
},
{
"path": "packages/webui/tests/tester-cli.test.mjs",
"baselineSha256": "dd74517b336e1670a5910481920ba875ffb004d52d8c6bee7cc5efc81e43215d",
"candidateSha256": "a5c6c95dbda75dce37d38d3ce4761d3ad576babd9304a046e9a62b1b5dc2fdfd"
},
{
"path": "packages/webui/tests/tester-fixture.mjs",
"baselineSha256": "2169803c50815d214cab120eaa9d1542aa3b96bfe4738d68307eb0b92c1213f3",
"candidateSha256": "7d4f3a59d21675807b1bfb92d40b7378134df450590bc6d42fa4103ab1343348"
},
{
"path": "packages/webui/tests/tester-listener.test.mjs",
"baselineSha256": "aed4cec37401bc7953b1a872349ed77c70210fcfb75ebf6d4e881a393f92c7e9",
"candidateSha256": "7f0fc2daead11048cb86e0a74b6a223314785b1740a2927461d012b876a69f18"
},
{
"path": "packages/webui/tests/tester-socket.test.mjs",
"baselineSha256": null,
"candidateSha256": "c01a942da1ad66a08a4e7c92d3c7e64f517530acfe5517f1e9895767d5ea3c0b"
},
{
"path": "scripts/stack-release.sh",
"baselineSha256": "5bb3ac40102ce515f1acdcbc5670feb161ce40a193e94e2f8b104ebf98eddc38",
"candidateSha256": "59b39d63feb77f22abb4b7152c18feff53715cf3a10dde6374c69615329868c7"
},
{
"path": "scripts/webui-service.sh",
"baselineSha256": "d514caf757c03e5d02ef39b19359669bd2476dca1877f5d237c65bd62c33d2bf",
"candidateSha256": "67cc347231d4410acf0fd845bde58aeabf099d1d6cf3e36efda4db66c5438c10"
}
]
}
+20
View File
@@ -0,0 +1,20 @@
# Row 196 execution ledger
Plan: agents/gander/work/queue-196/PLAN.md
Base: 4d26003f7. Build authorized by sage-1371, mos-1021 item 2 and mos-1023 item 3.
Pre-flight: Task 1 supplies peer reader and interpreter evidence to Task 2 and release gate in Task 4. Task 2 supplies principal provenance; R3 supplies user policy. Task 3 supplies tester transport to Task 4. Interfaces match approved design.
Ruling: retain this repository packet as the ledger instead of adding a root .superpowers directory, honoring bootstrap-only root. No code commits before applicable suites pass; Duckworth is the independent manifest reviewer. Suites and mutant batches require Sage slot.
Task 1: in progress.
Task 1: interpreter tests RED missing module -> GREEN 3/3; peer reader RED missing module -> timeout diagnosed: Node spawn stdio pauses Socket -> explicit caller resume -> GREEN 3/3. Concurrency test observed nine distinct peers, eight accepted, copier refused, post-helper bidirectional traffic. No suite/commit yet.
Task 2: principal RED missing module -> GREEN 3/3; existing human 4/4, peer-transport RED -> GREEN 2/2, transport 5/5, tester 14/14, process 8/8, all individual files. Runtime records interpreter and supplies kernel peer to shared verifier; cap traffic does not spawn helper.
Task 3: BLOCKED by design contradiction, reported gander-0016/0017 at 2026-10-20T19:34:28Z. Node/libuv closes Unix listener by unlinking its pathname even after replacement inode. Scratch cleanup assertion reproduces; conditional JavaScript cleanup is too late. Tester module not wired into serve. Recommend explicitly accepting same-uid pathname replacement as residual under private-parent boundary. No product commit, suite or mutant batch.
Task 3 resumed: sage-1382 selected Duckworth option 1. Temporary bind -> rename -> conditional final inode cleanup implemented. Replacement survival and absence of temporary name RED -> GREEN. M24 is bind-final-directly. Remaining same-uid lstat/unlink race recorded in BUILD.md. No design weakening.
Gate run 1 bus: 463 pass, 1 fail, no other packages started. Failure was row164 byte baseline, which already records intentional re-pins in later rows. Ruling: update only human.mjs and webui bus.mjs pins because their approved row196 changes are intentional, preserving the other two pins, literal entry-path tests and plug-in import ban. This does not bypass a policy refusal or weaken the proof walk.
Gate run 2: bus 464/464 pass. WebUI stopped on second failure: tester-browser.test.mjs still constructed TCP URLs using Unix address().port. 269 top-level passing cases observed, 2 failed, final suite summary unavailable due interruption. CLI and M1-M24 not started. Not timing failures. Sage notified clear in gander-0023. Node24 peer 3/3 and principal 4/4 passed. No suite/mutant rerun without renewed slot. Next: migrate scratch browser fixture without adding a product relay.
Focused repair after gate stop: tester-browser 2/2 GREEN using CDP request interception and real scratch Unix HTTP, no TCP listener or product relay. Self-review added FIFO startup-receipt refusal (RED timeout -> GREEN) and helper file preflight (RED no refusal -> GREEN). interpreter 5/5 and peer 4/4. Renewed slot requested gander-0024; suites and mutant batch still stopped.
sage-1409 accepts the fixture repair and requires separate manifest items for the row164 hashes and startup refusals, each with tests and mutants. sage-1416 delays the slot about an hour; no suite or mutant batch before go. Draft MANIFEST.md and PRODUCT-FILES.json prepared. M25 FIFO, M26 helper preflight and M27 helper shutdown are prepared, not executed. Gander-0025 reports the planned M1-M27 batch.
Focused shutdown check: peer reader now awaits killed helper close events and server close awaits the reader. peer-shutdown-red.txt records the failing assertion, peercred-green.txt the 5/5 result. Final focused Node24 reruns: peercred 5/5, principal 4/4. Node26 peer-transport 3/3 after async reader close. git diff --check clean. These are single-file checks, not renewed suite acceptance.
Consumer audit found proof insertion missed the new socket flag, making pointer and rendered unit argument order disagree. Focused row196 release fixture extended with preview + explicit proof: RED exact-order mismatch -> GREEN 1/1 after adding --tester-socket to the insertion boundary. Existing proof-file no-read check passes. Evidence release-proof-order-{red,green}.txt. M28 prepared for that boundary. Final planned mutant batch M1-M28; none executed. Shell syntax and diff whitespace checks pass.
sage-1425: complete static self-review before slot, document refused caller outcome. Real Client test now verifies human-required for proof refusal and peercred-required for helper failure, without dispatch; peer-transport 4/4. Scratch HEAD-vs-candidate comparison returns human-required in both. Quackerjack's exact failing instance is not reproduced; client.mjs unchanged and abrupt close still outcome-unknown. Manifest states the limit.
Static review complete: peer/helper lifecycle, runtime, verifier walk, tester socket, Console spawn, Unix client, release/recovery/preflight and fixtures inspected. Found release path preflight missing before stop: long-path fixture RED -> GREEN with preflight; unsafe parent also refuses before any stop. release-path-{red,green}.txt. M29 prepared. Restored unchanged legacy tester-port-invalid wording. Prepared batch now M1-M29, still unexecuted. No further implementation changes planned before Sage's go.
sage-1464 (2026-10-20T20:40Z): renewed bus/WebUI/CLI and M1-M29 slot is fifth, after rows 192, 201, 204 and 200. Continue holding until Sage's go. Fleet notice #426(a), mos-1086, live 20:32:55Z: no branch deletion; any rebase must use a new branch name; leave pr-merge.sh -d off. No rebase or deletion is planned for this row.
sage-2051 / mos-1193 (2026-10-21T18:45Z) explicitly orders committing and fast-forward pushing the current row196 build before its deferred full verification, then rebasing onto a36523713 on new gander/row196-r2. This supersedes the earlier no-product-commit-before-suite gate for this preservation commit only. Full acceptance remains pending. No tests until Sage says clear. Subsequent batch order: M1-M29, bus, WebUI, CLI, every whole scripts/test-*.sh; TMPDIR must be created with mktemp under /mnt/storage/scratch/tmp, never /tmp. Preserve the old branch. Row252 design may proceed while waiting; no row252 code before Webby approves.
@@ -0,0 +1,10 @@
✔ channel conversion preserves the four proof/transport byte baselines and modules cannot import a proof caller (0.85226ms)
✔ channel recovery still requires the same shim path and rejects an absent or forged proof (308.49278ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 389.108211
@@ -0,0 +1,12 @@
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.430093ms)
✔ process reader gets own kernel identity without exposing environment values (0.499236ms)
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.651712ms)
✔ real pid 1 remains inspectable when its environment is protected (0.268904ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 60.373844
@@ -0,0 +1,28 @@
✖ non-regular startup receipt refuses promptly without waiting on a FIFO peer (1008.229518ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1059.700302
✖ failing tests:
test at packages/bus/tests/interpreter.test.mjs:69:1
✖ non-regular startup receipt refuses promptly without waiting on a FIFO peer (1008.229518ms)
AssertionError [ERR_ASSERTION]: unsafe FIFO must refuse without blocking startup
null !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/tests/interpreter.test.mjs:75:9)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: null,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,21 @@
✔ system Python is a resolved root-owned ELF, never a shim or home interpreter (1.874163ms)
✔ real system interpreter is available and safely inspected (16.54588ms)
✔ restart receipt appends actual replacement identity and refuses unsafe files (3.340596ms)
✖ activation gate compares fresh startup receipt to the current interpreter identity (1.116553ms)
ℹ tests 4
ℹ suites 0
ℹ pass 3
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 70.983973
✖ failing tests:
test at packages/bus/tests/interpreter.test.mjs:59:1
✖ activation gate compares fresh startup receipt to the current interpreter identity (1.116553ms)
TypeError: verifyInterpreterReceipt is not a function
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/tests/interpreter.test.mjs:64:15)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
@@ -0,0 +1,13 @@
✔ system Python is a resolved root-owned ELF, never a shim or home interpreter (1.992922ms)
✔ real system interpreter is available and safely inspected (18.869225ms)
✔ restart receipt appends actual replacement identity and refuses unsafe files (3.111417ms)
✔ activation gate compares fresh startup receipt to the current interpreter identity (1.357978ms)
✔ non-regular startup receipt refuses promptly without waiting on a FIFO peer (32.332597ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 105.511835
@@ -0,0 +1,35 @@
node:internal/modules/esm/resolve:272
throw new ERR_MODULE_NOT_FOUND(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/src/interpreter.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/tests/interpreter.test.mjs
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
at moduleResolve (node:internal/modules/esm/resolve:879:10)
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
at #resolve (node:internal/modules/esm/loader:690:17)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
at ModuleJob.syncLink (node:internal/modules/esm/module_job:277:33)
at ModuleJob.link (node:internal/modules/esm/module_job:389:17) {
code: 'ERR_MODULE_NOT_FOUND',
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/src/interpreter.mjs'
}
Node.js v26.8.1
✖ packages/bus/tests/interpreter.test.mjs (29.295027ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 34.780279
✖ failing tests:
test at packages/bus/tests/interpreter.test.mjs:1:1
✖ packages/bus/tests/interpreter.test.mjs (29.295027ms)
'test failed'
@@ -0,0 +1,13 @@
✔ T2 eight proof callers and a racing copier bind the exact accepted socket, which survives helper exit (244.921933ms)
✔ helper output and failures fail closed including unsafe gid (2.053177ms)
✔ helper timeout, capacity and shutdown refuse without leaving children (26.359085ms)
✔ missing or non-regular peer helper refuses at construction before opening the bus (0.377385ms)
✔ shutdown waits for killed helper close before returning (5.980511ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 352.965488
@@ -0,0 +1,12 @@
✔ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (2.18658ms)
✔ user requires its own nonce and full walk plus immediate same-tree Console parent (0.652569ms)
✔ tester deliberately admits agent parents but rejects managed runs and another nonce (0.382801ms)
✔ human leaf identity is reread after walking the parents (0.23585ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 69.614066
@@ -0,0 +1,6 @@
{
"version": "v24.22.0",
"path": "/mnt/storage/scratch/tmp/g196-node24-gzFqsW/bin/node",
"sha256": "cc2f601cee82d9a5a233302fe307abc600727035fd7d2c3a3571755989e95008",
"source": "https://nodejs.org/dist/v24.22.0/node-v24.22.0-linux-x64.tar.xz"
}
@@ -0,0 +1,27 @@
✖ missing or non-regular peer helper refuses at construction before opening the bus (0.992412ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 73.967525
✖ failing tests:
test at packages/bus/tests/peercred.test.mjs:93:1
✖ missing or non-regular peer helper refuses at construction before opening the bus (0.992412ms)
AssertionError [ERR_ASSERTION]: Missing expected exception.
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/tests/peercred.test.mjs:94:9)
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: /peercred-required/,
operator: 'throws',
diff: 'simple'
}
@@ -0,0 +1,28 @@
✖ shutdown waits for killed helper close before returning (1.761026ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 78.151245
✖ failing tests:
test at packages/bus/tests/peercred.test.mjs:98:1
✖ shutdown waits for killed helper close before returning (1.761026ms)
AssertionError [ERR_ASSERTION]: shutdown must await the helper close event
0 !== 1
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/tests/peercred.test.mjs:106:9)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 0,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,12 @@
✔ proof receives actual peer, cap traffic never invokes helper and transient cap disconnects (7.225507ms)
✔ proof helper error and disconnect during verification never dispatch or bind (1.683542ms)
✔ Client receives explicit proof refusal codes without dispatch, not outcome-unknown (50.186199ms)
✔ asynchronous proof calls release their transient session on success, failure and timeout (28.028729ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 148.350042
@@ -0,0 +1,50 @@
✖ proof receives actual peer, cap traffic never invokes helper and transient cap disconnects (5.927716ms)
✖ proof helper error and disconnect during verification never dispatch or bind (1.959972ms)
ℹ tests 2
ℹ suites 0
ℹ pass 0
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 57.64878
✖ failing tests:
test at packages/bus/tests/peer-transport.test.mjs:9:1
✖ proof receives actual peer, cap traffic never invokes helper and transient cap disconnects (5.927716ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
false !== true
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/tests/peer-transport.test.mjs:15:9)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/bus/tests/peer-transport.test.mjs:20:1
✖ proof helper error and disconnect during verification never dispatch or bind (1.959972ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ undefined
- 'peercred-required'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/tests/peer-transport.test.mjs:25:9)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: undefined,
expected: 'peercred-required',
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,21 @@
✖ T2 eight proof callers and a racing copier bind the exact accepted socket, which survives helper exit (10044.708285ms)
ℹ started 9 clients
ℹ received 9 envelopes
ℹ read 9 peers
ℹ received 9 replies; after 0
✔ helper output and failures fail closed including unsafe gid (2.230205ms)
✔ helper timeout, capacity and shutdown refuse without leaving children (25.395463ms)
ℹ tests 3
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 1
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 10137.502565
✖ failing tests:
test at packages/bus/tests/peercred.test.mjs:15:1
✖ T2 eight proof callers and a racing copier bind the exact accepted socket, which survives helper exit (10044.708285ms)
'test timed out after 10000ms'
@@ -0,0 +1,13 @@
✔ T2 eight proof callers and a racing copier bind the exact accepted socket, which survives helper exit (274.013965ms)
✔ helper output and failures fail closed including unsafe gid (1.948532ms)
✔ helper timeout, capacity and shutdown refuse without leaving children (26.176111ms)
✔ missing or non-regular peer helper refuses at construction before opening the bus (0.331892ms)
✔ shutdown waits for killed helper close before returning (5.788665ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 396.110405
@@ -2,7 +2,7 @@ node:internal/modules/esm/resolve:272
throw new ERR_MODULE_NOT_FOUND(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/mnt/storage/scratch/gt-iwfcrf5s/base/packages/library/src/mirror.mjs' imported from /mnt/storage/scratch/gt-iwfcrf5s/base/packages/library/tests/mirror.test.mjs
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/src/peercred.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/tests/peercred.test.mjs
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
at moduleResolve (node:internal/modules/esm/resolve:879:10)
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
@@ -14,11 +14,11 @@ Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/mnt/storage/scratch/gt-iwfcrf
at ModuleJob.syncLink (node:internal/modules/esm/module_job:277:33)
at ModuleJob.link (node:internal/modules/esm/module_job:389:17) {
code: 'ERR_MODULE_NOT_FOUND',
url: 'file:///mnt/storage/scratch/gt-iwfcrf5s/base/packages/library/src/mirror.mjs'
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/src/peercred.mjs'
}
Node.js v26.8.1
✖ packages/library/tests/mirror.test.mjs (35.196517ms)
✖ packages/bus/tests/peercred.test.mjs (31.978232ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
@@ -26,10 +26,10 @@ Node.js v26.8.1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 41.636181
ℹ duration_ms 37.606365
✖ failing tests:
test at packages/library/tests/mirror.test.mjs:1:1
✖ packages/library/tests/mirror.test.mjs (35.196517ms)
test at packages/bus/tests/peercred.test.mjs:1:1
✖ packages/bus/tests/peercred.test.mjs (31.978232ms)
'test failed'
@@ -0,0 +1,15 @@
✔ T2 eight proof callers and a racing copier bind the exact accepted socket, which survives helper exit (259.164726ms)
ℹ started 9 clients
ℹ received 9 envelopes
ℹ read 9 peers; states false/true/false,false/true/false,false/true/false,false/true/false,false/true/false,false/true/false,false/true/false,false/true/false,false/true/false
ℹ received 9 replies; after 9
✔ helper output and failures fail closed including unsafe gid (1.460068ms)
✔ helper timeout, capacity and shutdown refuse without leaving children (26.09001ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 355.364452
@@ -0,0 +1,12 @@
✔ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.70707ms)
✔ user requires its own nonce and full walk plus immediate same-tree Console parent (0.585886ms)
✔ tester deliberately admits agent parents but rejects managed runs and another nonce (0.289109ms)
✔ human leaf identity is reread after walking the parents (0.170627ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 53.605151
@@ -2,7 +2,7 @@ node:internal/modules/esm/resolve:272
throw new ERR_MODULE_NOT_FOUND(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/mnt/storage/scratch/gt-iwfcrf5s/base/packages/library/src/mirror.mjs' imported from /mnt/storage/scratch/gt-iwfcrf5s/base/packages/library/tests/mirror.test.mjs
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/src/principal.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/tests/principal.test.mjs
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
at moduleResolve (node:internal/modules/esm/resolve:879:10)
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
@@ -14,11 +14,11 @@ Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/mnt/storage/scratch/gt-iwfcrf
at ModuleJob.syncLink (node:internal/modules/esm/module_job:277:33)
at ModuleJob.link (node:internal/modules/esm/module_job:389:17) {
code: 'ERR_MODULE_NOT_FOUND',
url: 'file:///mnt/storage/scratch/gt-iwfcrf5s/base/packages/library/src/mirror.mjs'
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/bus/src/principal.mjs'
}
Node.js v26.8.1
✖ packages/library/tests/mirror.test.mjs (29.912154ms)
✖ packages/bus/tests/principal.test.mjs (28.063196ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
@@ -26,10 +26,10 @@ Node.js v26.8.1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 35.532092
ℹ duration_ms 33.574982
✖ failing tests:
test at packages/library/tests/mirror.test.mjs:1:1
✖ packages/library/tests/mirror.test.mjs (29.912154ms)
test at packages/bus/tests/principal.test.mjs:1:1
✖ packages/bus/tests/principal.test.mjs (28.063196ms)
'test failed'
@@ -0,0 +1,16 @@
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (135.66144ms)
✔ startup token refusal returns safe code without value or partial listening broker (72.874581ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (105.550828ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (164.1127ms)
✔ trusted host registers later launches; socket clients never have a registration verb (109.156675ms)
✔ REQ-LIMIT-4 the broker process answers the launcher's mayLaunch op with the limits' awaited decision (100.062279ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (70.91343ms)
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (105.059528ms)
ℹ tests 8
ℹ suites 0
ℹ pass 8
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 913.679719
@@ -0,0 +1,11 @@
✔ proof startup refuses partial flags non-preview and invalid fixture file metadata or bytes (1039.716731ms)
✔ serve argv carries only fixture proof path and header; startup explicitly reports proof on or off (241.707876ms)
✔ CLI applies proof on the main listener and rejects that header on its tester listener (232.216606ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1631.223267
@@ -0,0 +1,12 @@
✔ proxy proof admits named reads and sends, reads its fixture once, and never exposes the value (33.64167ms)
✔ proxy proof refuses missing wrong short or repeated proof before auth and before any read (9.334211ms)
✔ proxy proof header is refused on other Hosts and on the tester listener (4.637787ms)
✔ proof options reject unsafe or colliding header names without disclosing file contents (0.742812ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 170.24723
@@ -0,0 +1,2 @@
baseline human-required
candidate human-required
@@ -0,0 +1,9 @@
✔ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (5883.382797ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 6639.610757
@@ -0,0 +1,36 @@
✖ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (6495.905564ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 7217.097801
✖ failing tests:
test at packages/cli/tests/stack-release.test.mjs:2088:1
✖ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (6495.905564ms)
AssertionError [ERR_ASSERTION]: stack-release: interpreter: {"path":"/usr/bin/python3","sha256":"fixture"}
stack-release: captured the rollback set: /mnt/storage/scratch/tmp/sr-eLxVuy/data-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/state/stack-rollback/20261020T201023Z-bb03a6447dc9
stack-release: stopped both units
stack-release: stopped store schema: 3g
stack-release: backup: /mnt/storage/scratch/tmp/sr-eLxVuy/data-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/backups/stack/20261020T201023Z-bb03a6447dc9
stack-release: installed bb03a6447dc98f172cbd28c2fbab126b37ff69c3
stack-release: gate passed on bb03a6447dc98f172cbd28c2fbab126b37ff69c3
stack-release: activate ok: bb03a6447dc98f172cbd28c2fbab126b37ff69c3 is live for acme
0 !== 3
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/stack-release.test.mjs:2108:9)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 0,
expected: 3,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,9 @@
✔ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (5160.650984ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 6043.67883
@@ -0,0 +1,43 @@
✖ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (3046.881963ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3691.652388
✖ failing tests:
test at packages/cli/tests/stack-release.test.mjs:2088:1
✖ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (3046.881963ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
... Skipped lines
[
'--port',
'43495',
'--business',
'acme',
...
'jason',
+ '--tester-socket',
'--proof-header',
'x-console-proof',
'--proof-file',
'/mnt/storage/scratch/tmp/sr-fLVpjD/proof.187',
- '--tester-socket'
]
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/stack-release.test.mjs:2102:9)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: [ '--port', '43495', '--business', 'acme', '--public-host', 'console.example', '--auth-header', 'x-user', '--allow-user', 'jason', '--tester-socket', '--proof-header', 'x-console-proof', '--proof-file', '/mnt/storage/scratch/tmp/sr-fLVpjD/proof.187' ],
expected: [ '--port', '43495', '--business', 'acme', '--public-host', 'console.example', '--auth-header', 'x-user', '--allow-user', 'jason', '--proof-header', 'x-console-proof', '--proof-file', '/mnt/storage/scratch/tmp/sr-fLVpjD/proof.187', '--tester-socket' ],
operator: 'deepStrictEqual',
diff: 'simple'
}
@@ -0,0 +1,9 @@
✔ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (4452.603661ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5183.817431
@@ -0,0 +1,36 @@
✖ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (2938.271119ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3279.087225
✖ failing tests:
test at packages/cli/tests/stack-release.test.mjs:2088:1
✖ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (2938.271119ms)
AssertionError [ERR_ASSERTION]: stack-release: captured the rollback set: /mnt/storage/scratch/tmp/sr-QgMOBP/data/state/stack-rollback/20261020T194438Z-6e9ce64cf63b
stack-release: drop --tester-port 7333; the target Console does not support it
stack-release: stopped both units
stack-release: stopped store schema: 3g
stack-release: backup: /mnt/storage/scratch/tmp/sr-QgMOBP/data/backups/stack/20261020T194438Z-6e9ce64cf63b
stack-release: installed 6e9ce64cf63b125d007476844c4d695a308514d8
stack-release: gate passed on 6e9ce64cf63b125d007476844c4d695a308514d8
stack-release: activate ok: 6e9ce64cf63b125d007476844c4d695a308514d8 is live for acme
0 !== 3
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/cli/tests/stack-release.test.mjs:2095:45)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 0,
expected: 3,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,9 @@
✔ row196 socket flag renders only for supporting targets and refuses mixed transports (16.910871ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 77.189844
@@ -0,0 +1,32 @@
✖ row196 socket flag renders only for supporting targets and refuses mixed transports (7.489228ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 75.646616
✖ failing tests:
test at packages/webui/tests/service.test.mjs:812:1
✖ row196 socket flag renders only for supporting targets and refuses mixed transports (7.489228ms)
AssertionError [ERR_ASSERTION]: usage: scripts/webui-service.sh copy COMMIT [--copies DIR] [--allow-worktree] | render [--copy ID [--copies DIR]] [FLAGS] | install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [--allow-worktree] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR]
FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-port N] [--proof-header NAME --proof-file PATH]
--tester-port: 1024..65535, different from --port and never 7332; loopback tester listener.
--allow-worktree is for the tests only: copy and install otherwise refuse outside the canonical checkout.
4 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/webui/tests/service.test.mjs:815:9)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 4,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,40 @@
✔ tester service render carries the tester port alongside existing flags (24.94035ms)
✔ tester service refuses forwarded 7332 even with another main port (9.82912ms)
✔ tester service invalid ports refuse render and install with exit 4 and leave the unit alone (104.005996ms)
✔ tester service counts the digits as written before it strips leading zeros (10.30812ms)
✔ tester service install and status show the tester port without changing send reporting (205.553924ms)
✔ render prints the unit without the preview flags (7.561049ms)
✔ render carries every serve flag, the preview set included (6.973808ms)
✔ a partial preview set is refused (16.637515ms)
✔ a value the unit cannot carry is refused, for every flag (256.274405ms)
✔ the rendered unit names none of the human proof agent markers and sets no agent variable (16.730737ms)
✔ install into --dir with --no-reload writes the file whole, then reports unchanged (48.962613ms)
✔ install without --no-reload runs daemon-reload and nothing that starts the unit (20.812535ms)
✔ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not (109.285453ms)
✔ status prints the unit state and GET /healthz on the installed port (162.506277ms)
✔ usage: an unknown verb or flag, and start or enable, exit 4 (59.94317ms)
✔ without --send, render is the template with its placeholders filled, byte for byte (32.400878ms)
✔ render --send with the preview set ends ExecStart with --send and changes nothing else (52.094309ms)
✔ --send without the whole preview set is refused and writes nothing (131.187356ms)
✔ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends (215.263849ms)
✔ uninstall and status take no --send (29.745082ms)
✔ install refuses while the unit is running, in any state but inactive or failed, and changes nothing (248.230583ms)
✔ when the state read fails, install and uninstall refuse with one line and change nothing (71.067475ms)
✔ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree (63.261377ms)
✔ install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing (235.658605ms)
✔ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged (19.314052ms)
✔ the uid header takes only an Authentik uid; the username header still takes a username (300.432846ms)
✔ a preview value cannot start with - or . (28.388974ms)
✔ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy (388.565391ms)
✔ proxy proof render carries both flags but never opens the fixture file (10.105723ms)
✔ proxy proof render refuses incomplete non-preview missing unsafe-mode and repo/copy paths (80.016026ms)
✔ proxy proof status retains send and tester reporting with both proof flags (97.518111ms)
✔ row196 socket flag renders only for supporting targets and refuses mixed transports (18.052409ms)
ℹ tests 32
ℹ suites 0
ℹ pass 32
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3155.23567
@@ -0,0 +1,18 @@
{
"bus": {
"top_level_passes_observed": 464,
"top_level_failures": 0,
"final_pass_count": [
"464"
],
"interrupted": false
},
"webui": {
"top_level_passes_observed": 269,
"top_level_failures": 2,
"final_pass_count": [],
"interrupted": true
},
"mutants_executed": 0,
"cli_started": false
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,83 @@
[
{
"package": "bus",
"args": [
"--test",
"--test-reporter=tap",
"--test-concurrency=2",
"packages/bus/tests/account-migration.test.mjs",
"packages/bus/tests/agents.test.mjs",
"packages/bus/tests/approval-binding.test.mjs",
"packages/bus/tests/broker.test.mjs",
"packages/bus/tests/business.test.mjs",
"packages/bus/tests/channel-proof.test.mjs",
"packages/bus/tests/chief-of-staff.test.mjs",
"packages/bus/tests/comment-cursors.test.mjs",
"packages/bus/tests/comment-migration.test.mjs",
"packages/bus/tests/credential-status.test.mjs",
"packages/bus/tests/credentials.test.mjs",
"packages/bus/tests/csuite-messaging.test.mjs",
"packages/bus/tests/daily-quota-runtime.test.mjs",
"packages/bus/tests/decision-close.test.mjs",
"packages/bus/tests/decision-route.test.mjs",
"packages/bus/tests/decisions.test.mjs",
"packages/bus/tests/end-launch.test.mjs",
"packages/bus/tests/hard-waits.test.mjs",
"packages/bus/tests/human.test.mjs",
"packages/bus/tests/instances-verb.test.mjs",
"packages/bus/tests/interpreter.test.mjs",
"packages/bus/tests/land.test.mjs",
"packages/bus/tests/launch-notice.test.mjs",
"packages/bus/tests/limit-checks.test.mjs",
"packages/bus/tests/limit-stop.test.mjs",
"packages/bus/tests/limits-runtime.test.mjs",
"packages/bus/tests/limits.test.mjs",
"packages/bus/tests/message-citation.test.mjs",
"packages/bus/tests/messages.test.mjs",
"packages/bus/tests/messaging.test.mjs",
"packages/bus/tests/name-migration.test.mjs",
"packages/bus/tests/names.test.mjs",
"packages/bus/tests/peer-transport.test.mjs",
"packages/bus/tests/peercred.test.mjs",
"packages/bus/tests/plugins-verb.test.mjs",
"packages/bus/tests/principal.test.mjs",
"packages/bus/tests/process.test.mjs",
"packages/bus/tests/project-await.test.mjs",
"packages/bus/tests/project-files.test.mjs",
"packages/bus/tests/project-runtime.test.mjs",
"packages/bus/tests/replies.test.mjs",
"packages/bus/tests/requirements.test.mjs",
"packages/bus/tests/review.test.mjs",
"packages/bus/tests/routes-verb.test.mjs",
"packages/bus/tests/schema.test.mjs",
"packages/bus/tests/single-use.test.mjs",
"packages/bus/tests/spec-migration.test.mjs",
"packages/bus/tests/spec-review.test.mjs",
"packages/bus/tests/spec-socket.test.mjs",
"packages/bus/tests/spec.test.mjs",
"packages/bus/tests/staff-request.test.mjs",
"packages/bus/tests/state.test.mjs",
"packages/bus/tests/store.test.mjs",
"packages/bus/tests/task-page.test.mjs",
"packages/bus/tests/task-read.test.mjs",
"packages/bus/tests/tasks.test.mjs",
"packages/bus/tests/tester-threads.test.mjs",
"packages/bus/tests/tester-writes.test.mjs",
"packages/bus/tests/tester.test.mjs",
"packages/bus/tests/thread-migration.test.mjs",
"packages/bus/tests/threads.test.mjs",
"packages/bus/tests/transport.test.mjs",
"packages/bus/tests/usage-broker.test.mjs",
"packages/bus/tests/usage-migration.test.mjs",
"packages/bus/tests/usage-runtime.test.mjs",
"packages/bus/tests/usage.test.mjs",
"packages/bus/tests/web.test.mjs",
"packages/bus/tests/workflow-roles.test.mjs"
],
"log": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-196/evidence/suite-bus.tap",
"failures": 1,
"stopped": false,
"code": 1,
"signal": null
}
]
@@ -0,0 +1,143 @@
[
{
"package": "bus",
"args": [
"--test",
"--test-reporter=tap",
"--test-concurrency=2",
"packages/bus/tests/account-migration.test.mjs",
"packages/bus/tests/agents.test.mjs",
"packages/bus/tests/approval-binding.test.mjs",
"packages/bus/tests/broker.test.mjs",
"packages/bus/tests/business.test.mjs",
"packages/bus/tests/channel-proof.test.mjs",
"packages/bus/tests/chief-of-staff.test.mjs",
"packages/bus/tests/comment-cursors.test.mjs",
"packages/bus/tests/comment-migration.test.mjs",
"packages/bus/tests/credential-status.test.mjs",
"packages/bus/tests/credentials.test.mjs",
"packages/bus/tests/csuite-messaging.test.mjs",
"packages/bus/tests/daily-quota-runtime.test.mjs",
"packages/bus/tests/decision-close.test.mjs",
"packages/bus/tests/decision-route.test.mjs",
"packages/bus/tests/decisions.test.mjs",
"packages/bus/tests/end-launch.test.mjs",
"packages/bus/tests/hard-waits.test.mjs",
"packages/bus/tests/human.test.mjs",
"packages/bus/tests/instances-verb.test.mjs",
"packages/bus/tests/interpreter.test.mjs",
"packages/bus/tests/land.test.mjs",
"packages/bus/tests/launch-notice.test.mjs",
"packages/bus/tests/limit-checks.test.mjs",
"packages/bus/tests/limit-stop.test.mjs",
"packages/bus/tests/limits-runtime.test.mjs",
"packages/bus/tests/limits.test.mjs",
"packages/bus/tests/message-citation.test.mjs",
"packages/bus/tests/messages.test.mjs",
"packages/bus/tests/messaging.test.mjs",
"packages/bus/tests/name-migration.test.mjs",
"packages/bus/tests/names.test.mjs",
"packages/bus/tests/peer-transport.test.mjs",
"packages/bus/tests/peercred.test.mjs",
"packages/bus/tests/plugins-verb.test.mjs",
"packages/bus/tests/principal.test.mjs",
"packages/bus/tests/process.test.mjs",
"packages/bus/tests/project-await.test.mjs",
"packages/bus/tests/project-files.test.mjs",
"packages/bus/tests/project-runtime.test.mjs",
"packages/bus/tests/replies.test.mjs",
"packages/bus/tests/requirements.test.mjs",
"packages/bus/tests/review.test.mjs",
"packages/bus/tests/routes-verb.test.mjs",
"packages/bus/tests/schema.test.mjs",
"packages/bus/tests/single-use.test.mjs",
"packages/bus/tests/spec-migration.test.mjs",
"packages/bus/tests/spec-review.test.mjs",
"packages/bus/tests/spec-socket.test.mjs",
"packages/bus/tests/spec.test.mjs",
"packages/bus/tests/staff-request.test.mjs",
"packages/bus/tests/state.test.mjs",
"packages/bus/tests/store.test.mjs",
"packages/bus/tests/task-page.test.mjs",
"packages/bus/tests/task-read.test.mjs",
"packages/bus/tests/tasks.test.mjs",
"packages/bus/tests/tester-threads.test.mjs",
"packages/bus/tests/tester-writes.test.mjs",
"packages/bus/tests/tester.test.mjs",
"packages/bus/tests/thread-migration.test.mjs",
"packages/bus/tests/threads.test.mjs",
"packages/bus/tests/transport.test.mjs",
"packages/bus/tests/usage-broker.test.mjs",
"packages/bus/tests/usage-migration.test.mjs",
"packages/bus/tests/usage-runtime.test.mjs",
"packages/bus/tests/usage.test.mjs",
"packages/bus/tests/web.test.mjs",
"packages/bus/tests/workflow-roles.test.mjs"
],
"log": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-196/evidence/suite-bus.tap",
"failures": 0,
"stopped": false,
"code": 0,
"signal": null
},
{
"package": "webui",
"args": [
"--test",
"--test-reporter=tap",
"--test-concurrency=2",
"packages/webui/tests/assistant.test.mjs",
"packages/webui/tests/board-held.test.mjs",
"packages/webui/tests/browser-edge.test.mjs",
"packages/webui/tests/browser.test.mjs",
"packages/webui/tests/bus-browser.test.mjs",
"packages/webui/tests/bus.test.mjs",
"packages/webui/tests/channel-chat.test.mjs",
"packages/webui/tests/chat-browser.test.mjs",
"packages/webui/tests/chat.test.mjs",
"packages/webui/tests/conversation.test.mjs",
"packages/webui/tests/discord.test.mjs",
"packages/webui/tests/docs-browser.test.mjs",
"packages/webui/tests/editor-browser.test.mjs",
"packages/webui/tests/editor-provenance.test.mjs",
"packages/webui/tests/editor.test.mjs",
"packages/webui/tests/history-return-flow.test.mjs",
"packages/webui/tests/instances-browser.test.mjs",
"packages/webui/tests/instances.test.mjs",
"packages/webui/tests/limits-browser.test.mjs",
"packages/webui/tests/limits-route.test.mjs",
"packages/webui/tests/markdown-preview.test.mjs",
"packages/webui/tests/messages.test.mjs",
"packages/webui/tests/pinned-copy.test.mjs",
"packages/webui/tests/preview.test.mjs",
"packages/webui/tests/project-file.test.mjs",
"packages/webui/tests/project-name-repo.test.mjs",
"packages/webui/tests/proof-cli.test.mjs",
"packages/webui/tests/proof.test.mjs",
"packages/webui/tests/reads.test.mjs",
"packages/webui/tests/relaunch.test.mjs",
"packages/webui/tests/return-flow.test.mjs",
"packages/webui/tests/routes-browser.test.mjs",
"packages/webui/tests/routes.test.mjs",
"packages/webui/tests/send-browser.test.mjs",
"packages/webui/tests/send.test.mjs",
"packages/webui/tests/serve.test.mjs",
"packages/webui/tests/service.test.mjs",
"packages/webui/tests/settings-form.test.mjs",
"packages/webui/tests/settings-write.test.mjs",
"packages/webui/tests/shell.test.mjs",
"packages/webui/tests/tester-browser.test.mjs",
"packages/webui/tests/tester-cli.test.mjs",
"packages/webui/tests/tester-listener.test.mjs",
"packages/webui/tests/tester-socket.test.mjs",
"packages/webui/tests/usage-browser.test.mjs",
"packages/webui/tests/usage.test.mjs",
"packages/webui/tests/views.test.mjs"
],
"log": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-196/evidence/suite-webui.tap",
"failures": 2,
"stopped": true,
"code": 1,
"signal": null
}
]
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,10 @@
✔ tester Console banner exposes only instance route and name editors and saves a route as tester (903.47494ms)
✔ row 177: the tester Console chat composer is enabled and a send shows in its own thread (779.82198ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1835.567916
@@ -0,0 +1,29 @@
✖ tester cleanup must leave a replacement inode untouched (4.588498ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 77.335255
✖ failing tests:
test at packages/webui/tests/tester-socket.test.mjs:31:1
✖ tester cleanup must leave a replacement inode untouched (4.588498ms)
AssertionError [ERR_ASSERTION]: closing the listener must not unlink a replacement inode
false !== true
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/webui/tests/tester-socket.test.mjs:40:9)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,9 @@
✔ tester cleanup must leave a replacement inode untouched (4.721014ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 69.761924
@@ -0,0 +1,29 @@
✖ tester cleanup must leave a replacement inode untouched (5.181827ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 70.292577
✖ failing tests:
test at packages/webui/tests/tester-socket.test.mjs:31:1
✖ tester cleanup must leave a replacement inode untouched (5.181827ms)
AssertionError [ERR_ASSERTION]: closing the listener must not unlink a replacement inode
false !== true
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/webui/tests/tester-socket.test.mjs:41:9)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,10 @@
✔ tester-port validates range tunnel main port and requires configured bus before listening (842.750345ms)
✔ CLI tester listener binds a private Unix socket and spawns TESTER_CLI for every Console bus read (2447.891251ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3426.404641
@@ -0,0 +1,10 @@
✔ tester HTTP helper uses instance socket, logical origin and JSON without retrying (16.169258ms)
✔ tester HTTP timeout closes the socket and does not retry (24.057725ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 109.82809
@@ -0,0 +1,18 @@
✔ main listener refuses x-mosaic-tester before any board or bus read for every Host (17.594021ms)
✔ tester listener rejects preview binding and requires its fixed logical Host and no proxy auth header (97.026908ms)
✔ tester listener writes only instances routes and names with tester receipts and actor (102.526377ms)
✔ tester listener refuses every human-only write with no bus call (74.180878ms)
✔ tester-off and tester-required reads return HTTP 403 (72.172231ms)
✔ tester credentials routes and committed project reads keep secrets and paths bounded (139.034531ms)
✔ row 177 W1: the tester chat makes, sends to, opens and reads its own thread as tester (19.236645ms)
✔ row 177 W2: tester chat writes refuse a missing or foreign Origin, cross-site, text/plain and the proxy header with no bus call (9.497407ms)
✔ row 177 W3: the human thread through the tester chat is thread-not-owned, and bus.tester off answers tester-off (22.342419ms)
✔ row 177 W4: the main listener refuses x-mosaic-tester on chat reads and writes before the chat (1.907ms)
ℹ tests 10
ℹ suites 0
ℹ pass 10
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 677.96388
@@ -0,0 +1,11 @@
✔ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (56.084397ms)
✔ private tester refuses symlink, unsafe parent and long path without repairs (0.822118ms)
✔ tester cleanup must leave a replacement inode untouched (1.433318ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 119.311098
@@ -0,0 +1,35 @@
node:internal/modules/esm/resolve:272
throw new ERR_MODULE_NOT_FOUND(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/webui/src/tester-socket.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/webui/tests/tester-socket.test.mjs
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
at moduleResolve (node:internal/modules/esm/resolve:879:10)
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
at #resolve (node:internal/modules/esm/loader:690:17)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
at ModuleJob.syncLink (node:internal/modules/esm/module_job:277:33)
at ModuleJob.link (node:internal/modules/esm/module_job:389:17) {
code: 'ERR_MODULE_NOT_FOUND',
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/packages/webui/src/tester-socket.mjs'
}
Node.js v26.8.1
✖ packages/webui/tests/tester-socket.test.mjs (33.788429ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 42.005573
✖ failing tests:
test at packages/webui/tests/tester-socket.test.mjs:1:1
✖ packages/webui/tests/tester-socket.test.mjs (33.788429ms)
'test failed'
@@ -0,0 +1,22 @@
✔ the tester binds only when the business sets bus.tester to exactly true (61.599223ms)
✔ a business may not name a role tester, so the tester address is never a role (17.534761ms)
✔ every verb outside the tester list refuses tester-refused and is recorded against the tester (37.114694ms)
✔ a tester message is from tester, needs no prefix, and writes no human.input or authority (14.658214ms)
✔ a tester message to a role no run holds refuses tester-target-not-running and stores nothing (10.010587ms)
✔ a tester message is refused while the run is only launched, and routes once it claims (11.801045ms)
✔ a human message to a role no run holds still waits and tells launch.by (9.843013ms)
✔ a role answers a tester only in reply to a tester message, and the answer never reaches the human (21.065845ms)
✔ the tester reads the human views a reader reads (23.085923ms)
✔ tester proof binds its own CLI, start time and nonce, refuses a managed run and allows an agent (1.132907ms)
✔ neither CLI's nonce proves the other: each proof is bound to its own CLI and nonce variable (0.338746ms)
✔ testerNonce reads only the tester's variable and gives null on any read error (0.167036ms)
✔ an envelope carries exactly one of cap, human and tester; no tester authenticator refuses (82.495712ms)
✔ tester-cli.mjs binds through the runtime when the switch is on, and refuses off or under a run marker (949.011498ms)
ℹ tests 14
ℹ suites 0
ℹ pass 14
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1344.0805
@@ -0,0 +1,13 @@
✔ socket capability stamps launch identity; shared views use wire, no SQL client (55.397456ms)
✔ two wire claims serialize; a lost reply never automatically retries (48.315452ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (34.817302ms)
✔ client preserves UTF-8 when a response divides a multibyte character (11.903844ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (29.646174ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 288.566303
@@ -0,0 +1,18 @@
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (67.351635ms)
✔ decisions route serves the roles and recent lists from the reader view (32.001579ms)
✔ credentials route serves the broker credential status from the reader view (11.335934ms)
✔ replies route serves the replies to one message from the reader view (33.381558ms)
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (56.18236ms)
✔ bus refusals map to statuses and never carry a path or a stack (26.580535ms)
✔ humanCall speaks the human transport protocol without blocking the server (1664.225925ms)
✔ busReader takes --business, else the live bus host, else refuses (179.394177ms)
✔ humanCall kills a transport that runs past its timeout (527.236263ms)
✔ serve refuses a --business value that is not a business id (267.848133ms)
ℹ tests 10
ℹ suites 0
ℹ pass 10
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3002.288149
+65
View File
@@ -0,0 +1,65 @@
// Authorized batch only. Mutate isolated scratch copies, never the candidate or installed tree.
import {mkdtempSync,cpSync,readFileSync,writeFileSync,rmSync,symlinkSync,existsSync,mkdirSync} from 'node:fs';
import {tmpdir} from 'node:os';import {join,resolve} from 'node:path';
import {spawnSync,execFileSync} from 'node:child_process';import {createHash} from 'node:crypto';
import {fileURLToPath} from 'node:url';
const root=fileURLToPath(new URL('../../../../',import.meta.url));
const out=join(root,'agents/gander/work/queue-196/evidence/mutants');mkdirSync(out,{recursive:true});
const scratch=mkdtempSync(join(tmpdir(),'g196-mut-'));
const p='packages/bus/src/principal.mjs',h='packages/bus/src/human.mjs',w='packages/webui/src/tester-socket.mjs',s='packages/bus/src/server.mjs',peer='packages/bus/src/peercred.mjs';
const principal='packages/bus/tests/principal.test.mjs',socket='packages/webui/tests/tester-socket.test.mjs',transport='packages/bus/tests/peer-transport.test.mjs',interpreter='packages/bus/tests/interpreter.test.mjs';
const release=['--test-name-pattern=row196 explicit socket','packages/cli/tests/stack-release.test.mjs'];
const specs=[
['M1', 'packages/webui/src/serve.mjs','tester uses TCP',socket, text=>text.replace('if (tester) return bindTesterSocket(server, testerSocket);',"if (tester) return new Promise(r=>server.listen(0,'127.0.0.1',()=>r(server)));" )],
['M2',w,'socket mode widened',socket,t=>t.replace('chmodSync(temporary, 0o600)','chmodSync(temporary, 0o666)')],
['M3','packages/webui/src/serve.mjs','sign-in accepted on tester',socket,t=>t.replace("if (tester && (Object.hasOwn(req.headers, 'authorization')", "if (false && (Object.hasOwn(req.headers, 'authorization')")],
['M4',p,'copied PID accepted',principal,t=>t.replace('peer.pid !== proof.pid','false')],
['M5',peer,'helper parse error ignored','packages/bus/tests/peercred.test.mjs',t=>t.replace('} catch { finish(true); }','} catch { finish(false, {pid:2,uid:0,gid:0}); }')],
['M6',p,'foreign uid accepted',principal,t=>t.replace('peer.uid !== process.getuid()','false')],
['M7',w,'unsafe parent accepted',socket,t=>t.replace('(dir.mode & 0o077)','false')],
['M8',h,'agent environment walk skipped',principal,t=>t.replace('current.env !== null && MARKERS.some','false && MARKERS.some')],
['M9',h,'leaf identity not reread',principal,t=>t.replace('if (again.startTime !== proof.startTime || again.ppid !== p.ppid)','if (false)')],
['M10',p,'Console direct-parent check removed',principal,t=>t.replace("if (parent.uid !== peer.uid || parent.argv[1] !== resolve(paths.console) || parent.argv[2] !== 'serve')",'if (false)')],
['M11',p,'Console basename accepted',principal,t=>t.replace("import { resolve }", "import { resolve, basename }").replace('parent.argv[1] !== resolve(paths.console)','basename(parent.argv[1] ?? "") !== basename(paths.console)')],
['M12',h,'unknown proof identity accepted',principal,t=>t.replace("Object.keys(proof).some((k) => !['pid', 'startTime', 'nonce', 'business'].includes(k))",'false')],
['M13',s,'transient cap leaked',transport,t=>t.replace('broker.disconnect(boundCap)','void(boundCap)')],
['M14',p,'tester walk used for user',principal,t=>t.replace("verifyHuman(checked, { cliPath: paths[kind], launches, readProcess,\n nonceKey: kind === 'user' ? 'MOSAIC_BUS_USER_NONCE' : 'MOSAIC_BUS_CLI_NONCE' })",'verifyTester(checked, {cliPath:paths[kind],launches,readProcess,readNonce:()=>checked.nonce})')],
['M15','scripts/stack-release.sh','TCP retained during conversion',release,t=>t.replace('RUN_CONSOLE=("${without_tester[@]}" --tester-socket)','RUN_CONSOLE=("${P_CONSOLE[@]}" --tester-socket)')],
['M16','scripts/stack-release.sh','TCP reopened on downgrade',release,t=>t.replace('RUN_CONSOLE=("${without_tester[@]}") DROP_TESTER="--tester-socket','RUN_CONSOLE=("${without_tester[@]}" --tester-port 7333) DROP_TESTER="--tester-socket')],
['M17','packages/bus/src/broker.mjs','tester switch ignored','packages/bus/tests/tester.test.mjs',t=>t.replace("if (this.#business(record.business).vars?.['bus.tester'] !== true)",'if (false)')],
['M18',p,'user kind becomes owner',principal,t=>t.replace("return { kind: 'user', business, id: proof.user }","return { kind: 'owner', business, id: proof.user }")],
['M19','packages/cli/src/tester-http.mjs','selected instance root ignored',socket,t=>t.replace('socketPath: testerSocketPath(dataRoot)','socketPath: testerSocketPath(dataRoot + "/wrong-instance")')],
['M20',peer,'helper gets another pending socket','packages/bus/tests/peercred.test.mjs',t=>t.replace('export function createPeerReader','function originalPeerReader')+`\nexport function createPeerReader(options){const base=originalPeerReader(options),queue=[];let scheduled=false;return{read(socket){return new Promise((resolve,reject)=>{queue.push({socket,resolve,reject});if(!scheduled){scheduled=true;queueMicrotask(()=>{scheduled=false;const jobs=queue.splice(0);jobs.forEach((job,i)=>base.read(jobs[(i+1)%jobs.length].socket).then(job.resolve,job.reject));});}});},close(){base.close();}};}\n`],
['M21','packages/bus/src/interpreter.mjs','user-owned interpreter accepted',interpreter,t=>t.replace('s.uid !== 0 || (s.mode & 0o022)','false || (s.mode & 0o022)')],
['M22',s,'cap requests invoke helper',transport,t=>t.replace(' if (kind) {',' if (r.cap) await peers.read(socket);\n if (kind) {')],
['M23','packages/bus/src/interpreter.mjs','restart retains stale identity',interpreter,t=>t.replace('export function recordInterpreterStart(directory, record) {','export function recordInterpreterStart(directory, record) {\n try { readFileSync(join(directory,"peercred-starts.jsonl")); return; } catch {}')],
['M24',w,'listener binds final path directly',socket,t=>t.replace("const temporary = join(parent, '.' + randomBytes(4).toString('hex') + '.s');",'const temporary = path;')],
['M25','packages/bus/src/interpreter.mjs','FIFO receipt blocks before type refusal',['--test-name-pattern=non-regular startup receipt',interpreter],t=>t.replaceAll(' | constants.O_NONBLOCK','')],
['M26',peer,'missing helper accepted at startup',['--test-name-pattern=missing or non-regular peer helper','packages/bus/tests/peercred.test.mjs'],t=>t.replace('if (!s.isFile() || s.isSymbolicLink()) throw Error();','if (false) throw Error();').replace('catch { throw refused(); }','catch {}')],
['M27',peer,'shutdown returns before helpers are reaped',['--test-name-pattern=shutdown waits','packages/bus/tests/peercred.test.mjs'],t=>t.replace(" await Promise.all([...children].map(child => new Promise(resolve => {\n child.once('close', resolve);\n child.kill('SIGKILL');\n })));"," for (const child of children) child.kill('SIGKILL');")],
['M28','scripts/stack-release.sh','proof pair placed after tester socket flag',release,t=>t.replace('--send | --tester-port | --tester-socket) break','--send | --tester-port) break')],
['M29','scripts/stack-release.sh','unsafe tester path reaches service stop',release,t=>t.split('\n').filter(line=>!line.includes('|| check_refuse tester-socket-path')).join('\n')],
];
const hash=x=>createHash('sha256').update(x).digest('hex');
const results=[];
try {
for(const name of ['packages','scripts','adapters','roles','contracts','RELEASE','package.json','package-lock.json','.git'])if(existsSync(join(root,name)))cpSync(join(root,name),join(scratch,name),{recursive:true,filter:src=>!src.includes('/node_modules/')&&!src.endsWith('/node_modules')});
symlinkSync(join(root,'node_modules'),join(scratch,'node_modules'));
for(const [id,file,description,test,mutate] of specs) {
const at=join(scratch,file),original=readFileSync(at,'utf8'),before=hash(original),changed=mutate(original);
if(changed===original)throw Error(id+' did not change bytes');
writeFileSync(at,changed);
const syntax=spawnSync(file.endsWith('.sh')?'bash':process.execPath,[file.endsWith('.sh')?'-n':'--check',at],{encoding:'utf8'});
if(syntax.status!==0)throw Error(id+' invalid mutation: '+syntax.stderr);
const args=['--test',...(Array.isArray(test)?test:[test])];
const run=spawnSync(process.execPath,args,{cwd:scratch,encoding:'utf8',timeout:30000,maxBuffer:2*1024*1024});
const log=(run.stdout??'')+(run.stderr??'');writeFileSync(join(out,id+'.txt'),log);
writeFileSync(at,original);if(hash(readFileSync(at))!==before)throw Error(id+' restore hash failed');
const failed=/[ℹ#]\s*(?:fail|tests failed)\s+[1-9]/.test(log) || /✖/.test(log);
const killed=run.status!==0&&failed&&!/SyntaxError|ERR_MODULE_NOT_FOUND/.test(log);
const result={id,description,test,args,status:run.status,signal:run.signal,killed,restoredSha256:before};results.push(result);
writeFileSync(join(out,'results.json'),JSON.stringify(results,null,2)+'\n');
console.log(id+' '+(killed?'KILLED':'SURVIVED/INVALID'));
if(!killed) {process.exitCode=1;break;}
}
} finally {rmSync(scratch,{recursive:true,force:true});}
@@ -0,0 +1,20 @@
import {spawn} from 'node:child_process';
import {createWriteStream,writeFileSync,readdirSync} from 'node:fs';
import {fileURLToPath} from 'node:url';import {join} from 'node:path';
const root=fileURLToPath(new URL('../../../../',import.meta.url));
const results=[];
for(const pkg of ['bus','webui','cli']) {
const files=readdirSync(join(root,'packages',pkg,'tests')).filter(n=>n.endsWith('.test.mjs')).map(n=>`packages/${pkg}/tests/${n}`);
const log=join(root,`agents/gander/work/queue-196/evidence/suite-${pkg}.tap`),output=createWriteStream(log);
let failures=0,buffer='',stopped=false;
const args=['--test','--test-reporter=tap','--test-concurrency=2',...files];
const child=spawn(process.execPath,args,{cwd:root,stdio:['ignore','pipe','pipe'],detached:true});
const read=data=>{output.write(data);buffer+=data;const lines=buffer.split('\n');buffer=lines.pop();for(const line of lines)if(/^not ok /.test(line)){failures++;if(failures>=2&&!stopped){stopped=true;process.kill(-child.pid,'SIGTERM');}}};
child.stdout.on('data',read);child.stderr.on('data',data=>output.write(data));
const status=await new Promise(resolve=>child.on('close',(code,signal)=>resolve({code,signal})));
await new Promise(resolve=>output.end(resolve));
results.push({package:pkg,args,log,failures,stopped,...status});
writeFileSync(join(root,'agents/gander/work/queue-196/evidence/suite-results.json'),JSON.stringify(results,null,2)+'\n');
console.log(pkg+' '+JSON.stringify(status)+' failures='+failures);
if(status.code!==0||failures){process.exitCode=1;break;}
}
@@ -1,711 +0,0 @@
{
"base": "c3d88840c",
"stage": "pre-implementation; not the passing final static check",
"occurrences": [
{
"file": "scripts/case2-unit.sh",
"line": 25,
"text": "# optional --tester-port N (1024..65535, never --port). Only factory takes"
},
{
"file": "scripts/case2-unit.sh",
"line": 163,
"text": " --tester-port) [ \"$PROFILE\" = factory ] || die \"the library profile refuses --tester-port: the tester listener takes writes from loopback with no sign-in\" ;;"
},
{
"file": "scripts/case2-unit.sh",
"line": 193,
"text": " --tester-port) tester_port=\"$value\""
},
{
"file": "scripts/case2-unit.sh",
"line": 194,
"text": " [[ \"$value\" =~ ^[1-9][0-9]{3,4}$ ]] && [ \"$value\" -ge 1024 ] && [ \"$value\" -le 65535 ] || die \"--tester-port must be 1024..65535: $value\" ;;"
},
{
"file": "scripts/case2-unit.sh",
"line": 204,
"text": " [ -z \"$tester_port\" ] || [ \"$tester_port\" -ne \"$port\" ] || die \"--tester-port must differ from --port\""
},
{
"file": "scripts/instance.sh",
"line": 114,
"text": "# this instance's, whose ExecStart takes PORT as --port or --tester-port."
},
{
"file": "scripts/instance.sh",
"line": 132,
"text": " --tester-port) [[ \"$tok\" =~ ^[0-9]{1,5}$ ]] && ports+=(\"$((10#$tok))\") ;;"
},
{
"file": "scripts/case1-accept.sh",
"line": 65,
"text": " case \"$a\" in --port) PORT=\"$v\"; i=$((i + 1)) ;; --tester-port) TESTER_PORT=\"$v\"; i=$((i + 1)) ;; esac"
},
{
"file": "scripts/case1-accept.sh",
"line": 88,
"text": " refuse \"$ARGS_FILE lacks a numeric --port or --tester-port after --\""
},
{
"file": "scripts/webui-service.sh",
"line": 43,
"text": "# Console), --tester-socket (private Unix tester), or legacy --tester-port N"
},
{
"file": "scripts/webui-service.sh",
"line": 74,
"text": "# --tester-port that another installed Console unit in the unit directory"
},
{
"file": "scripts/webui-service.sh",
"line": 96,
"text": " FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH]"
},
{
"file": "scripts/webui-service.sh",
"line": 97,
"text": " --tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener."
},
{
"file": "scripts/webui-service.sh",
"line": 137,
"text": " --tester-port) [ $# -ge 2 ] || usage; TESTER_PORT=\"$2\"; TESTER_SET=1; SERVE_SET=1; shift 2 ;;"
},
{
"file": "scripts/webui-service.sh",
"line": 182,
"text": " echo \"webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT\" >&2"
},
{
"file": "scripts/webui-service.sh",
"line": 266,
"text": " [ \"$TESTER_SET\" = 0 ] || flags=\"$flags --tester-port $TESTER_PORT\""
},
{
"file": "scripts/webui-service.sh",
"line": 462,
"text": " if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' \"$target\"; then"
},
{
"file": "scripts/webui-service.sh",
"line": 467,
"text": " if grep -Eq '^ExecStart=.* --sso( --send)?( --tester-port [0-9]+)?$' \"$target\"; then"
},
{
"file": "scripts/webui-service.sh",
"line": 471,
"text": " tester_port=\"$(sed -n 's/^ExecStart=.* --tester-port \\([0-9][0-9]*\\)$/\\1/p' \"$target\")\""
},
{
"file": "scripts/stack-release.sh",
"line": 47,
"text": " stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]"
},
{
"file": "scripts/stack-release.sh",
"line": 63,
"text": "activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the"
},
{
"file": "scripts/stack-release.sh",
"line": 126,
"text": " --tester-port) [ \"$OP\" = activate ] && [ $# -ge 2 ] && [ -n \"$2\" ] || usage; TESTER_PORT=\"$2\"; shift 2 ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 797,
"text": " elif grep -q -- '--tester-port' \"$cli\"; then supported=1; fi"
},
{
"file": "scripts/stack-release.sh",
"line": 800,
"text": " --tester-port) i=$((i + 1)); inherited=\"${BASE_CONSOLE[i]:-}\" ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 814,
"text": " [ -z \"$inherited\" ] || DROP_TESTER=\"--tester-port $inherited (replaced by private Unix socket)\""
},
{
"file": "scripts/stack-release.sh",
"line": 830,
"text": " check_refuse tester-port-invalid \"--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other\""
},
{
"file": "scripts/stack-release.sh",
"line": 832,
"text": " [ \"$supported\" = 1 ] || check_refuse tester-port-target \"$TO's Console does not support --tester-port\""
},
{
"file": "scripts/stack-release.sh",
"line": 833,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\" --tester-port \"$TESTER_PORT\")"
},
{
"file": "scripts/stack-release.sh",
"line": 837,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\") DROP_TESTER=\"--tester-port $inherited\""
},
{
"file": "scripts/stack-release.sh",
"line": 878,
"text": " # --send and --tester-port follow the pointer, so a --no-send or a tester port set after that"
},
{
"file": "scripts/stack-release.sh",
"line": 885,
"text": " --tester-port) i=$((i + 1)) ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 893,
"text": " --tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port \"${P_CONSOLE[i]:-}\") ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 897,
"text": " SSO_PLAN=\"sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them\""
},
{
"file": "scripts/stack-release.sh",
"line": 903,
"text": "# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP."
},
{
"file": "scripts/stack-release.sh",
"line": 919,
"text": " case \"${kept[i]}\" in --send | --tester-port | --tester-socket) break ;; esac"
},
{
"file": "scripts/stack-release.sh",
"line": 1076,
"text": " case \"${without_proof[i]}\" in --send | --tester-port | --tester-socket) break ;; esac"
},
{
"file": "scripts/stack-release.sh",
"line": 1134,
"text": "# Row 190: the target Console's --port (7332 when absent) and --tester-port against every other"
},
{
"file": "scripts/stack-release.sh",
"line": 1142,
"text": " --tester-port) ports+=(\"${RUN_CONSOLE[i + 1]:-}\") ;;"
},
{
"file": "scripts/stack-release.sh",
"line": 2106,
"text": " for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do [ \"${P_CONSOLE[i]}\" != --tester-port ] || reserved+=(\"${P_CONSOLE[i + 1]:-}\"); done"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 80,
"text": "const factoryFlags = () => [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"7333\"];"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 328,
"text": " [\"--tester-port\", \"7333\", /the library profile refuses --tester-port/],"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 364,
"text": " refuses(unit(h, { profile: \"factory\", flags: [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"443\"] }), /--tester-port must be 1024\\.\\.65535|--tester-port must differ from --port/);"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 365,
"text": " refuses(unit(h, { profile: \"factory\", flags: [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"1443\", \"--port\", \"1443\"] }), /given twice/);"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 378,
"text": " refuses(unit(h, { profile: \"factory\", flags: [\"--port\", \"443\", \"--business\", \"acme\", \"--tester-port\", \"443\"] }), /--tester-port must be 1024\\.\\.65535/);"
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 380,
"text": " assert.match(read(UNIT), /--tester-port must differ from --port/);"
},
{
"file": "packages/cli/tests/stack-release-fixture.mjs",
"line": 306,
"text": " for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\\n// serve accepts '--tester-port'\\n// serve accepts '--proof-header' (row 178)\\n${[C1,C].includes(id)?\"// serve accepts '--sso' (row 192)\\n\":''}`:'// fixture Console before tester listener, never run\\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 681,
"text": "const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 687,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 688,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 690,
"text": " r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 692,
"text": " assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 693,
"text": " assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 694,
"text": " r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 697,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 702,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 705,
"text": " assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 708,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 709,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 714,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 718,
"text": " assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 724,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 731,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 732,
"text": " r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 736,
"text": " edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 738,
"text": " edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 740,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 741,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 746,
"text": " let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 749,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 821,
"text": " const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 827,
"text": " const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 830,
"text": " const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 839,
"text": " const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 843,
"text": " await t.test('a target Console without --tester-port',async(t)=>{"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 846,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 848,
"text": " assert.deepEqual(r.out.split('\\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 850,
"text": " await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1056,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1057,
"text": " assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1058,
"text": " assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1129,
"text": " await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1132,
"text": " let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1135,
"text": " assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1137,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1138,
"text": " assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1141,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1162,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1351,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1352,
"text": " assert.deepEqual(want.slice(-2),['--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1367,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1468,
"text": "const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1480,
"text": " let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1481,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1483,
"text": " const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1486,
"text": " `sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1563,
"text": " const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1564,
"text": " assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1584,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1594,
"text": " const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1682,
"text": " assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1811,
"text": " writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme','--tester-port',TESTER],schema:store,activation:'manual',at:'2026-10-20T00:00:00Z'})+'\\n');"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2198,
"text": " ['beta\\'s tester port on alpha\\'s port',b,['--port',free,'--tester-port',a.port],1,`webui-service: refusing port-claimed: ${held}; nothing was changed\\n`],"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2202,
"text": " ['beta\\'s tester port on 7333',b,['--port',free,'--tester-port','7333'],4,'webui-service: port-reserved: 7333 is reserved for the default instance (7330 7332 7333)\\n'],"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2217,
"text": " await refused('--tester-port','--tester-port',a.port);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2280,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2290,
"text": " r=await run(e,'activate',C1,'acme','--tester-socket','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/--tester-socket/);assert.match(r.out,/drop.*--tester-port 7333/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2292,
"text": " assert.deepEqual(pointer(e).console,[...base,...pair(proof),'--tester-socket']);assert.match(consoleText(e),/--tester-socket/);assert.doesNotMatch(consoleText(e),/--tester-port/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2319,
"text": " assert.doesNotMatch(conExec(e),/ --send(?: |$)|--tester-port|--public-host|--proof-header/);"
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2388,
"text": " assert.equal(context.argv.includes('--tester-socket'),marked);assert.equal(context.argv.includes('--tester-port'),false);"
},
{
"file": "packages/cli/tests/case1-accept.test.mjs",
"line": 28,
"text": " \"--node\", NODE, \"--claude\", CLAUDE, \"--pm\", \"--\", \"--port\", \"732\", \"--business\", \"mosaic-stack\", \"--tester-port\", \"7333\"];"
},
{
"file": "packages/cli/tests/case1-accept.test.mjs",
"line": 477,
"text": " \"no tester port\": [(h) => put(h.args, ARGS.slice(0, -2).join(\"\\n\") + \"\\n\", 0o644), /lacks a numeric --port or --tester-port after --$/],"
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 68,
"text": " \"[Service]\\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\\n\");"
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 215,
"text": " put(join(h.root, \"etc\", \"systemd\", \"system\", \"mosaic-webui.service\"), \"[Service]\\nExecStart=/x serve --business mosaic-stack --tester-port 7333\\n\");"
},
{
"file": "packages/cli/tests/stack-release-external.test.mjs",
"line": 75,
"text": " for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){"
},
{
"file": "packages/webui/README.md",
"line": 128,
"text": "need `--port`, and 7330, 7332 or 7333 as `--port` or `--tester-port` exits 4"
},
{
"file": "packages/webui/README.md",
"line": 130,
"text": "`--tester-port` that another installed Console unit in the unit directory"
},
{
"file": "packages/webui/README.md",
"line": 144,
"text": "`serve --sso [--send] [--tester-port N]` makes the Console an OpenID Connect"
},
{
"file": "packages/webui/README.md",
"line": 176,
"text": "- With `--tester-port`, the tester listener refuses any request carrying an"
},
{
"file": "packages/webui/README.md",
"line": 1001,
"text": "There is no tester TCP listener. `--tester-port` is retired for this copy."
},
{
"file": "packages/webui/tests/tester-cli.test.mjs",
"line": 25,
"text": " for (const flags of [['--tester-port', '7332'], ['--port', '7444', '--tester-port', '7444'], ['--tester-port', '7444', '--port', '7444'], ...['1023', '65536', '1.5', '-1', 'nope'].map(p => ['--tester-port', p])]) {"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 125,
"text": " const r = await run(dir, ['render', '--tester-port', port, ...PREVIEW, '--send']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 127,
"text": " assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 129,
"text": " assert.ok(execStart(r.stdout).endsWith(` ${PREVIEW_FLAGS} --send --tester-port ${port}`));"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 136,
"text": " const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 150,
"text": " ['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 151,
"text": " ['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 152,
"text": " ['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\\n'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 153,
"text": " ['--port', '7444', '--tester-port', '7444'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 154,
"text": " ['--tester-port', '7444', '--port', '7444'],"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 164,
"text": " assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 172,
"text": " const six = await run(dir, ['render', '--tester-port', '007333']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 175,
"text": " assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\\n');"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 176,
"text": " const five = await run(dir, ['render', '--tester-port', '07333']);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 178,
"text": " assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 186,
"text": " const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...PREVIEW, '--send'], bin);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 188,
"text": " assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 189,
"text": " assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 199,
"text": " const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 799,
"text": " const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send', '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);"
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 813,
"text": " const both=await run(dir,['render','--tester-socket','--tester-port','7333']);"
},
{
"file": "packages/webui/src/cli.mjs",
"line": 30,
"text": " if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket');"
}
]
}
-313
View File
@@ -1,313 +0,0 @@
# Row253 build record
2026-10-21 design stage. Read brief Static check and Gate before starting. Busy check was CLEAR; claimant gander-253-start recorded rev2995. Branch gander/row253 starts from origin/refactor c3d88840c, after196 landed7247d80c7. Plain linked node_modules retained.
Only DESIGN.md and this record/inventory are written. No product code, tests, host config, units or slot files changed. BASE-PORT-INVENTORY.json records the initial source hits; it is not a passing final static check. The final check must classify every remaining hit after implementation.
Design r1 proposes configured dataRoot resolution as the target user, socket metadata plus an isolated system-Python SO_PEERCRED observation, unchanged hardening, P8 migration and honest unnamed P10 same-uid evidence. Duckworth approval is required before implementation. No test slot requested until the final static check passes; every single/pattern run also needs the slot under sage-2142.
## Implementation before slot
Duckworth-0100 approved design r1 with C1-C5. Code and new tests were written after that approval. Tests are written but unrun: sage-2142 requires a slot even for patterns, and the row253 brief requests static preparation before the slot request. Syntax checks and static source inspection only have run. No behavioral passing claim is made.
C1: component containment, dot/dotdot/double/trailing slash rejection and a no-follow component walk. T4 includes middle symlink and sibling-prefix homes, plus protected/private roots. C2: conservative ASCII path alphabet without whitespace; LC_ALL=C makes pathname length a byte count. C3: bash accepts only the canonical two-key string/boolean JSON. Root never imports the copy. C4: GNU timeout owns a process group; output is capped and the wrapper kills remaining group members and waits for its direct child on success, failure or signal. Tests cover a TERM-ignoring reader and a child that closes stdout before its parent returns. C5: both headers name the new unprivileged config query.
N1: deferred the optional broader MainPID TCP inventory. P8 retains the approved main listener plus 7331/7333 check. N2: removed accepted mos-1101 from MARKS and P_PASS.P10; healthy and failed observations both stay unnamed, per sage-2173. N3: sudo resolves through PATH, matching case1's existing fake seam. N4: F11 retains the default DATA_ROOT Discord check; generalizing it to configured dataRoot is a separate F11 follow-up.
The README had three obsolete current-usage passages. They now show the socket marker; the remaining mention explicitly says TCP is retired. No deliberate legacy source or test changed. Acceptance still runs F9's existing builtin-only settings parser as root under its existing pinned-path checks; the new query and fixed-system-Python observation run only as the stack user with env -i. P10 separately checks its parent, final socket, process uids, stable MainPID and metadata snapshots. Its probe sends no application bytes. Hostile same-uid changes before or after that snapshot remain the stated residual.
Static checks: bash -n on both scripts, node --check on both test files, git diff --check, Python AST parsing on the record drivers, all 33 variant shell syntax checks and unique-target validation. All passed. These are not behavioral tests. Five source/test/doc blobs are frozen in CANDIDATE-SHAS.json for the requested slot. UNIQUENESS.json records original and changed hashes for every definition. No suites or checks have run. Driver names, wrapper argv and controller.log avoid the busy detector's patterns; actual commands are loaded from structured JSON only after the busy guard succeeds. Every run uses bash pipefail and tee.
## Every remaining retired-port hit
Static source audit, not a test run. Each remaining hit and its reason follows.
- `packages/cli/tests/case1-accept.test.mjs:594`: New negative saved-argument fixture asserting the migration refusal. Text: `"retired TCP": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\n')+'\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],`
- `packages/cli/tests/case1-hostcheck.test.mjs:68`: Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged. Text: `"[Service]\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\n");`
- `packages/cli/tests/case1-hostcheck.test.mjs:215`: Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged. Text: `put(join(h.root, "etc", "systemd", "system", "mosaic-webui.service"), "[Service]\nExecStart=/x serve --business mosaic-stack --tester-port 7333\n");`
- `packages/cli/tests/case2-unit.test.mjs:445`: New negative factory/library fixtures asserting the migration refusal. Text: `["--tester-port", "7333", /case2-unit.sh retires tester TCP; use --tester-socket/],`
- `packages/cli/tests/case2-unit.test.mjs:496`: New negative factory/library fixtures asserting the migration refusal. Text: `refuses(unit(h, { profile: "factory", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);`
- `packages/cli/tests/stack-release-external.test.mjs:75`: Legacy stack-release external refusal fixture, excluded by brief step 5. Text: `for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){`
- `packages/cli/tests/stack-release-fixture.mjs:306`: Retained pre-196 copy fixture for legacy activation and rollback. Text: `for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\n// serve accepts '--tester-port'\n// serve accepts '--proof-header' (row 178)\n${[C1,C].includes(id)?"// serve accepts '--sso' (row 192)\n":''}`:'// fixture Console before tester listener, never run\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){`
- `packages/cli/tests/stack-release.test.mjs:681`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];`
- `packages/cli/tests/stack-release.test.mjs:687`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:688`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:690`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:692`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);`
- `packages/cli/tests/stack-release.test.mjs:693`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);`
- `packages/cli/tests/stack-release.test.mjs:694`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);`
- `packages/cli/tests/stack-release.test.mjs:697`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:702`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:705`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(r.out,/drop[^\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:708`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:709`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:714`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:718`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:724`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);`
- `packages/cli/tests/stack-release.test.mjs:731`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:732`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:736`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;`
- `packages/cli/tests/stack-release.test.mjs:738`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');`
- `packages/cli/tests/stack-release.test.mjs:740`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:741`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:746`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:749`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:821`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;`
- `packages/cli/tests/stack-release.test.mjs:827`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);`
- `packages/cli/tests/stack-release.test.mjs:830`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);`
- `packages/cli/tests/stack-release.test.mjs:839`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');`
- `packages/cli/tests/stack-release.test.mjs:843`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await t.test('a target Console without --tester-port',async(t)=>{`
- `packages/cli/tests/stack-release.test.mjs:846`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');`
- `packages/cli/tests/stack-release.test.mjs:848`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(r.out.split('\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);`
- `packages/cli/tests/stack-release.test.mjs:850`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);`
- `packages/cli/tests/stack-release.test.mjs:1056`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1057`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1058`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));`
- `packages/cli/tests/stack-release.test.mjs:1129`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{`
- `packages/cli/tests/stack-release.test.mjs:1132`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1135`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));`
- `packages/cli/tests/stack-release.test.mjs:1137`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1138`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));`
- `packages/cli/tests/stack-release.test.mjs:1141`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1162`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);`
- `packages/cli/tests/stack-release.test.mjs:1351`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);`
- `packages/cli/tests/stack-release.test.mjs:1352`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(want.slice(-2),['--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1367`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);`
- `packages/cli/tests/stack-release.test.mjs:1468`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;`
- `packages/cli/tests/stack-release.test.mjs:1480`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1481`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1483`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];`
- `packages/cli/tests/stack-release.test.mjs:1486`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: ``sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);`
- `packages/cli/tests/stack-release.test.mjs:1563`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');`
- `packages/cli/tests/stack-release.test.mjs:1564`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);`
- `packages/cli/tests/stack-release.test.mjs:1584`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);`
- `packages/cli/tests/stack-release.test.mjs:1594`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];`
- `packages/cli/tests/stack-release.test.mjs:1682`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]);`
- `packages/cli/tests/stack-release.test.mjs:1811`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme','--tester-port',TESTER],schema:store,activation:'manual',at:'2026-10-20T00:00:00Z'})+'\n');`
- `packages/cli/tests/stack-release.test.mjs:2198`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `['beta\'s tester port on alpha\'s port',b,['--port',free,'--tester-port',a.port],1,`webui-service: refusing port-claimed: ${held}; nothing was changed\n`],`
- `packages/cli/tests/stack-release.test.mjs:2202`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `['beta\'s tester port on 7333',b,['--port',free,'--tester-port','7333'],4,'webui-service: port-reserved: 7333 is reserved for the default instance (7330 7332 7333)\n'],`
- `packages/cli/tests/stack-release.test.mjs:2217`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused('--tester-port','--tester-port',a.port);`
- `packages/cli/tests/stack-release.test.mjs:2280`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:2290`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',C1,'acme','--tester-socket','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/--tester-socket/);assert.match(r.out,/drop.*--tester-port 7333/);`
- `packages/cli/tests/stack-release.test.mjs:2292`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...base,...pair(proof),'--tester-socket']);assert.match(consoleText(e),/--tester-socket/);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:2319`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.doesNotMatch(conExec(e),/ --send(?: |$)|--tester-port|--public-host|--proof-header/);`
- `packages/cli/tests/stack-release.test.mjs:2388`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.equal(context.argv.includes('--tester-socket'),marked);assert.equal(context.argv.includes('--tester-port'),false);`
- `packages/webui/README.md:1001`: Current retirement notice explicitly states no tester TCP listener. Text: `There is no tester TCP listener. `--tester-port` is retired for this copy.`
- `packages/webui/src/cli.mjs:30`: Current CLI explicit retirement refusal, not an activation path. Text: `if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket');`
- `packages/webui/tests/service.test.mjs:125`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['render', '--tester-port', port, ...PREVIEW, '--send']);`
- `packages/webui/tests/service.test.mjs:127`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));`
- `packages/webui/tests/service.test.mjs:129`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.ok(execStart(r.stdout).endsWith(` ${PREVIEW_FLAGS} --send --tester-port ${port}`));`
- `packages/webui/tests/service.test.mjs:136`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);`
- `packages/webui/tests/service.test.mjs:150`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],`
- `packages/webui/tests/service.test.mjs:151`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],`
- `packages/webui/tests/service.test.mjs:152`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\n'],`
- `packages/webui/tests/service.test.mjs:153`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--port', '7444', '--tester-port', '7444'],`
- `packages/webui/tests/service.test.mjs:154`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', '7444', '--port', '7444'],`
- `packages/webui/tests/service.test.mjs:164`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);`
- `packages/webui/tests/service.test.mjs:172`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const six = await run(dir, ['render', '--tester-port', '007333']);`
- `packages/webui/tests/service.test.mjs:175`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\n');`
- `packages/webui/tests/service.test.mjs:176`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const five = await run(dir, ['render', '--tester-port', '07333']);`
- `packages/webui/tests/service.test.mjs:178`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));`
- `packages/webui/tests/service.test.mjs:186`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...PREVIEW, '--send'], bin);`
- `packages/webui/tests/service.test.mjs:188`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);`
- `packages/webui/tests/service.test.mjs:189`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);`
- `packages/webui/tests/service.test.mjs:199`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);`
- `packages/webui/tests/service.test.mjs:799`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send', '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);`
- `packages/webui/tests/service.test.mjs:813`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const both=await run(dir,['render','--tester-socket','--tester-port','7333']);`
- `packages/webui/tests/tester-cli.test.mjs:25`: Explicit current-CLI negative calls; brief step 5. Text: `for (const flags of [['--tester-port', '7332'], ['--port', '7444', '--tester-port', '7444'], ['--tester-port', '7444', '--port', '7444'], ...['1023', '65536', '1.5', '-1', 'nope'].map(p => ['--tester-port', p])]) {`
- `scripts/case1-accept.sh:71`: New explicit migration refusal for saved old arguments. Text: `--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;`
- `scripts/case2-unit.sh:215`: New explicit migration refusal before rendering units. Text: `--tester-port) die "case2-unit.sh retires tester TCP; use --tester-socket" ;;`
- `scripts/instance.sh:114`: Legacy installed-unit port inventory/parser retained by brief step 5. Text: `# this instance's, whose ExecStart takes PORT as --port or --tester-port.`
- `scripts/instance.sh:132`: Legacy installed-unit port inventory/parser retained by brief step 5. Text: `--tester-port) [[ "$tok" =~ ^[0-9]{1,5}$ ]] && ports+=("$((10#$tok))") ;;`
- `scripts/stack-release.sh:47`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]`
- `scripts/stack-release.sh:63`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the`
- `scripts/stack-release.sh:126`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; TESTER_PORT="$2"; shift 2 ;;`
- `scripts/stack-release.sh:797`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `elif grep -q -- '--tester-port' "$cli"; then supported=1; fi`
- `scripts/stack-release.sh:800`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)); inherited="${BASE_CONSOLE[i]:-}" ;;`
- `scripts/stack-release.sh:814`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `[ -z "$inherited" ] || DROP_TESTER="--tester-port $inherited (replaced by private Unix socket)"`
- `scripts/stack-release.sh:830`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `check_refuse tester-port-invalid "--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other"`
- `scripts/stack-release.sh:832`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `[ "$supported" = 1 ] || check_refuse tester-port-target "$TO's Console does not support --tester-port"`
- `scripts/stack-release.sh:833`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `RUN_CONSOLE=("${without_tester[@]}" --tester-port "$TESTER_PORT")`
- `scripts/stack-release.sh:837`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `RUN_CONSOLE=("${without_tester[@]}") DROP_TESTER="--tester-port $inherited"`
- `scripts/stack-release.sh:878`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# --send and --tester-port follow the pointer, so a --no-send or a tester port set after that`
- `scripts/stack-release.sh:885`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)) ;;`
- `scripts/stack-release.sh:893`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port "${P_CONSOLE[i]:-}") ;;`
- `scripts/stack-release.sh:897`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `SSO_PLAN="sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them"`
- `scripts/stack-release.sh:903`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP.`
- `scripts/stack-release.sh:919`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `case "${kept[i]}" in --send | --tester-port | --tester-socket) break ;; esac`
- `scripts/stack-release.sh:1076`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `case "${without_proof[i]}" in --send | --tester-port | --tester-socket) break ;; esac`
- `scripts/stack-release.sh:1134`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# Row 190: the target Console's --port (7332 when absent) and --tester-port against every other`
- `scripts/stack-release.sh:1142`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) ports+=("${RUN_CONSOLE[i + 1]:-}") ;;`
- `scripts/stack-release.sh:2106`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do [ "${P_CONSOLE[i]}" != --tester-port ] || reserved+=("${P_CONSOLE[i + 1]:-}"); done`
- `scripts/webui-service.sh:43`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `# Console), --tester-socket (private Unix tester), or legacy --tester-port N`
- `scripts/webui-service.sh:74`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `# --tester-port that another installed Console unit in the unit directory`
- `scripts/webui-service.sh:96`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH]`
- `scripts/webui-service.sh:97`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `--tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener.`
- `scripts/webui-service.sh:137`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `--tester-port) [ $# -ge 2 ] || usage; TESTER_PORT="$2"; TESTER_SET=1; SERVE_SET=1; shift 2 ;;`
- `scripts/webui-service.sh:182`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `echo "webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT" >&2`
- `scripts/webui-service.sh:266`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `[ "$TESTER_SET" = 0 ] || flags="$flags --tester-port $TESTER_PORT"`
- `scripts/webui-service.sh:462`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' "$target"; then`
- `scripts/webui-service.sh:467`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `if grep -Eq '^ExecStart=.* --sso( --send)?( --tester-port [0-9]+)?$' "$target"; then`
- `scripts/webui-service.sh:471`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `tester_port="$(sed -n 's/^ExecStart=.* --tester-port \([0-9][0-9]*\)$/\1/p' "$target")"`
Sage-2191 accepted the requested scope and queued it behind the re-pin, 194, 202 and 254 windows. No CLEAR yet. TMPDIR will be created with `mktemp -d /mnt/storage/scratch/tmp/gXXXXXX`. STATIC.json records final syntax/diff results; no behavioral tests have run. No product commit or push has been made before the applicable suites pass.
Grant sage-2279 run1: both intended RED cases observed. First installer GREEN file 35/1 in 17.231 s: EEXIST in the pre-existing outside-secrets test because the shared hub now creates dataRoot. Changed only that mkdir to recursive; its refusal assertion stays unchanged. No product bytes changed. Recorded old/new test SHA in run1/fixture-fix.json. Harness now explicitly selects TAP for machine counts and supports GREEN-only resume. This is the first unexpected failure; a second ends the window.
Grant sage-2279 run1 ended on unexpected failure 2, reported to Sage and Mos as gander-0073, slot returned 2026-10-22T00:34:10Z. Acceptance whole 27/1 in 41.962 s: the broad new od fake intercepted F3 random-token generation as well as the ELF header read. Narrowed only that fake to -An -tx1 -N4; all other calls exec /usr/bin/od. The 12-hex assertion stays unchanged. Product blobs have not changed during the window. No tests run after the stop, and no checks/full suites/shell files started. run1/SUMMARY.json sums all raw logs; sweep count 0. Need a new CLEAR before any rerun.
Sage-2287 approves both test-only fixture fixes and requests their commit with unchanged assertions. Commit contains the two prepared row253 test files and packet, including both old/new test SHA pairs and raw run1 results. Product scripts, README and root BUILD-LOG remain uncommitted. PORT-AUDIT.md/json refresh current line numbers after the od fake grew two lines. The earlier audit above describes the pre-fix candidate. Next CLEAR scope: both whole touched files, M1-M33, cli and webui whole and ten shell scripts. Intended RED carries under unchanged product bytes; no further test has run.
## C4 static gap and addendum
Grant sage-2297: installer whole36/0 in16.449s, acceptance whole28/0 in37.550s, M1-M33 killed33/33 (bases11.808s, changed runs41.202s), CLI whole755/0 in597.084s. Webui was deliberately interrupted after4.736s for a static C4 gap; it is not a failed test under sage-2307 item5 and has no completed total. Ten shells never started. Slot returned with sweep0 as gander-0076. run2/SUMMARY.json is script-summed from the preserved results and raw logs.
The current outer GNU timeout kills its own group, but sudo(8) Process model documents a pty monitor creating a different terminal session. Signal handling says SIGKILL cannot be relayed. The exec fake does not cover that path. No host experiment ran. Duckworth-0109/0110 at2c49dd80c confirms the static gap. DESIGN.md remains unchanged; DESIGN-C4.md proposes the after-uid supervisor and independent outer bound, states the stalled-supervisor residual, compares runtime options and records the old talk watchdog as a proposed bounded follow-up.
Sage-2307/2312 authorizes writing the fake and addendum without tests. The shared case-sudo-fixture.mjs is now imported by both host-script tests. It preserves exec mode and offers a parent/setsid/catchable-signal hop mode with PID/SID records. A new installer counterexample blocks a TERM/HUP-resistant copy under both modes and checks session/PID cleanup; its explicit fixture cleanup only kills recorded fixture PIDs. This draft fake has not run, and no syntax execution is claimed for it. The two script product blobs remain exactly the granted bytes; test bytes now differ for the draft counterexample and shared seam. No repair is implemented.
The next action is Duckworth's static ruling on DESIGN-C4.md and the fake commit, not a test run. If product changes, the completed gate results are historical only and the whole gate reruns on Sage's next CLEAR. C4 now means killed at the inner bound and root never waits past the outer bound; a stopped/killed supervisor and setsid escape remain the same-uid residual. No root cross-session killer is proposed.
For the static pass, c4/*.sh.txt are non-executable snapshots of the exact granted product source. FROZEN-PRODUCT.json pins their equality to the uncommitted source blobs. They are review records, not an implemented repair or a product commit.
## C4 repair prepared for static review
Duckworth-0111 approves DESIGN-C4.md with C1-C5. The repaired scripts now contain the same inline supervisor payload after the uid drop. It uses fixed validated system binaries, starts a query session without setsid -f/-w, confirms SID equals the child PID from the kernel, reads whole stat files, scans every process group in that session, and reaps its direct child. Query bounds are inner 5 s / outer 7 s; peer bounds are 2 s / 4 s. Separate trusted timeout notices distinguish the outer refusal from a child exit status. Both headers name the bounds and tools.
The shared fake retains exec mode and a separate-session parent mode. Exact inner, outer and output-limit reasons are asserted in both modes. The setpgid child has a comm containing a closing parenthesis, space and newline. Session cleanup checks all members of recorded self-led sessions as well as exact recorded PIDs; exec mode never scans the runner's session. Every fixture cleanup kill checks the recorded SID against the kernel immediately before signalling. The stalled-supervisor case deliberately records the same-uid residual, refuses without using stdout, and performs explicit fixture cleanup. The outer timing slack is 2 s. The post-return polling budget remains 50 x 10 ms.
M1-M33 were retargeted where the repair changed source, with M34-M39 for absent inner/outer supervision, group-only cleanup, unbounded read, dropped setsid and one-copy payload drift. All 39 target strings occur exactly once in a static byte check. These are proposed checks, not kill results. The old runs remain historical evidence only: product bytes changed, so the entire gate must rerun on Sage's next CLEAR after the short static pass. No repaired-source syntax check, test, probe or host experiment has run. git diff --check reports no whitespace errors.
F5 now names row 276 / #1781 in docs/plans/DEFERRED.md (sage-2331). That path will be in the row253 manifest. c4/UPSTREAM-PATHS.json records the requested 968db0bcd..ddfb29af7 comparison and the branch-base comparison. Only DEFERRED.md overlaps upstream since c3d88840c; its current upstream text was preserved before appending this row's entry. The branch-to-refactor comparison also lists the unlanded test files on our side. No rebase was performed.
c4/REPAIR.diff, *.repair.txt and REPAIR-SHAS.json freeze the complete repair for Duckworth's C1/C3/C5 static look. The original c4/*.sh.txt and FROZEN-PRODUCT.json stay unchanged. The runnable product and test files remain uncommitted pending the applicable gate; this packet commit is records only. No node_modules path is included.
2026-10-22 02:13:30 UTC: sage-2335 publishes b64aa316a and explicitly requires a committed runnable candidate before the gate. Accordingly the seven repaired source/test/doc paths, the existing BUILD-LOG append and this record are committed now. All seven bytes still match c4/REPAIR-SHAS.json; this commit changes no reviewed packet source. CANDIDATE-SHAS.json is refreshed for the new gate. MANIFEST.sha256 covers the eight product/test/doc/log paths, including DEFERRED.md. No repaired-source verification ran; the static verdict and CLEAR remain pending, and all old results are historical only.
## duckworth-0112 delta, no runs
B1: both supervisor copies use fork-free IFS= read -r -d '' with content-based EOF/error handling. The 250 ms confirmation budget starts after the first full inventory pass. A deterministic stand-in clock test pins this placement without wall timing or real signals; M40 moves the budget back before pass one. All 40 proposed targets statically occur once. No test or syntax execution ran.
M1: BUILD-LOG.md is removed from MANIFEST.sha256. Its row253 phase lines land as a separate doc-only append after code, preserving newer upstream lines, under sage-2342. The manifest now has seven entries. The initial eight-entry manifest is superseded.
N4: SID confirmation now waits within the existing inner watchdog budget rather than only 100 sleeps. It still refuses a vanished/unreadable child or unconfirmed session, and the independent outer bound remains. N1/N2 are explicitly deferred in the existing F5 entry to row276 / #1781, as Sage rules. No case1 analogue checks were added in this delta.
The required gate base is the refactor tip Sage names at CLEAR, 378a76b85 or later. This delta is prepared on the current seat branch for static review only; no old-base gate is authorized. The final integrated gate candidate will get fresh hashes and integration records before execution. c4/delta-0112/ freezes the seven source/test/doc paths, the delta from aa81ca390, and static uniqueness. Prior frozen packets remain unchanged.
Duckworth-0113 approves 761893dac after git reads only (8d3fa3f20). B1 and M1 are closed; real timing remains unverified until the gate. N5 is harmless diagnostic noise on a vanished stat entry: the input redirection can report before stderr is redirected, and tester_capture discards supervisor stderr. No source change is made for it. N6 is recorded with R3: if the inner watchdog expires before kernel SID confirmation, finish has only the known child PID and kills that PID, not an unverified session. Other members may remain. Root still refuses, discards stdout and waits only within the outer bound. This is an explicit limit of cleanup before session identity is established, not a claim that the session was swept. The independent outer bound does not prove cross-session cleanup. Source/test bytes and the seven-entry manifest remain exactly those approved at 761893dac.
Sage-2463 integration onto 0fa7bf421: new gander/row253-0fa7 preserves both earlier branches. Only BUILD-LOG append conflicted; current upstream remains an exact prefix. README auto-merged with row191 processing controls. Focused row253 tests do not read the changed Console/processing code, but WebUI whole covers it on this base. Only README changed in the manifest; refreshed digest 24b6a01b2ede6f025ec898fa4ec830bb9585ce6151d6298e713e31c7430a502b. integration-0fa7/ records source-only dependency inspection, range-diff and every commit patch-id. No test/syntax/probe ran.
## Re-integration under sage-2659, no runs
New branch gander/row253-385a on385a7194c, rebased source141b79c29. Only the BUILD-LOG append conflicted; kept upstream as exact prefix. Runtime scripts, focused tests and shared fixture match8de88458d byte for byte. DEFERRED/README absorb upstream additions; manifest72a70e99ba0416ceb1f63ed737132ebbeb212332d1441c362b3dfa6c8b00209c. integration-385a/ holds the range-diff, full and no-log patch-ids, upstream manifest diff, test import/read drift and40 unique targets. No test or Node ran.
The focused tests indirectly import Store through CLI helpers, so they read/hash-check current schema.sql and migrations. 264a changes3k-to-3l ahead of this gate and needs a dependency re-check before CLEAR, even if its manifest overlap is empty. Whole stack-release T46 also reads current schema. case2 sandbox availability skip remains named under Duckworth0116 N9. The real Console files moved in278, but the focused tests write Console/config stand-ins; wholeWebUI covers the real changes.
SHELL-LIMITS.json now includes fresh278 run3 receipts. Same ceil(3*longest)+120 formula; foundation maximum39.901s gives240s. window.py uses each recorded whole-shell limit, and driver.py classifies GNUtimeout124/137 as timeout, never kill. No script/product/assertion changed. Historical row253 results carry nothing after C4.
| Shell | Longest recorded seconds | Limit seconds |
|---|---:|---:|
| scripts/test-auth.sh | 1.908123 | 126 |
| scripts/test-conductor.sh | 8.057174 | 145 |
| scripts/test-config.sh | 3.167892 | 130 |
| scripts/test-discord.sh | 8.776728 | 147 |
| scripts/test-extension-package.sh | 3.012857 | 130 |
| scripts/test-foundation.sh | 39.901000 | 240 |
| scripts/test-land-work.sh | 12.623263 | 158 |
| scripts/test-queue.sh | 23.443742 | 191 |
| scripts/test-release.sh | 12.168674 | 157 |
| scripts/test-task.sh | 36.783798 | 231 |
## Post264a integration, sage-2698, no runs
New gander/row253-264a rebases eleven commits onto84f53540d without conflicts; every pair and patch-id equals the385a branch. Rebased sourceb9d3d4c75c78aa6b06aefd734da781bef9b7f1da. Only DEFERRED absorbs upstream changes; seven-path manifest735fa2d7405d01ace8eb7aab366753fa5b1877cf8044b0151cb2113a0502bc1f. All runnable253 blobs unchanged. Store now loads schema3l and3k-3l migration from the landed base. integration-264a/ records the exact upstream drift and27 whole runnable test-file union (upstream-changed tests plus own focused files). Shared fixture is covered by importers. No carried result. Ask Sage for focused CLEAR, then publish after those whole files pass, then Duckworth rebase read, then whole gate on another CLEAR. N9 exact sandbox skip reason must be copied from the case2 log. No execution or push ran during integration.
## Corrected docs base, sage-2700/2702, no runs
New gander/row253-b789 rebases ontob78918850. Sourcec75c9bbd3339cb2343467715a14455044c2e4aea. Only append conflicts in BUILD-LOG and DEFERRED: current base remains exact byte prefix,253 entries appended. No old entry edited or removed. Manifest573c51cf72f2398532a6ea768536f04e3c1681997cb8b9a6f14a8aef884870ae. Five runnable blobs and README match preceding candidate; only DEFERRED hash changes.27-file whole union unchanged, no result carries. Hold branch local: final Gizmoduck docs tip must land first, then rebase/publish and focused CLEAR. No tests/Node/push. integration-b789/ records the resolution and hashes.
## Final base aca16404c, sage-2705, before execution
New gander/row253-aca1 rebases13 commits ontoaca16404c without conflicts; all13 final-hop pairs equal. Sourceed3bb376cdd5128dc58b3aa193282e1c2bfcf57d. Seven blobs unchanged, manifest573c51cf72f2398532a6ea768536f04e3c1681997cb8b9a6f14a8aef884870ae. Both append paths retain the full base prefix. Final upstream docs/queue changes touch no focused read.27-file union remains unchanged.
Step2 file bounds: every file listed in integration-aca1/FILE-LIMITS.json runs whole as timeout -k10 300 node --test --test-reporter=tap FILE, outer harness330s. Own case1/case2 first, then25 others. Busy check before each start, pipefail and tee, TMPDIR from mktemp-d /mnt/storage/scratch/tmp/gXXXXXX. First unexpected failure or timeout stops to Sage/Mos unplaced; no retry. Print HEAD and manifest digest first. No test has run. Expect10-15min; status at15min. Whole-suite limits are separate and will be written before whole-gate CLEAR.
Whole-shell limits remain as in the earlier table and SHELL-LIMITS.json:auth126,conductor145,config130,discord147,extension130,foundation240,land-work158,queue191,release157,task231. Each is at least3x its longest recorded successful whole run. No script changed.
## Focused27 green under sage-2709
Fresh candidate0604d91fc onaca16404c, manifest573c51cf. HEAD and digest are the first controller-log line, followed by the full900s CLI/300s bus-WebUI limit header before any test. Actual run14:30:03Z to2026-10-22T14:44:57Z, 894.0 wall seconds. All27 files whole604/0/0, timeout0, no retry or busy refusal. Commands use timeout-k10 per file, pipefail/tee and scratch gXXXXXX TMPDIR. No product/test/assertion changed. Entire seven-path manifest verifies after the run.
N9 did not skip. Verbatim case2 result: `ok 43 - inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds`. There are no skip lines in any focused file. Later wholeWebUI and shell skips are not measured by this focused scope.
Every measured file duration and its later3x basis follows. The last column is ceil(3*measured whole-file wall)+120, not a completed whole-gate result. Historical C4 gates carry nothing. The whole gate still awaits separate CLEAR and Duckworth rebase review.
# Focused27 whole-file results
| Whole file | Pass/fail/skip | Wall seconds | Command limit seconds | Next3x plus120 basis |
|---|---:|---:|---:|---:|
| packages/cli/tests/case1-accept.test.mjs | 34/0/0 | 99.281 | 900 | 418 |
| packages/cli/tests/case2-unit.test.mjs | 43/0/0 | 66.418 | 900 | 320 |
| packages/bus/tests/account-migration.test.mjs | 3/0/0 | 0.415 | 300 | 122 |
| packages/bus/tests/chief-of-staff.test.mjs | 42/0/0 | 2.168 | 300 | 127 |
| packages/bus/tests/comment-migration.test.mjs | 5/0/0 | 0.565 | 300 | 122 |
| packages/bus/tests/end-launch.test.mjs | 7/0/0 | 0.415 | 300 | 122 |
| packages/bus/tests/fire-revokes-grants.test.mjs | 22/0/0 | 3.271 | 300 | 130 |
| packages/bus/tests/gitea-grant.test.mjs | 49/0/0 | 8.634 | 300 | 146 |
| packages/bus/tests/gitea-migration.test.mjs | 5/0/0 | 0.415 | 300 | 122 |
| packages/bus/tests/grant-executor.test.mjs | 42/0/0 | 21.562 | 300 | 185 |
| packages/bus/tests/grant-migration.test.mjs | 6/0/0 | 0.465 | 300 | 122 |
| packages/bus/tests/lifecycle-revoke.test.mjs | 8/0/0 | 0.665 | 300 | 122 |
| packages/bus/tests/message-filter.test.mjs | 6/0/0 | 0.515 | 300 | 122 |
| packages/bus/tests/name-migration.test.mjs | 1/0/0 | 0.214 | 300 | 121 |
| packages/bus/tests/schema.test.mjs | 1/0/0 | 0.214 | 300 | 121 |
| packages/bus/tests/sessions.test.mjs | 7/0/0 | 0.615 | 300 | 122 |
| packages/bus/tests/spec-migration.test.mjs | 1/0/0 | 0.214 | 300 | 121 |
| packages/bus/tests/tester-writes.test.mjs | 8/0/0 | 0.565 | 300 | 122 |
| packages/bus/tests/thread-claims.test.mjs | 6/0/0 | 0.264 | 300 | 121 |
| packages/bus/tests/thread-migration.test.mjs | 3/0/0 | 0.365 | 300 | 122 |
| packages/bus/tests/threads.test.mjs | 17/0/0 | 1.216 | 300 | 124 |
| packages/bus/tests/usage-migration.test.mjs | 1/0/0 | 0.214 | 300 | 121 |
| packages/cli/tests/lifecycle.test.mjs | 24/0/0 | 13.296 | 900 | 160 |
| packages/cli/tests/stack-release.test.mjs | 218/0/0 | 583.453 | 900 | 1871 |
| packages/cli/tests/thread-cli.test.mjs | 11/0/0 | 1.876 | 900 | 126 |
| packages/cli/tests/thread-launch.test.mjs | 11/0/0 | 16.603 | 900 | 170 |
| packages/webui/tests/chat.test.mjs | 23/0/0 | 0.515 | 300 | 122 |
N9 verbatim: ok 43 - inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds
Sweep0 run-created leftovers. Initial broad sweep found nine current MCP/npm/code-mode infrastructure processes, all started14:20:57-14:21:07Z before this run14:30:03Z. UTC ps and /proc start ticks are retained; initial output saved, no signalling. Deleted cwd under run TMPDIR is included. No old-PID assumption replaces the receipt check.
Sage2711/2714 reported unrelated queue reads/write and Python reads by other seats. First and subsequent busy checks passed; no retry was needed and no effect is attributed to those processes. No live data was read by this seat. Raw command/log paths and all27 totals live in run3-focused/files/results.json and SUMMARY.json. Return slot after the record publication; no whole suite or M1-M40 batch starts here.
## Whole gate2737/2739, before execution
HEAD34401e7c9, product0604d91fc, manifest573c51cf. Outside agents/gander the record-tip diff is only BUILD-LOG.md, skipped by gate-lane-auth SKIP_COPY per Sage2739. Full lists/counts are printed in run3-gate/HEADER.json before first test. Serial CLI whole55 files,WebUI whole58 files, plus the20 bus files in the rebase union (subset coverage, not a bus-whole claim). Every Node call including RED/controls/changes is timeout-k10 with its killer/whole file measured bound, or900 for an unmeasured file, outerwait+30. WHOLE-GATE-LIMITS.json pins this. The two touched case files also run whole after intended REDs. M1-M40 then serial package/union files and10 shells; every mutation restores exact bytes/status before next. Shell table unchanged. First unexpected pre-existing red/product suspect/loosened assertion/timeout/second red stops unplaced. First-run own new-test red may be placed under2737, but no automatic loosening. No whole-gate test run yet.
## Whole gate2737 stopped unplaced atM21
Run started14:59:22Z. Two intendedREDs observed, exact restoration. Touched whole case243/0/0 in66.112s; case134/0/0 in93.721s. N9 ran and passed. M1-M20 killed;M21survived1/0 in8.581s against8.532s base.9controls passed,0timeouts. M22-M40 and all full-package/union/shell stages NOTRUN. Driver restored bytes/status; all7manifest checks pass, packages/scripts diff empty. Sweep0run-created leftovers. Stopped and returned slot as0106 before any retry/edit.
M21 drops only the peer regex end anchor. peer-malformed already writes a valid three-number prefix plus extra text, but the aggregate passingTAP does not expose the returnedreason. No loggedP10line can be quoted fromM21 orM17-base. Static trace shows onlyP10peerprobe executes PYTHON_STUB; configandearlierchecks useNode/shell, while tester_python reads metadata/ELF only. See run3-gate/STATIC-TRACE.md.
Mos1293requires a scratch-base diagnostic onSagegap, all17P10lines, then exactdesignreasonassertions. peer-malformed expectsprobe. Unexpecteddesignreason or anyproductchange goes toMosbeforeedits. No diagnostic or repair yet. Newcandidate/manifest/review and entiregatefromstart, no carry. Stop packet committed locally; helperpushwaitsforgap.
## Fixture-only M21 repair, mos1297, not run
Diagnostic(a) on0604 observed probe-inner-session for peer-malformed andpeer-error, not their designed probe/probe-inner-command. Mos confirmed instant shell output exits before session verification; product fails closed. Authorized option1 uses the same system Python exec for peer-output bytes/status; no delay and no product change. Sourcecommit362ec674c9e7aa8cef0c26f4d7448f459171dbd4 names1297 andquotesoldline100.
All17faults now assert exact reason with following space and anchoredP10prefix, from the accepted draft. Every old assertion retained. C4overflow andpeer-hang test bodies are byte-identical; overflow must remain probe-output-limit. Peer-hang looseprefixoutofscope unchanged. STATIC-PRESERVATION.json andREPAIR.diff in repair-1297/.
Seven-path manifesta2ac57342d9c59ddaebddade0fa475a9568719fed50c132bd7bb141167c7e72e; onlycase1testhashchanges. No repaired-source test/syntax/Node execution. Whole gate from start, no carry, after Duckworth review and SageCLEAR. M21survival orwrongmalformed/errorreason goes toMosbeforeanychange. Product fast-exit issue isVikunja532post10-27; SageownsDEFERREDentry.
### mos-1297 fresh gate under sage-2768, stopped
Ran on pushed e1bc766d8, manifest a2ac5734, no carry. Both intendedREDs observed; touched whole43/0/0 and34/0/0,17fresh controls passed. M1-M37 killed including M21 by named P10 assertion; M38 survived (drop setsid, signals killer). M39-M40 and full package/union/shell stages NOTRUN. Wall404.837s,0timeouts,0skips,N9passed. Exact restoration and all7manifest checks pass; sweep0run-created leftovers. Stopped unplaced to Sage/Mos/Duckworth as gander-0116 and returned slot; no repair or rerun. run4-gate/ preserves raw logs,commands,counts and timing.
### mos-1298: pin the direct query session
M38 drops setsid but a blocking signals query can still fail closed with inner-timeout; the old kill rule was wrong. The named killer stays row253 C4 signals query. Every sleep/signals/member case in both exec/hop modes now reads the first query-pids line and asserts exact equality of its numeric PID and kernel SID. M38 must fail this new assertion, never a timeout or a cleanup-only assertion. Every old line of the C4 query test is preserved byte for byte. Product scripts remain byte-identical. No test or Node preflight run; fresh whole gate from start follows new candidate review and Sage CLEAR, with nothing carried. Base assertion failure, another survivor or any mismatch goes to Mos before a further change.
@@ -1,9 +0,0 @@
{
"docs/plans/DEFERRED.md": "f8ad28aa8dc0f12b2c6dddb3b5dc805da05a97c7723e0525905456c064d0cfa3",
"packages/cli/tests/case-sudo-fixture.mjs": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
"packages/cli/tests/case1-accept.test.mjs": "27378eb1297dc6115b4b1ce849de6ffdb3adccf5249783106fa0570c9d696e8a",
"packages/cli/tests/case2-unit.test.mjs": "9ca67403d8614266061624ab5fbf4ea8e8a54cbd08f5b569c95592ab5ed40472",
"packages/webui/README.md": "ce9054060867120798c6b26737ac7d910d8cfbc7aa4ad614f4295619c7605e46",
"scripts/case1-accept.sh": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"scripts/case2-unit.sh": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b"
}
-147
View File
@@ -1,147 +0,0 @@
# Row253 C4 addendum: the supervisor belongs after sudo
Draft for Duckworth's static ruling. DESIGN.md remains unchanged. Authority:
duckworth-0109/0110, RULING-c4.md at 2c49dd80c, and sage-2307/2312.
No repaired product code or new test has run. Product scripts still have the
original frozen bytes; this addendum does not claim they satisfy C4.
## Finding and evidence
The current tester_capture puts GNU timeout --signal=KILL outside sudo, then
kills that timeout's process group. It works for the exec-style sudo fake.
sudo(8), **Process model**, says the pty monitor creates a new terminal session
and forks the command there. **Signal handling** (called Signals in the local
rendering) says SIGKILL cannot be caught and relayed. Therefore an outer group
kill does not establish that the query in the new session is killed.
Evidence was a static read of the installed sudo(8)/sudoers(5) documentation,
then Duckworth's independent ruling against sudo 1.9.17p2. No real sudo, host
experiment, live unit or live-port experiment ran. The shared fake below is
written but unrun. It models the session boundary, not authentication or a pty.
## Proposed trusted supervisor and runtime trade
Recommend a reviewed inline Bash supervisor, run **after the uid drop**, using
root-validated fixed system Bash, setsid, sleep and kill/builtins. These are
host tools, never binaries or scripts from the selected copy. Resolve system
symlinks to their real targets; require executable root-owned ELF files and
safe root-owned ancestors, outside homes. Do not use PATH lookup for these
supervisor tools. env -i removes BASH_ENV and other injected runtime settings.
Add setsid to the documented host dependencies. No new configuration knob.
This is a proposal under R1's root-validated system-binary option, pending
Duckworth's approval of Bash rather than the example GNU timeout. The exact
inline supervisor must be identical in both host scripts, with budgets and
command argv passed separately. No helper is imported from the copy.
Alternatives weighed:
- GNU timeout alone inside sudo fixes the session hop but kills one process
group. It misses a grandchild that uses setpgid without setsid. It needs the
same owned-session cleanup as the proposed supervisor.
- Validated --node with inline builtins can implement the same supervisor.
The installer already validates it. Acceptance deliberately reports unsafe
--node paths through F9/F10; making that binary a required trusted supervisor
would change those existing outcomes or need another pinned runtime.
- Fixed root-validated system Python can fork a session, multiplex bounded
output and scan /proc cleanly. It is already used for P10, but making it
mandatory for the config query changes missing/unsafe Python from a failed
P10 observation into a preflight refusal. Preserve that behavior here.
## R1: own a query session, not sudo's group
The supervisor starts in sudo's possibly separate session as the stack uid.
It forks the query child, then setsid occurs in that child through the fixed
system setsid binary. The supervisor itself may already be a group leader,
so it must not attempt setsid in its own process. Disable Bash job control;
the newly forked child is not a group leader, so setsid need not fork again.
The child's PID from $! is the kernel session id. No PID comes from copy output.
Run a separate five-second watchdog inside the uid boundary. On expiry, on a
supervisor catchable signal, on output refusal, and after normal query exit:
1. Scan numeric /proc/*/stat entries for the owned session id. Parse after the
final closing parenthesis, so spaces, parentheses and newlines in comm do
not shift the kernel fields. Use the kernel session field, not ancestry.
2. SIGKILL every live PID in that session, including different process groups.
Repeat until no running members remain or a fixed 250 ms cleanup budget
expires. Never select a process from a different session.
3. Wait for the direct child and the watchdog. Refuse on a cleanup limit or
read/parse error that prevents establishing cleanup. Zombies are not running;
only direct children can be reaped here. Tests pin that distinction.
A five-second deadline concerns the inner query. The root-side deadline is
independent. Query bytes are accepted only after success and completed cleanup.
The query can never authorize a root kill: its stdout supplies no PID/session.
The supervisor runs without root authority and kills only its owned session.
## R2: independent root bound and bounded output
Root waits at most seven seconds for the metadata sudo call. Keep GNU timeout
outside sudo for this separate purpose. Keep the 4096-byte cap plus one-byte
overflow detection. All partial output is discarded on any refusal.
Give outer timeout its own operator reason, distinct from inner timeout,
child failure, output limit and cleanup refusal. A child-supplied exit value
alone must not masquerade as proof the outer timer fired. Capture GNU timeout's
trusted verbose signal notice separately, with sudo/helper stderr redirected
away from that channel. The root-owned notice file is outside the user's
home. Validate the combination of notice and status; no eval or source.
Apply the same primitive to the new P10 peer probe, with inner two seconds,
outer four seconds, a 128-byte output cap and the existing one-second socket
connect timeout. Preserve its fixed system-Python safety check and failed-P10
behavior. All observed stdout still requires successful cleanup and exact
pid/uid/gid validation. Neither path sends application bytes to the socket.
## R3: narrowed claim and residual
C4 now means: killed at the inner bound, and root never waits past the outer
bound. A hostile same-uid query can kill/stop the supervisor or setsid away.
A hang in the trusted supervisor has the same outer-bound outcome. In that
case root refuses and discards stdout; the separate session may remain.
This is the existing DESIGN.md:97-99 same-uid limit, not a new closure claim.
No root-side cross-session killer is added. It would require another row.
The stalled-supervisor fake must prove outer timing, its own refusal reason
and no use of partial stdout. It then explicitly kills its recorded leftover
session as fixture cleanup, naming that step as the R3 residual. It must not
claim the product cleaned that session. Other normal-supervisor cases assert
no running members of the recorded query session remain after the call.
## R4 and F5: headers and remaining legacy calls
Both headers will name the inner supervisor, its runtime/tool validation,
five/seven-second query bounds and two/four-second peer-probe bounds.
The old F3 talk path (currently timeout 330 around AS_USER talk) has the same
sudo session-hop issue. Propose recording it as a bounded follow-up, rather
than changing it in this slice: it predates the socket metadata/probe helpers,
drives a 300-second functional conversation and simultaneous F5 scan, and has
existing stdout/stderr diagnostics. It is not part of the new five-second
config query. This is an explicit F5 scope choice for Duckworth/Sage to accept
or reject, not a claim the old talk timer kills a separate session. Likewise,
the other legacy AS_USER diagnostics are not newly supervised by this row.
## Shared fake and next evidence
packages/cli/tests/case-sudo-fixture.mjs provides the one process-model seam.
Both existing test files import it. Mode exec retains the prior fake. Mode
hop keeps a parent, forks through setsid, records kernel PID and SID, and
relays TERM/HUP/INT only. It cannot relay KILL. The installer draft case makes
the copy ignore TERM/HUP and block; it checks recorded query PIDs and the
separate fake session, then explicitly cleans fixture-owned PIDs on failure.
This is written but not run, and is expected to expose the old product gap.
Before the next whole gate, complete both-mode cases for timeout, TERM/HUP,
setpgid grandchild, overflow, and the distinct stalled-supervisor residual.
Both host-script paths use this seam. Add changes for absent inner supervisor,
absent outer bound, leader-group-only cleanup and unbounded read. The setpgid
case must kill the group-only change. Record every restore and raw log.
Duckworth reviews this addendum and fake statically first. Product code then
implements the approved mechanism. All product changes void prior results
under sage-2307/mos-1200. Only Sage's next CLEAR permits a diagnostic or gate
run. The next gate is both whole touched files, all checks, CLI/WebUI whole
and all ten shell files. Existing RED evidence carries only if applicable
product bytes remain equal. No host deployment is authorized by this packet.
-248
View File
@@ -1,248 +0,0 @@
# Row 253: private tester socket in the host scripts
Gander, 2026-10-21. F27, #1758. Base c3d88840c, after row196 landing
7247d80c7. Brief: docs/plans/2026-10-21_tester-socket-scripts.md at
0cdead66c. Reviewer Duckworth; gate owner Sage. Status: proposed, no product
code or tests changed. Claim gander-253-start is queue rev2995.
## Outcome and scope
The factory installer renders a bare --tester-socket flag instead of a TCP
port. Case1 acceptance consumes that saved form and examines the private
listener without claiming that a private pathname defeats hostile same-uid
processes. The library profile still has no tester. No .53 or .54 host may
take a copy carrying196 before253 lands, per mos-1198.
Change scripts/case2-unit.sh, scripts/case1-accept.sh and their two existing
CLI test files. Their comments/usage and fixture docs move with the code.
Do not edit the deliberate legacy paths named in the brief. No runtime bus,
Console, role, grant, control-board, dependency or systemd-template change.
No live installer or acceptance run, provider, home/config, unit or host access.
## Choices
1. Recommended: resolve the configured dataRoot as the stack user, preserve
hardening, check the final pathname's metadata and use SO_PEERCRED on a
connection to identify its listener. This supports custom roots within the
unit's existing writable home and matches196's kernel-peer approach.
2. Assume <home>/.mosaic-dev and inspect a pathname in /proc/net/unix. This
misses a custom config and is not a reliable final-path ownership check:
196 binds a temporary name and renames it. A socket's filesystem inode also
must not be confused with its kernel socket inode. Do not use this option.
3. Add a TCP relay, relax hardening or give acceptance a configurable probe
executable. All widen the surface or trust inputs and are outside this row.
There is no second bus principal verifier. The acceptance probe observes an
OS endpoint; it grants no capability and sends no HTTP or bus request.
## Installer arguments and preflight
Factory keeps required --port (1..1023) and --business; tester remains optional
for the installer. --tester-socket is a single marker, takes no value and keeps
its position in the rendered flags. The existing duplicate-flag check applies.
A following stray value is an unknown flag. --tester-port always refuses in
serve arguments, for both profiles, before unit writes or systemctl actions:
case2-unit: case2-unit.sh retires tester TCP; use --tester-socket
Library rejects --tester-socket with an explicit library-profile refusal. Its
other flags, proof-file checks and install behavior are unchanged. No legacy
conversion or fallback to TCP is introduced in this installer.
When the factory socket marker is present, a small inline metadata query runs
the selected copy's config loader under the target uid with sudo -u USER -H,
env -i, explicit HOME and the same pinned-node-first PATH used by the unit.
Root never imports user-writable copy code as root. It invokes the previously
validated --node path after dropping uid, with the copy module path as a
separate argument. This resolves the default instance just as the rendered
unit does; no MOSAIC_CONFIG or named-instance override is inherited.
The query returns only dataRoot and whether that copy's Console carries the
MOSAIC_TESTER_SOCKET_V1 marker. It prints no config values, credentials or child
diagnostics. Bound it to five seconds and 4096 output bytes; reject errors,
extra output/keys, malformed results, missing config and unsupported targets.
No root process opens a config or credential under the user's home. Existing
root metadata checks remain root metadata checks. Both script headers explain
the new, explicitly unprivileged config query.
Before any unit writes, validate the socket location:
- dataRoot is an absolute normalized plain path, below the declared home,
with no symlink in its existing path. Never expand the unit's writable scope.
- Neither home nor dataRoot is under /home, /root, /run/user, /tmp or /var/tmp.
These roots conflict with the existing ProtectHome/PrivateTmp contract.
- <dataRoot>/console/tester.sock is at most 107 ASCII bytes, matching196.
- A missing console parent is allowed; runtime creates it. An existing parent
must be a real directory, not a symlink, owned by the target uid with no
group/other permissions. A foreign owner, non-directory or unsafe mode refuses.
- Do not create/chmod the parent or inspect/delete an existing tester entry in
the installer. A running Console may own it during a re-pin. Runtime196 owns
binding and cleanup. Reinstall still does not restart running units.
The dataRoot query and all refusal families get fake-only tests. A failed
preflight leaves both units and the systemctl log unchanged.
## How the rendered unit reaches the socket
The unit still has User/Group for the stack account, ProtectSystem=strict,
ReadWritePaths=<home>, ProtectHome=yes and PrivateTmp=yes, in the same order.
Its ExecStart contains --tester-socket, with no pathname argument. The Console
loads dataRoot and derives console/tester.sock itself. Case1/2's /srv/mosaic
home is outside ProtectHome's protected hierarchies; the allowed dataRoot is
inside the existing writable home, and the socket is outside private /tmp.
No extra capability or writable directory is added. Systemd's documented
namespace rules motivate the location refusals, rather than a hardening change.
[systemd.exec](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html)
A mutable same-uid config can change between preflight and start. The unit's
writable boundary is still home; runtime's own config/socket checks still
refuse unsafe state. This row does not promise an immutable same-uid config.
The host window must verify the rendered unit actually starts under its
namespace; scratch tests inspect its directives, not a real systemd namespace.
## Acceptance arguments and endpoint observation
The saved args must contain one --tester-socket and the numeric --port.
Reject a missing/duplicate marker, a marker with an old numeric value, and
--tester-port with the same migration wording naming case1-accept.sh. Preserve
all existing pre-- argument validation and the default-instance contract.
Malformed saved inputs still refuse with exit2 before functional checks.
Acceptance resolves the actual configured tester dataRoot with the same
unprivileged, bounded query. It does not silently use the default path. An
unreadable/invalid config or a root outside the unit's allowed home refuses
before the functional talk. The old DATA_ROOT used by F11 remains unchanged;
generalizing that separate Discord check is outside this row.
P10 independently requires the Console's systemd MainPID to be positive and
stable, and its four /proc uid fields to be the stack uid. Check the private
parent's type, owner and mode, and the final entry's non-symlink socket type,
owner and exact 0600 mode. Snapshot parent/socket dev+ino and metadata before
and after the peer probe; a replacement or MainPID change fails observation.
The port floor must still be numeric and above the privileged main Console
port, retaining that existing part of the observation.
The bounded probe runs as the stack user with an empty inherited environment,
the fixed system /usr/bin/python3 resolved to a root-owned ELF, safe root-owned
ancestors, and -I -S. This is the system Python dependency already required by
196; never use PATH python, a home shim or a configurable helper. Unsafe/missing
Python is a failed P10 observation, not a fallback. Resolve/check the system
entry and target using metadata and the ELF header; do not run copy Python code
as root. Test seams emulate these root facts and the system executable.
The probe opens AF_UNIX/SOCK_STREAM, uses a one-second connect timeout, reads
SO_PEERCRED and closes. An outer three-second process-group bound applies. It
returns only pid/uid/gid, with exact structure, bounded output and numeric
validation. pid must equal MainPID and uid the stack uid; gid is validated but
not an identity grant. No application bytes, cookies, credentials or proof
envelope are sent. Linux supplies the peer credentials for the connected
stream, so the check does not depend on the temporary bind pathname.
[unix(7), SO_PEERCRED](https://man7.org/linux/man-pages/man7/unix.7.html)
This is a snapshot. Hostile same-uid processes can alter same-uid state or
interfere with the unprivileged probe. Root, ptrace and later pathname changes
are not defeated here. The probe does not prove another uid is denied; that
remains a host-window observation, not a fake test claim.
## P8 and P10 outcome lines
P8 must change with removal of TESTER_PORT. It checks the main Console port is
loopback-only and held only by MainPID, and that nothing listens on TCP7331 or
the retired TCP7333. Socket ownership belongs to P10. Its healthy line is:
P8 lan-listeners closed <port> is on 127.0.0.1 only by MainPID <pid>; no TCP listener on 7331 or 7333; tester uses <socket>
An unexpected listener, missing main listener or wrong owner produces P8
lan-listeners unnamed with the observed listener summaries, as today.
Do not reuse accepted mos-1101 for a new Unix-socket claim: that ruling named a
TCP-port condition. Proposed safe default pending any new Mos classification:
P10 tester-socket-squat unnamed boundary=observed path=<socket> parent=0700 owner=<uid> socket=0600 peer_pid=<pid> peer_uid=<uid> floor=<floor> port=<port>; hostile same-uid substitution remains
Any missing/unsafe metadata, wrong peer, unsafe interpreter, malformed/timeout
probe, floor miss or changed snapshot produces one P10 line instead:
P10 tester-socket-squat unnamed boundary=failed reason=<bounded-code> path=<socket> MainPID=<pid>; hostile same-uid substitution remains
Healthy observation does not mark the same-uid squat path closed or invent an
accepted ruling. P checks still never change exit status. Tests pin the reason
and observed/failed distinction, so validation cannot disappear behind an
always-unnamed mark. Sage/Mos may supply a new classification at design review;
it must be explicit before changing this proposed mark. No Jason question is
needed merely to retain unnamed evidence.
## Evidence and planned checks
Keep the existing scratch fixtures, fake account database/systemctl/curl/ss/
sudo/stat and fake interpreter. Add copy config/marker fixtures and probe
metadata under those same roots. No test connects to a live socket or runs the
real host scripts without their existing seams. Fixture fakes verify the
unprivileged command argv; no test claims a real uid drop or namespace.
The fake system interpreter executes the actual probe payload against a fake
Python socket API, checking AF_UNIX/SOCK_STREAM, timeout, connect, getsockopt
arguments and returned peer bytes. This catches errors in the inline probe
without opening an OS listener. The metadata query is tested with nonzero
status plus valid-looking output as well as malformed output, so removing its
status refusal cannot hide behind a later parse error.
| Checks | Required observations |
|---|---|
| T1 factory render | Bare marker exactly once, argument order retained, no TCP flag, hardening unchanged; duplicate/stray value refuses |
| T2 profile/migration | Both profiles refuse retired TCP; library refuses socket; all refuse before writes/start |
| T3 config/target | Invalid/missing/noisy/timed-out metadata and unmarked target refuse; query uses target uid and clean environment |
| T4 location | Default and custom in-home roots work; outside-home, protected/private hierarchies, symlinks and >107-byte pathname refuse |
| T5 parent | Missing parent allowed; file/symlink/foreign owner/group-other mode refuse without mutation or systemctl calls |
| T6 saved args | Required socket marker; missing/duplicate/stray value and retired TCP refuse exit2 before talk |
| T7 P8 | Main-only loopback succeeds; LAN/wrong owner/7331 or7333 listener gives unnamed |
| T8 P10 | Exact metadata, stable MainPID and matching peer produce boundary=observed; custom configured root used |
| T9 failed P10 | Parent/socket faults, wrong PID/uid, malformed/timeout probe, unsafe Python, bad floor and changed snapshot each give failed reason |
| T10 preserved behavior | Functional checks, their exit semantics, library behavior and all other P paths retained |
Planned checks to kill, one target change at a time in isolated snapshots:
| IDs | Removed requirement |
|---|---|
| M1-M3 | Installer TCP refusal, library socket refusal, duplicate socket refusal |
| M4-M5 | Config-query failure refusal, unsupported-target refusal |
| M6-M8 | Home containment, protected/private path refusal,107-byte limit |
| M9-M11 | Existing parent type/symlink, uid, mode checks |
| M12-M15 | Acceptance TCP refusal, required marker, duplicate/stray marker rejection, config-query refusal |
| M16 | P8 retired TCP7333 check |
| M17-M18 | P10 parent and final socket metadata checks |
| M19-M21 | Peer PID, peer uid, malformed/timeout probe refusal |
| M22-M23 | Safe fixed system interpreter, stable metadata/MainPID snapshot |
| M24-M25 | Target-uid invocation in installer and acceptance metadata/probe paths |
| M26 | Privileged main-port floor observation |
| M27 | Probe returns its own PID rather than the connected peer's credentials |
Before execution, give every definition one unique source target and a named
killer test. Preserve raw logs, base times, syntax preflight and byte-identical
SHA/Git restoration. A timeout counts only under Sage's explicit rules and
must kill the process group. Refusal variants in T1-T9 must not be merely
assertions of a generic nonzero exit.
## Static inventory and gate
After implementation, enumerate every --tester-port hit in scripts/packages
in BUILD.md and a machine-readable file. The only permitted hits are the brief's
deliberate legacy producers/parsers/tests and this row's explicit refusals and
negative fixtures. Inspect arguments and generated units, not just filenames.
Readmes must describe retirement rather than produce old argv. No modification
to webui-service, stack-release, instance, their tests/fixture, case1-hostcheck
or tester-cli negative calls is planned. Stop and ask Sage if one is needed.
Then request Sage's slot. Under that grant, run CLI whole, WebUI whole, every
shell test suite whole and all named checks above. Use an isolated candidate,
plain linked node_modules, a short gXXX TMPDIR under /mnt/storage/scratch/tmp,
busy &&, pipefail and tee. Single/pattern runs need the slot too, per sage-2142.
No harness/helper/log argument may match the busy detector pattern, per
mos-1205. Two failures end the window; report any stop to Sage/Mos before a
retry. Return the slot with script-summed counts/timings and a leftover sweep.
Design approval precedes code. The manifest, immutable candidate hashes,
static inventory and gate evidence go to Duckworth before Sage's landing.
Queue moves use the helper, with a coordinated queue-write gap. Sage posts the
jarvis review request after the in-review move. BUILD-LOG/session history stays
append-only; Sage continues session registration. No host deployment is part
of row253 acceptance.
-26
View File
@@ -1,26 +0,0 @@
# Row253 committed C4 candidate manifest
MANIFEST.sha256 names seven source/test/doc landing paths and pins their bytes. Its digest is the review identifier. BUILD-LOG.md is excluded: land the row253 phase lines as a doc-only append after the code, preserving upstream appends (duckworth-0112 M1, sage-2342).
Base before the required gate integration: c3d88840c. Initial static repair packet: b64aa316a; initial runnable candidate: aa81ca390. The delta packet is c4/delta-0112/. Full records live under agents/gander/work/queue-253/ as review evidence. DEFERRED.md preserves upstream entries and names row276 / #1781 for F5 and N1/N2.
Sage-2335 authorizes the runnable commit before the whole gate. No repaired-source syntax check, test, probe or host experiment has run. No results from 8a8df4171 carry. After Duckworth's delta static verdict, the gate must use the candidate integrated onto the refactor tip named by Sage at CLEAR (378a76b85 or later under sage-2342), never this old base. Fresh integration records and hashes will identify that final gate candidate.
Superseding integration under sage-2463: gander/row253-0fa7 on 0fa7bf421. See integration-0fa7/. Only README changed among the seven landing entries, retaining row191 processing-control documentation; the refreshed manifest digest is 24b6a01b2ede6f025ec898fa4ec830bb9585ce6151d6298e713e31c7430a502b. No gate has run.
Superseding integration under sage-2659: gander/row253-385a on385a7194c, source141b79c29, digest72a70e99ba0416ceb1f63ed737132ebbeb212332d1441c362b3dfa6c8b00209c. Only README and DEFERRED hashes change. See integration-385a/ for drift, patch-ids and dependency reads. No gate run;264a schema dependency must be re-checked before CLEAR.
Post264a supersedes the385a preview: gander/row253-264a on84f53540d, rebased sourceb9d3d4c75c78aa6b06aefd734da781bef9b7f1da, manifest735fa2d7405d01ace8eb7aab366753fa5b1877cf8044b0151cb2113a0502bc1f. Only DEFERRED differs from the preview. See integration-264a/ for all11 equal pairs, patch-ids and whole-file union. Focused whole-file runs, publication, rebase review and full gate are pending Sage CLEAR.
Corrected interim integration2700/2702: gander/row253-b789 onb78918850, sourcec75c9bbd3339cb2343467715a14455044c2e4aea, digest573c51cf72f2398532a6ea768536f04e3c1681997cb8b9a6f14a8aef884870ae.27-file scope unchanged. Pending final docs tip, rebase and CLEAR; no gate run.
Final base under sage-2705: gander/row253-aca1 onaca16404c, sourceed3bb376cdd5128dc58b3aa193282e1c2bfcf57d, digest573c51cf unchanged. integration-aca1/ is the final-hop packet. No runs yet.
Focused27 under sage-2709 now passed on0604d91fc:604/0/0, no timeouts, no retries, N9 executed and passed (exact line in run3-focused/SUMMARY.json). All7 blobs unchanged;573c51cf remains the review digest. run3-focused/ contains every raw whole-file log/command, HEAD/digest/limits preamble, timings and3x basis. No full gate or mutation-batch result is claimed.
Whole gate2737 stopped atM21 survivor after20kills. No final acceptance. run3-gate/STOP-SUMMARY.json andSTATIC-TRACE.md preserve the stop. Mos1293requires diagnostic thennewcandidate/review/fullgatefromstart. Current7sourceblobs remain573c51cf; no test/product repair made.
Mos1297 supersedes573c51cf after one fixture-only repair and17exactreasonassertions: source362ec674c9e7aa8cef0c26f4d7448f459171dbd4, digesta2ac57342d9c59ddaebddade0fa475a9568719fed50c132bd7bb141167c7e72e. Onlycase1testchanges. repair-1297/ is the new review packet. Tests unrun; Duckworthreview and fullfreshgatepending.
mos-1298 candidate source 14e46006171ef700a72f971fda5cb3a0b8dcc4f2, seven-path manifest 591020a1096a1606c11f2ee34b37243acddf83d91c69922ccc5cfe22d6180d3a. Only case2-unit.test.mjs changes among manifest paths from e1bc766d8. Pending review and fresh full gate; no carry.
@@ -1,7 +0,0 @@
f8ad28aa8dc0f12b2c6dddb3b5dc805da05a97c7723e0525905456c064d0cfa3 docs/plans/DEFERRED.md
b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008 packages/cli/tests/case-sudo-fixture.mjs
27378eb1297dc6115b4b1ce849de6ffdb3adccf5249783106fa0570c9d696e8a packages/cli/tests/case1-accept.test.mjs
9ca67403d8614266061624ab5fbf4ea8e8a54cbd08f5b569c95592ab5ed40472 packages/cli/tests/case2-unit.test.mjs
ce9054060867120798c6b26737ac7d910d8cfbc7aa4ad614f4295619c7605e46 packages/webui/README.md
c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd scripts/case1-accept.sh
d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b scripts/case2-unit.sh
-37
View File
@@ -1,37 +0,0 @@
# Row253 implementation plan
Goal: migrate the two host scripts to the private tester socket without widening
their unit or root execution boundary. Spec: DESIGN.md, approved by
duckworth-0100 with C1-C5; Sage2173 approves the two design decisions.
Execution: Gander implements inline; Duckworth reviews the final manifest.
Constraints: four product/test files only; no edits to the brief's legacy paths;
no tests before Sage's slot, including patterns. Static inspection precedes the
full slot request. All tests use the existing scratch/fake seams.
- [x] Task1: write factory/acceptance socket fixture and refusal tests first.
Add config/marker and isolated probe fakes; assert target-uid argv, strict
parsing, all C1 path variants and C4 descendant cleanup. Preserve baseline
functional/P checks while changing only the intended socket outcomes.
- [x] Task2: implement the marker parsing, bounded target-uid query, strict bash
parser, component/no-follow location checks and parent preflight in installer.
Preserve hardening and update its header, including sudo dependency.
- [x] Task3: migrate saved args and P8/P10. Implement metadata snapshots and the
bounded isolated system-Python peer observation; keep P10 unnamed with explicit
observed/failed evidence and same-uid residual. Update header and remove the
obsolete accepted-mos-1101 mark. Keep F11 default DATA_ROOT behavior unchanged.
- [x] Task4: static audit every remaining retired-port occurrence; prepare unique
M1-M33 including C1 middle-symlink/sibling-prefix, C2 character, C3 parser and C4
process-group checks. Request the slot once the candidate static check passes.
- [ ] Task5: on Sage's grant, execute RED cases against isolated prior behavior,
GREEN candidate checks, CLI/WebUI whole, all shell files and the checks. Record
counts/times, clean restoration, leak sweep and exact frozen manifest. Return
the slot, then queue review only in a coordinated write gap.
Review focus: prefix collisions; intermediate symlinks; forged output lines;
nonzero query with valid-looking JSON; ignored signals/leftover descendants;
kernel versus filesystem inode; wrong Console peer or metadata replacement.
Ruling: tests are written first, but their execution waits for the user-required
slot after static preparation. No pre-slot test or passing claim; RED/GREEN
execution is an explicit part of the granted verification, not claimed in advance.
@@ -1,768 +0,0 @@
{
"command": "git grep -n -- --tester-port scripts packages",
"hits": 127,
"entries": [
{
"file": "packages/cli/tests/case1-accept.test.mjs",
"line": 596,
"text": " \"retired TCP\": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\\n')+'\\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],",
"reason": "New negative saved-argument fixture asserting the migration refusal."
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 68,
"text": " \"[Service]\\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\\n\");",
"reason": "Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged."
},
{
"file": "packages/cli/tests/case1-hostcheck.test.mjs",
"line": 215,
"text": " put(join(h.root, \"etc\", \"systemd\", \"system\", \"mosaic-webui.service\"), \"[Service]\\nExecStart=/x serve --business mosaic-stack --tester-port 7333\\n\");",
"reason": "Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged."
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 445,
"text": " [\"--tester-port\", \"7333\", /case2-unit.sh retires tester TCP; use --tester-socket/],",
"reason": "New negative factory/library fixtures asserting the migration refusal."
},
{
"file": "packages/cli/tests/case2-unit.test.mjs",
"line": 496,
"text": " refuses(unit(h, { profile: \"factory\", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);",
"reason": "New negative factory/library fixtures asserting the migration refusal."
},
{
"file": "packages/cli/tests/stack-release-external.test.mjs",
"line": 75,
"text": " for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){",
"reason": "Legacy stack-release external refusal fixture, excluded by brief step 5."
},
{
"file": "packages/cli/tests/stack-release-fixture.mjs",
"line": 306,
"text": " for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\\n// serve accepts '--tester-port'\\n// serve accepts '--proof-header' (row 178)\\n${[C1,C].includes(id)?\"// serve accepts '--sso' (row 192)\\n\":''}`:'// fixture Console before tester listener, never run\\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){",
"reason": "Retained pre-196 copy fixture for legacy activation and rollback."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 681,
"text": "const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 687,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 688,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 690,
"text": " r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 692,
"text": " assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 693,
"text": " assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 694,
"text": " r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 697,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\\n|$)/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 702,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 705,
"text": " assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 708,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 709,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 714,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 718,
"text": " assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 724,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 731,
"text": " let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 732,
"text": " r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\\n]*--tester-port 7333/i);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 736,
"text": " edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 738,
"text": " edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 740,
"text": " assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 741,
"text": " assert.match(stepText(e),/drop[^\\n]*--tester-port 7333/i);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 746,
"text": " let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 749,
"text": " assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\\n|$)/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 821,
"text": " const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 827,
"text": " const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 830,
"text": " const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 839,
"text": " const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 843,
"text": " await t.test('a target Console without --tester-port',async(t)=>{",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 846,
"text": " const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 848,
"text": " assert.deepEqual(r.out.split('\\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 850,
"text": " await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1056,
"text": " const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1057,
"text": " assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1058,
"text": " assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1129,
"text": " await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1132,
"text": " let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1135,
"text": " assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1137,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1138,
"text": " assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1141,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1162,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1351,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1352,
"text": " assert.deepEqual(want.slice(-2),['--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1367,
"text": " await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1468,
"text": "const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1480,
"text": " let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1481,
"text": " assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1483,
"text": " const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1486,
"text": " `sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1563,
"text": " const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1564,
"text": " assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1584,
"text": " const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1594,
"text": " const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1682,
"text": " assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 1811,
"text": " writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme','--tester-port',TESTER],schema:store,activation:'manual',at:'2026-10-20T00:00:00Z'})+'\\n');",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2198,
"text": " ['beta\\'s tester port on alpha\\'s port',b,['--port',free,'--tester-port',a.port],1,`webui-service: refusing port-claimed: ${held}; nothing was changed\\n`],",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2202,
"text": " ['beta\\'s tester port on 7333',b,['--port',free,'--tester-port','7333'],4,'webui-service: port-reserved: 7333 is reserved for the default instance (7330 7332 7333)\\n'],",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2217,
"text": " await refused('--tester-port','--tester-port',a.port);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2280,
"text": " let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2290,
"text": " r=await run(e,'activate',C1,'acme','--tester-socket','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/--tester-socket/);assert.match(r.out,/drop.*--tester-port 7333/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2292,
"text": " assert.deepEqual(pointer(e).console,[...base,...pair(proof),'--tester-socket']);assert.match(consoleText(e),/--tester-socket/);assert.doesNotMatch(consoleText(e),/--tester-port/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2319,
"text": " assert.doesNotMatch(conExec(e),/ --send(?: |$)|--tester-port|--public-host|--proof-header/);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/cli/tests/stack-release.test.mjs",
"line": 2388,
"text": " assert.equal(context.argv.includes('--tester-socket'),marked);assert.equal(context.argv.includes('--tester-port'),false);",
"reason": "Legacy release compatibility, rollback, pointer and migration assertions; brief step 5."
},
{
"file": "packages/webui/README.md",
"line": 1001,
"text": "There is no tester TCP listener. `--tester-port` is retired for this copy.",
"reason": "Current retirement notice explicitly states no tester TCP listener."
},
{
"file": "packages/webui/src/cli.mjs",
"line": 30,
"text": " if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket');",
"reason": "Current CLI explicit retirement refusal, not an activation path."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 125,
"text": " const r = await run(dir, ['render', '--tester-port', port, ...PREVIEW, '--send']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 127,
"text": " assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 129,
"text": " assert.ok(execStart(r.stdout).endsWith(` ${PREVIEW_FLAGS} --send --tester-port ${port}`));",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 136,
"text": " const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 150,
"text": " ['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 151,
"text": " ['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 152,
"text": " ['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\\n'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 153,
"text": " ['--port', '7444', '--tester-port', '7444'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 154,
"text": " ['--tester-port', '7444', '--port', '7444'],",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 164,
"text": " assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 172,
"text": " const six = await run(dir, ['render', '--tester-port', '007333']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 175,
"text": " assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\\n');",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 176,
"text": " const five = await run(dir, ['render', '--tester-port', '07333']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 178,
"text": " assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 186,
"text": " const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...PREVIEW, '--send'], bin);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 188,
"text": " assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 189,
"text": " assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 199,
"text": " const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 799,
"text": " const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send', '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/service.test.mjs",
"line": 813,
"text": " const both=await run(dir,['render','--tester-socket','--tester-port','7333']);",
"reason": "Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5."
},
{
"file": "packages/webui/tests/tester-cli.test.mjs",
"line": 25,
"text": " for (const flags of [['--tester-port', '7332'], ['--port', '7444', '--tester-port', '7444'], ['--tester-port', '7444', '--port', '7444'], ...['1023', '65536', '1.5', '-1', 'nope'].map(p => ['--tester-port', p])]) {",
"reason": "Explicit current-CLI negative calls; brief step 5."
},
{
"file": "scripts/case1-accept.sh",
"line": 71,
"text": " --tester-port) refuse \"case1-accept.sh retires tester TCP; use --tester-socket\" ;;",
"reason": "New explicit migration refusal for saved old arguments."
},
{
"file": "scripts/case2-unit.sh",
"line": 215,
"text": " --tester-port) die \"case2-unit.sh retires tester TCP; use --tester-socket\" ;;",
"reason": "New explicit migration refusal before rendering units."
},
{
"file": "scripts/instance.sh",
"line": 114,
"text": "# this instance's, whose ExecStart takes PORT as --port or --tester-port.",
"reason": "Legacy installed-unit port inventory/parser retained by brief step 5."
},
{
"file": "scripts/instance.sh",
"line": 132,
"text": " --tester-port) [[ \"$tok\" =~ ^[0-9]{1,5}$ ]] && ports+=(\"$((10#$tok))\") ;;",
"reason": "Legacy installed-unit port inventory/parser retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 47,
"text": " stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 63,
"text": "activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 126,
"text": " --tester-port) [ \"$OP\" = activate ] && [ $# -ge 2 ] && [ -n \"$2\" ] || usage; TESTER_PORT=\"$2\"; shift 2 ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 797,
"text": " elif grep -q -- '--tester-port' \"$cli\"; then supported=1; fi",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 800,
"text": " --tester-port) i=$((i + 1)); inherited=\"${BASE_CONSOLE[i]:-}\" ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 814,
"text": " [ -z \"$inherited\" ] || DROP_TESTER=\"--tester-port $inherited (replaced by private Unix socket)\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 830,
"text": " check_refuse tester-port-invalid \"--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 832,
"text": " [ \"$supported\" = 1 ] || check_refuse tester-port-target \"$TO's Console does not support --tester-port\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 833,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\" --tester-port \"$TESTER_PORT\")",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 837,
"text": " RUN_CONSOLE=(\"${without_tester[@]}\") DROP_TESTER=\"--tester-port $inherited\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 878,
"text": " # --send and --tester-port follow the pointer, so a --no-send or a tester port set after that",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 885,
"text": " --tester-port) i=$((i + 1)) ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 893,
"text": " --tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port \"${P_CONSOLE[i]:-}\") ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 897,
"text": " SSO_PLAN=\"sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them\"",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 903,
"text": "# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP.",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 919,
"text": " case \"${kept[i]}\" in --send | --tester-port | --tester-socket) break ;; esac",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 1076,
"text": " case \"${without_proof[i]}\" in --send | --tester-port | --tester-socket) break ;; esac",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 1134,
"text": "# Row 190: the target Console's --port (7332 when absent) and --tester-port against every other",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 1142,
"text": " --tester-port) ports+=(\"${RUN_CONSOLE[i + 1]:-}\") ;;",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/stack-release.sh",
"line": 2106,
"text": " for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do [ \"${P_CONSOLE[i]}\" != --tester-port ] || reserved+=(\"${P_CONSOLE[i + 1]:-}\"); done",
"reason": "Legacy-copy activation/rollback, port inventory and migration retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 43,
"text": "# Console), --tester-socket (private Unix tester), or legacy --tester-port N",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 74,
"text": "# --tester-port that another installed Console unit in the unit directory",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 96,
"text": " FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH]",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 97,
"text": " --tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener.",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 137,
"text": " --tester-port) [ $# -ge 2 ] || usage; TESTER_PORT=\"$2\"; TESTER_SET=1; SERVE_SET=1; shift 2 ;;",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 182,
"text": " echo \"webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT\" >&2",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 266,
"text": " [ \"$TESTER_SET\" = 0 ] || flags=\"$flags --tester-port $TESTER_PORT\"",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 462,
"text": " if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' \"$target\"; then",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 467,
"text": " if grep -Eq '^ExecStart=.* --sso( --send)?( --tester-port [0-9]+)?$' \"$target\"; then",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
},
{
"file": "scripts/webui-service.sh",
"line": 471,
"text": " tester_port=\"$(sed -n 's/^ExecStart=.* --tester-port \\([0-9][0-9]*\\)$/\\1/p' \"$target\")\"",
"reason": "Legacy unit renderer/parser and port inventory retained by brief step 5."
}
]
}
-131
View File
@@ -1,131 +0,0 @@
# Row253 retired-port audit
Current static source audit after the two approved fixture fixes. Each remaining hit and its reason follows.
- `packages/cli/tests/case1-accept.test.mjs:596`: New negative saved-argument fixture asserting the migration refusal. Text: `"retired TCP": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\n')+'\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],`
- `packages/cli/tests/case1-hostcheck.test.mjs:68`: Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged. Text: `"[Service]\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\n");`
- `packages/cli/tests/case1-hostcheck.test.mjs:215`: Deliberate legacy hostcheck fixture; brief step 5 leaves it unchanged. Text: `put(join(h.root, "etc", "systemd", "system", "mosaic-webui.service"), "[Service]\nExecStart=/x serve --business mosaic-stack --tester-port 7333\n");`
- `packages/cli/tests/case2-unit.test.mjs:445`: New negative factory/library fixtures asserting the migration refusal. Text: `["--tester-port", "7333", /case2-unit.sh retires tester TCP; use --tester-socket/],`
- `packages/cli/tests/case2-unit.test.mjs:496`: New negative factory/library fixtures asserting the migration refusal. Text: `refuses(unit(h, { profile: "factory", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);`
- `packages/cli/tests/stack-release-external.test.mjs:75`: Legacy stack-release external refusal fixture, excluded by brief step 5. Text: `for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){`
- `packages/cli/tests/stack-release-fixture.mjs:306`: Retained pre-196 copy fixture for legacy activation and rollback. Text: `for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\n// serve accepts '--tester-port'\n// serve accepts '--proof-header' (row 178)\n${[C1,C].includes(id)?"// serve accepts '--sso' (row 192)\n":''}`:'// fixture Console before tester listener, never run\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){`
- `packages/cli/tests/stack-release.test.mjs:681`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];`
- `packages/cli/tests/stack-release.test.mjs:687`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:688`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:690`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:692`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);`
- `packages/cli/tests/stack-release.test.mjs:693`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);`
- `packages/cli/tests/stack-release.test.mjs:694`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);`
- `packages/cli/tests/stack-release.test.mjs:697`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:702`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:705`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(r.out,/drop[^\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:708`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:709`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:714`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:718`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:724`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);`
- `packages/cli/tests/stack-release.test.mjs:731`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:732`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:736`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;`
- `packages/cli/tests/stack-release.test.mjs:738`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');`
- `packages/cli/tests/stack-release.test.mjs:740`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:741`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);`
- `packages/cli/tests/stack-release.test.mjs:746`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:749`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);`
- `packages/cli/tests/stack-release.test.mjs:821`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;`
- `packages/cli/tests/stack-release.test.mjs:827`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);`
- `packages/cli/tests/stack-release.test.mjs:830`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);`
- `packages/cli/tests/stack-release.test.mjs:839`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');`
- `packages/cli/tests/stack-release.test.mjs:843`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await t.test('a target Console without --tester-port',async(t)=>{`
- `packages/cli/tests/stack-release.test.mjs:846`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');`
- `packages/cli/tests/stack-release.test.mjs:848`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(r.out.split('\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);`
- `packages/cli/tests/stack-release.test.mjs:850`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);`
- `packages/cli/tests/stack-release.test.mjs:1056`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1057`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1058`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));`
- `packages/cli/tests/stack-release.test.mjs:1129`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{`
- `packages/cli/tests/stack-release.test.mjs:1132`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1135`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));`
- `packages/cli/tests/stack-release.test.mjs:1137`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1138`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));`
- `packages/cli/tests/stack-release.test.mjs:1141`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1162`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);`
- `packages/cli/tests/stack-release.test.mjs:1351`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);`
- `packages/cli/tests/stack-release.test.mjs:1352`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(want.slice(-2),['--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1367`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);`
- `packages/cli/tests/stack-release.test.mjs:1468`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;`
- `packages/cli/tests/stack-release.test.mjs:1480`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:1481`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);`
- `packages/cli/tests/stack-release.test.mjs:1483`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];`
- `packages/cli/tests/stack-release.test.mjs:1486`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: ``sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);`
- `packages/cli/tests/stack-release.test.mjs:1563`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');`
- `packages/cli/tests/stack-release.test.mjs:1564`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);`
- `packages/cli/tests/stack-release.test.mjs:1584`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);`
- `packages/cli/tests/stack-release.test.mjs:1594`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];`
- `packages/cli/tests/stack-release.test.mjs:1682`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]);`
- `packages/cli/tests/stack-release.test.mjs:1811`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme','--tester-port',TESTER],schema:store,activation:'manual',at:'2026-10-20T00:00:00Z'})+'\n');`
- `packages/cli/tests/stack-release.test.mjs:2198`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `['beta\'s tester port on alpha\'s port',b,['--port',free,'--tester-port',a.port],1,`webui-service: refusing port-claimed: ${held}; nothing was changed\n`],`
- `packages/cli/tests/stack-release.test.mjs:2202`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `['beta\'s tester port on 7333',b,['--port',free,'--tester-port','7333'],4,'webui-service: port-reserved: 7333 is reserved for the default instance (7330 7332 7333)\n'],`
- `packages/cli/tests/stack-release.test.mjs:2217`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `await refused('--tester-port','--tester-port',a.port);`
- `packages/cli/tests/stack-release.test.mjs:2280`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);`
- `packages/cli/tests/stack-release.test.mjs:2290`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `r=await run(e,'activate',C1,'acme','--tester-socket','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/--tester-socket/);assert.match(r.out,/drop.*--tester-port 7333/);`
- `packages/cli/tests/stack-release.test.mjs:2292`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.deepEqual(pointer(e).console,[...base,...pair(proof),'--tester-socket']);assert.match(consoleText(e),/--tester-socket/);assert.doesNotMatch(consoleText(e),/--tester-port/);`
- `packages/cli/tests/stack-release.test.mjs:2319`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.doesNotMatch(conExec(e),/ --send(?: |$)|--tester-port|--public-host|--proof-header/);`
- `packages/cli/tests/stack-release.test.mjs:2388`: Legacy release compatibility, rollback, pointer and migration assertions; brief step 5. Text: `assert.equal(context.argv.includes('--tester-socket'),marked);assert.equal(context.argv.includes('--tester-port'),false);`
- `packages/webui/README.md:1001`: Current retirement notice explicitly states no tester TCP listener. Text: `There is no tester TCP listener. `--tester-port` is retired for this copy.`
- `packages/webui/src/cli.mjs:30`: Current CLI explicit retirement refusal, not an activation path. Text: `if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket');`
- `packages/webui/tests/service.test.mjs:125`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['render', '--tester-port', port, ...PREVIEW, '--send']);`
- `packages/webui/tests/service.test.mjs:127`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));`
- `packages/webui/tests/service.test.mjs:129`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.ok(execStart(r.stdout).endsWith(` ${PREVIEW_FLAGS} --send --tester-port ${port}`));`
- `packages/webui/tests/service.test.mjs:136`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);`
- `packages/webui/tests/service.test.mjs:150`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],`
- `packages/webui/tests/service.test.mjs:151`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],`
- `packages/webui/tests/service.test.mjs:152`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\n'],`
- `packages/webui/tests/service.test.mjs:153`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--port', '7444', '--tester-port', '7444'],`
- `packages/webui/tests/service.test.mjs:154`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `['--tester-port', '7444', '--port', '7444'],`
- `packages/webui/tests/service.test.mjs:164`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);`
- `packages/webui/tests/service.test.mjs:172`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const six = await run(dir, ['render', '--tester-port', '007333']);`
- `packages/webui/tests/service.test.mjs:175`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\n');`
- `packages/webui/tests/service.test.mjs:176`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const five = await run(dir, ['render', '--tester-port', '07333']);`
- `packages/webui/tests/service.test.mjs:178`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));`
- `packages/webui/tests/service.test.mjs:186`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...PREVIEW, '--send'], bin);`
- `packages/webui/tests/service.test.mjs:188`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);`
- `packages/webui/tests/service.test.mjs:189`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);`
- `packages/webui/tests/service.test.mjs:199`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);`
- `packages/webui/tests/service.test.mjs:799`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send', '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);`
- `packages/webui/tests/service.test.mjs:813`: Legacy unit renderer/parser compatibility and mixed-flag refusal fixtures; brief step 5. Text: `const both=await run(dir,['render','--tester-socket','--tester-port','7333']);`
- `packages/webui/tests/tester-cli.test.mjs:25`: Explicit current-CLI negative calls; brief step 5. Text: `for (const flags of [['--tester-port', '7332'], ['--port', '7444', '--tester-port', '7444'], ['--tester-port', '7444', '--port', '7444'], ...['1023', '65536', '1.5', '-1', 'nope'].map(p => ['--tester-port', p])]) {`
- `scripts/case1-accept.sh:71`: New explicit migration refusal for saved old arguments. Text: `--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;`
- `scripts/case2-unit.sh:215`: New explicit migration refusal before rendering units. Text: `--tester-port) die "case2-unit.sh retires tester TCP; use --tester-socket" ;;`
- `scripts/instance.sh:114`: Legacy installed-unit port inventory/parser retained by brief step 5. Text: `# this instance's, whose ExecStart takes PORT as --port or --tester-port.`
- `scripts/instance.sh:132`: Legacy installed-unit port inventory/parser retained by brief step 5. Text: `--tester-port) [[ "$tok" =~ ^[0-9]{1,5}$ ]] && ports+=("$((10#$tok))") ;;`
- `scripts/stack-release.sh:47`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]`
- `scripts/stack-release.sh:63`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the`
- `scripts/stack-release.sh:126`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; TESTER_PORT="$2"; shift 2 ;;`
- `scripts/stack-release.sh:797`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `elif grep -q -- '--tester-port' "$cli"; then supported=1; fi`
- `scripts/stack-release.sh:800`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)); inherited="${BASE_CONSOLE[i]:-}" ;;`
- `scripts/stack-release.sh:814`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `[ -z "$inherited" ] || DROP_TESTER="--tester-port $inherited (replaced by private Unix socket)"`
- `scripts/stack-release.sh:830`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `check_refuse tester-port-invalid "--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other"`
- `scripts/stack-release.sh:832`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `[ "$supported" = 1 ] || check_refuse tester-port-target "$TO's Console does not support --tester-port"`
- `scripts/stack-release.sh:833`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `RUN_CONSOLE=("${without_tester[@]}" --tester-port "$TESTER_PORT")`
- `scripts/stack-release.sh:837`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `RUN_CONSOLE=("${without_tester[@]}") DROP_TESTER="--tester-port $inherited"`
- `scripts/stack-release.sh:878`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# --send and --tester-port follow the pointer, so a --no-send or a tester port set after that`
- `scripts/stack-release.sh:885`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)) ;;`
- `scripts/stack-release.sh:893`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port "${P_CONSOLE[i]:-}") ;;`
- `scripts/stack-release.sh:897`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `SSO_PLAN="sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them"`
- `scripts/stack-release.sh:903`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP.`
- `scripts/stack-release.sh:919`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `case "${kept[i]}" in --send | --tester-port | --tester-socket) break ;; esac`
- `scripts/stack-release.sh:1076`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `case "${without_proof[i]}" in --send | --tester-port | --tester-socket) break ;; esac`
- `scripts/stack-release.sh:1134`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `# Row 190: the target Console's --port (7332 when absent) and --tester-port against every other`
- `scripts/stack-release.sh:1142`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `--tester-port) ports+=("${RUN_CONSOLE[i + 1]:-}") ;;`
- `scripts/stack-release.sh:2106`: Legacy-copy activation/rollback, port inventory and migration retained by brief step 5. Text: `for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do [ "${P_CONSOLE[i]}" != --tester-port ] || reserved+=("${P_CONSOLE[i + 1]:-}"); done`
- `scripts/webui-service.sh:43`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `# Console), --tester-socket (private Unix tester), or legacy --tester-port N`
- `scripts/webui-service.sh:74`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `# --tester-port that another installed Console unit in the unit directory`
- `scripts/webui-service.sh:96`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH]`
- `scripts/webui-service.sh:97`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `--tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener.`
- `scripts/webui-service.sh:137`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `--tester-port) [ $# -ge 2 ] || usage; TESTER_PORT="$2"; TESTER_SET=1; SERVE_SET=1; shift 2 ;;`
- `scripts/webui-service.sh:182`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `echo "webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT" >&2`
- `scripts/webui-service.sh:266`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `[ "$TESTER_SET" = 0 ] || flags="$flags --tester-port $TESTER_PORT"`
- `scripts/webui-service.sh:462`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' "$target"; then`
- `scripts/webui-service.sh:467`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `if grep -Eq '^ExecStart=.* --sso( --send)?( --tester-port [0-9]+)?$' "$target"; then`
- `scripts/webui-service.sh:471`: Legacy unit renderer/parser and port inventory retained by brief step 5. Text: `tester_port="$(sed -n 's/^ExecStart=.* --tester-port \([0-9][0-9]*\)$/\1/p' "$target")"`
-38
View File
@@ -1,38 +0,0 @@
# Row253 granted gate
Do not launch until Sage's CLEAR names this scope. No tests have run yet.
Use this worktree, the frozen five blobs in CANDIDATE-SHAS.json and the plain
node_modules link. No canonical-checkout node or queue writes during the slot.
Set TMPDIR with `mktemp -d /mnt/storage/scratch/tmp/gXXX`, export it and use
`set -o pipefail`. Record the grant and the exact date before launch. Use logs
under this packet's `run1/`, with the outer log named `controller.log`.
Neither helper nor wrapper argv may match the busy detector's patterns.
Sequence, stopping to inspect every result:
1. `window.py --stage focused`: isolated prior installer and acceptance RED
examples, restoring bytes/status in finally, then both focused test files
whole GREEN. Expected RED is recorded separately from gate failures.
2. `driver.py`: 33 unique single-target changes, named base cases and killer
runs. Each base must pass in under 30 seconds. bash -n is the only invalid
classification. Runtime errors kill a change; unexpected timeouts stop for
Sage/Mos. Every run records time, code, raw tee log and byte/status restore.
3. `window.py --stage whole`: cli whole, webui whole and the ten shell files
whole. No omitted tests, patterns or partial scripts in this phase.
4. Sum counts and seconds from raw logs by script. Sweep process cwd/ancestry
for this worktree's node, tee, query and probe descendants. Report existing
processes separately. Return the slot immediately, then prepare review.
Every driver invocation needs `--grant <Sage-id> --out <record-directory>`.
Before each actual test, driver.run checks Sage's busy guard and launches only
on CLEAR. It invokes a bash pipefail/tee pipeline through launch.py, which
loads the structured command after the guard. No command text is embedded in
wrapper argv. All execution is in a new process group and timed out groups
are killed and waited for. Query-owned groups are independently bounded by
product code; the orphan fixture also cleans its own recorded PID.
Two unexpected failures end the window. These drivers stop at the first
unexpected base, focused or whole-phase failure for inspection, without
automatically rerunning it. Any timeout is a stop. Send a stop to Sage/Mos
and do not turn an ungranted retry into a new run.
@@ -1,132 +0,0 @@
{
"authority": [
"mos-1275",
"sage-2659"
],
"scope": "Own Gander row196 receipt set documented for row278, plus the complete row278 run3 shell phase. Row253 old C4 results do not carry. Receipts choose timeouts only.",
"consulted_records": [
"ec1e7fad8:agents/gander/work/queue-196/evidence/suite-results-run1.json",
"ec1e7fad8:agents/gander/work/queue-196/evidence/suite-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r2/mutants/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r2/run2/base-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r2/run2/killers/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r3/gate/base-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r3/gate/killers/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r3/gate/sso-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/base-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/killers/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/sso-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/whole-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r5/gate/whole-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r5/resume/summary.json",
"ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r6/base-results.json",
"ec1e7fad8:agents/gander/work/queue-196/r6/first-attempt/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r6/killers/results.json",
"ec1e7fad8:agents/gander/work/queue-196/r6/summary.json",
"ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"385a7194c:agents/gander/work/queue-278/run3/whole/results.json"
],
"scripts": {
"scripts/test-auth.sh": {
"recorded_duration_seconds": 1.9081228229999543,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"limit_seconds": 126,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "dd4720cb0cbecd2fc523a318f2222d21d257c8c5889996a3d69e522ee971a707",
"previous_maximum_seconds": 1.9081228229999543,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"row278_run3_seconds": 0.816
},
"scripts/test-conductor.sh": {
"recorded_duration_seconds": 8.057173820000026,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"limit_seconds": 145,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "da84150467494a4c979759f400854411bd93a7efce981139757bf526d688a23b",
"previous_maximum_seconds": 8.057173820000026,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"row278_run3_seconds": 6.078
},
"scripts/test-config.sh": {
"recorded_duration_seconds": 3.1678915339999367,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"limit_seconds": 130,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "3301c1fabf616bb961d62f6decd0a46dadb0f399b8ba7f3c2ca60023d2b24c78",
"previous_maximum_seconds": 3.1678915339999367,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
"row278_run3_seconds": 1.868
},
"scripts/test-discord.sh": {
"recorded_duration_seconds": 8.776727847000002,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 147,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
"previous_maximum_seconds": 8.776727847000002,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 7.431
},
"scripts/test-extension-package.sh": {
"recorded_duration_seconds": 3.01285702399991,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 130,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "5d7b687f16fd1a9414b229501177d530ba3af4eadd3cac319b33f38fdba9bf99",
"previous_maximum_seconds": 3.01285702399991,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 1.366
},
"scripts/test-foundation.sh": {
"recorded_duration_seconds": 39.901,
"receipt": "385a7194c:agents/gander/work/queue-278/run3/whole/results.json",
"limit_seconds": 240,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
"previous_maximum_seconds": 39.70872647900006,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 39.901
},
"scripts/test-land-work.sh": {
"recorded_duration_seconds": 12.623263413000037,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 158,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "c9d1d5c9b006d5c12e5f796a9828361059f8e0410846fc55c070c0148b15cef4",
"previous_maximum_seconds": 12.623263413000037,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 11.338
},
"scripts/test-queue.sh": {
"recorded_duration_seconds": 23.443742031000088,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 191,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
"previous_maximum_seconds": 23.443742031000088,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 21.158
},
"scripts/test-release.sh": {
"recorded_duration_seconds": 12.168674453999964,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 157,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "be706dff832257bd4781abeabd19faa4ac16edf9a4af0c96457e33cf3bb6deb3",
"previous_maximum_seconds": 12.168674453999964,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 2.67
},
"scripts/test-task.sh": {
"recorded_duration_seconds": 36.783798266,
"receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"limit_seconds": 231,
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
"script_sha256": "10c4abb70bf9b83e617089b06a5436cc7fa6b866a176458b22128c3a15163f94",
"previous_maximum_seconds": 36.783798266,
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
"row278_run3_seconds": 13.843
}
}
}
-50
View File
@@ -1,50 +0,0 @@
{
"at": "2026-10-21T21:52:50.545130+00:00",
"behavioral_tests_run": false,
"checks": [
{
"command": [
"bash",
"-n",
"scripts/case1-accept.sh",
"scripts/case2-unit.sh"
],
"code": 0,
"stdout": "",
"stderr": ""
},
{
"command": [
"node",
"--check",
"packages/cli/tests/case1-accept.test.mjs"
],
"code": 0,
"stdout": "",
"stderr": ""
},
{
"command": [
"node",
"--check",
"packages/cli/tests/case2-unit.test.mjs"
],
"code": 0,
"stdout": "",
"stderr": ""
},
{
"command": [
"git",
"diff",
"--check"
],
"code": 0,
"stdout": "",
"stderr": ""
}
],
"retired_port_hits": 127,
"unique_changes": 33,
"slot": "Requested gander-0071; queued by sage-2191; no CLEAR"
}
@@ -1,233 +0,0 @@
[
{
"id": "M1",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "c25003c6fea2c7693d6b3e38e1986b8397886a8a47b5c6d7c0ddb535e676171b"
},
{
"id": "M2",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "977ef3f63de87cd260bc4fa44c2695b56880066d722681fa5d9e0f45786474f3"
},
{
"id": "M3",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "9dca577302011b11fc0459517420a4862dedb792e11eba6e06a975b9978c2db9"
},
{
"id": "M4",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "538ec4d9aaa942052b62628040445103128b8894ba5b2dafdf5d6c9a52b3b4d7"
},
{
"id": "M5",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "6632ebedee012f299ff5b5b36eaa2fd7c0bcae371b8f7b11c1bfd28bce0dde80"
},
{
"id": "M6",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "4f9cfb1b87ab33c39fe2ed77286296ad1047581afca8138722ddad6dddc938d8"
},
{
"id": "M7",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "00fa60f1d341ee67622d1f89be9c5062ebe45af3c49cc82ae004670f653097a3"
},
{
"id": "M8",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "92d8b45baf861d0a12327897d6fe09f87db867b1d875d39e9769fa46242826ea"
},
{
"id": "M9",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "3ba9e143b76ab0c37c575f8a446acf69e611e604074296a176bef1b5b84aa6ea"
},
{
"id": "M10",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "6eacec588684a0ccc0a1cc009f5ec0f27b1f38341f1fb72fec37c710de38b2a9"
},
{
"id": "M11",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "dc4c4d3d4761cc8976c7b813bdcc9dd9c56fc35cf03aadf3a19d4cfec6a32233"
},
{
"id": "M12",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "6ec5c79f0a0d19f5d642dffc79a7e39815f907071fae14df0cfd66b64ebaf221"
},
{
"id": "M13",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "2af786adc1269fde529b68b7dc121bead9c8b82e2615b4dbb18207aab60e72c8"
},
{
"id": "M14",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "f3411f4887e009e1b8cc38664be6eb5e1a34379472311284b726466171572758"
},
{
"id": "M15",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "148f30346fcbfd473ca0b4a717846a2d9743af9a046d5cc009271392d958d812"
},
{
"id": "M16",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "ec27d778b49a8e5ff0b356e6b27973db8eb97a078a571cf9720ab43f15781b01"
},
{
"id": "M17",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "4c7cd4c62d2137626158091c478d20e8ace5617c5e96b258478317e57bc808f3"
},
{
"id": "M18",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "4372a2e392054f80d491fc04b328303339a57cd4c72030abf3ccdae1e0dfb024"
},
{
"id": "M19",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "766524ec13b960bace472671aa62cfb3ea69f6bdb31d27c2427758e8d8ea53ae"
},
{
"id": "M20",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "4e653a809eaa504024d57c0af132222a374c0c26313942f6f0a68b3e8d099632"
},
{
"id": "M21",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "1067c1400870258f663e7e6bdb911e9623a9c666765e86cc7ec0bda4131852f3"
},
{
"id": "M22",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "f19cc06bf32c491bdfbea56888353fcc985e10298d8416892993e0757c429421"
},
{
"id": "M23",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "f1c7326e60d5ed26d55f43107939d775d6c81dfc45398a36274bac380f95e9b3"
},
{
"id": "M24",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "be8c6cb451c4d30b621c9eb38cf2c63629bd8ae5b6d30d5d91d365e4911ca964"
},
{
"id": "M25",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "53ee61ac53156707aa11bbad35b41a0bf21f0093a5fb6d03aff215940ed023ad"
},
{
"id": "M26",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "fc9cbfc3eaa0df6adb84b67dc9cf4fb9f773b89c16597703dbebc03559977b5f"
},
{
"id": "M27",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"after": "e727c63e45bb5a3a767c5e60870658d2a8d1cc970bfa3d6c2b8f439cb27fcbda"
},
{
"id": "M28",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "238b6bc3b2feb64de614aa2e9471dcc58867a3e44ebfad7130917d43101fd6bd"
},
{
"id": "M29",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "f7c5e06f46aea4d5c3fde288eebd732a3265583faef2d38c2b293a7364e299b7"
},
{
"id": "M30",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "d6ceb8529fa508eb48534000666f02fd4bf949dbeeeff03490c51c8d1e4ecf1f"
},
{
"id": "M31",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "c028760cf45fbae6b127b203f32ce208eeb77ea9dd646fee5af9e6e58661a6ec"
},
{
"id": "M32",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "f047a7024b67c1f7a7af503c3cb525b07c83a0e413b845818d3ea9cd0e6f9ea9"
},
{
"id": "M33",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"after": "a197452abed015e81d0620730b95292a89cff84f8f52ea679111b0b71b17ae1d"
}
]
@@ -1,142 +0,0 @@
{
"authority": "sage-2737/sage-2739",
"files": {
"packages/cli/tests/access.test.mjs": 900,
"packages/cli/tests/app.test.mjs": 900,
"packages/cli/tests/boot-status.test.mjs": 900,
"packages/cli/tests/case1-accept.test.mjs": 418,
"packages/cli/tests/case1-hostcheck.test.mjs": 900,
"packages/cli/tests/case2-persona-import.test.mjs": 900,
"packages/cli/tests/case2-unit.test.mjs": 320,
"packages/cli/tests/channel-cli.test.mjs": 900,
"packages/cli/tests/channel-recovery.test.mjs": 900,
"packages/cli/tests/cli.test.mjs": 900,
"packages/cli/tests/config.test.mjs": 900,
"packages/cli/tests/credentials.test.mjs": 900,
"packages/cli/tests/follow.test.mjs": 900,
"packages/cli/tests/format.test.mjs": 900,
"packages/cli/tests/gate-lane-auth.test.mjs": 900,
"packages/cli/tests/gate-lane-fixtures.test.mjs": 900,
"packages/cli/tests/gate-lane-image.test.mjs": 900,
"packages/cli/tests/gate-lane-state.test.mjs": 900,
"packages/cli/tests/gate-lane.test.mjs": 900,
"packages/cli/tests/git-socket.test.mjs": 900,
"packages/cli/tests/grants.test.mjs": 900,
"packages/cli/tests/host.test.mjs": 900,
"packages/cli/tests/hosts.test.mjs": 900,
"packages/cli/tests/instances.test.mjs": 900,
"packages/cli/tests/launch-account.test.mjs": 900,
"packages/cli/tests/launch-limit-checks.test.mjs": 900,
"packages/cli/tests/launch-limits.test.mjs": 900,
"packages/cli/tests/launch-routes.test.mjs": 900,
"packages/cli/tests/launcher.test.mjs": 900,
"packages/cli/tests/lifecycle.test.mjs": 160,
"packages/cli/tests/limits-tokens.test.mjs": 900,
"packages/cli/tests/limits.test.mjs": 900,
"packages/cli/tests/mark-read.test.mjs": 900,
"packages/cli/tests/names.test.mjs": 900,
"packages/cli/tests/notifier.test.mjs": 900,
"packages/cli/tests/other-tree.test.mjs": 900,
"packages/cli/tests/processing-cli.test.mjs": 900,
"packages/cli/tests/project-name-repo.test.mjs": 900,
"packages/cli/tests/project.test.mjs": 900,
"packages/cli/tests/publish-socket.test.mjs": 900,
"packages/cli/tests/stack-copy.test.mjs": 900,
"packages/cli/tests/stack-instances.test.mjs": 900,
"packages/cli/tests/stack-release-external.test.mjs": 900,
"packages/cli/tests/stack-release.test.mjs": 1871,
"packages/cli/tests/stack-upgrade.test.mjs": 900,
"packages/cli/tests/tester-http.test.mjs": 900,
"packages/cli/tests/tester-reads.test.mjs": 900,
"packages/cli/tests/tester-threads.test.mjs": 900,
"packages/cli/tests/tester.test.mjs": 900,
"packages/cli/tests/thread-cli.test.mjs": 126,
"packages/cli/tests/thread-launch.test.mjs": 170,
"packages/cli/tests/trackers-boot.test.mjs": 900,
"packages/cli/tests/transport.test.mjs": 900,
"packages/cli/tests/usage.test.mjs": 900,
"packages/cli/tests/verbs.test.mjs": 900,
"packages/webui/tests/agents-browser.test.mjs": 900,
"packages/webui/tests/agents.test.mjs": 900,
"packages/webui/tests/assistant.test.mjs": 900,
"packages/webui/tests/board-held.test.mjs": 900,
"packages/webui/tests/browser-edge.test.mjs": 900,
"packages/webui/tests/browser.test.mjs": 900,
"packages/webui/tests/bus-browser.test.mjs": 900,
"packages/webui/tests/bus.test.mjs": 900,
"packages/webui/tests/channel-chat.test.mjs": 900,
"packages/webui/tests/chat-browser.test.mjs": 900,
"packages/webui/tests/chat.test.mjs": 122,
"packages/webui/tests/conversation.test.mjs": 900,
"packages/webui/tests/discord.test.mjs": 900,
"packages/webui/tests/docs-browser.test.mjs": 900,
"packages/webui/tests/editor-browser.test.mjs": 900,
"packages/webui/tests/editor-provenance.test.mjs": 900,
"packages/webui/tests/editor.test.mjs": 900,
"packages/webui/tests/history-return-flow.test.mjs": 900,
"packages/webui/tests/instances-browser.test.mjs": 900,
"packages/webui/tests/instances.test.mjs": 900,
"packages/webui/tests/library-cli.test.mjs": 900,
"packages/webui/tests/library-status.test.mjs": 900,
"packages/webui/tests/limits-browser.test.mjs": 900,
"packages/webui/tests/limits-route.test.mjs": 900,
"packages/webui/tests/markdown-preview.test.mjs": 900,
"packages/webui/tests/messages.test.mjs": 900,
"packages/webui/tests/pinned-copy.test.mjs": 900,
"packages/webui/tests/preview.test.mjs": 900,
"packages/webui/tests/processing-browser.test.mjs": 900,
"packages/webui/tests/processing-ui.test.mjs": 900,
"packages/webui/tests/processing.test.mjs": 900,
"packages/webui/tests/project-file.test.mjs": 900,
"packages/webui/tests/project-name-repo.test.mjs": 900,
"packages/webui/tests/proof-cli.test.mjs": 900,
"packages/webui/tests/proof.test.mjs": 900,
"packages/webui/tests/reads.test.mjs": 900,
"packages/webui/tests/relaunch.test.mjs": 900,
"packages/webui/tests/return-flow.test.mjs": 900,
"packages/webui/tests/routes-browser.test.mjs": 900,
"packages/webui/tests/routes.test.mjs": 900,
"packages/webui/tests/send-browser.test.mjs": 900,
"packages/webui/tests/send.test.mjs": 900,
"packages/webui/tests/serve.test.mjs": 900,
"packages/webui/tests/service.test.mjs": 900,
"packages/webui/tests/settings-form.test.mjs": 900,
"packages/webui/tests/settings-write.test.mjs": 900,
"packages/webui/tests/shell.test.mjs": 900,
"packages/webui/tests/sso-browser.test.mjs": 900,
"packages/webui/tests/sso.test.mjs": 900,
"packages/webui/tests/tester-browser.test.mjs": 900,
"packages/webui/tests/tester-cli.test.mjs": 900,
"packages/webui/tests/tester-listener.test.mjs": 900,
"packages/webui/tests/tester-socket.test.mjs": 900,
"packages/webui/tests/thread-claims-browser.test.mjs": 900,
"packages/webui/tests/thread-claims.test.mjs": 900,
"packages/webui/tests/usage-browser.test.mjs": 900,
"packages/webui/tests/usage.test.mjs": 900,
"packages/webui/tests/views.test.mjs": 900,
"packages/bus/tests/account-migration.test.mjs": 122,
"packages/bus/tests/chief-of-staff.test.mjs": 127,
"packages/bus/tests/comment-migration.test.mjs": 122,
"packages/bus/tests/end-launch.test.mjs": 122,
"packages/bus/tests/fire-revokes-grants.test.mjs": 130,
"packages/bus/tests/gitea-grant.test.mjs": 146,
"packages/bus/tests/gitea-migration.test.mjs": 122,
"packages/bus/tests/grant-executor.test.mjs": 185,
"packages/bus/tests/grant-migration.test.mjs": 122,
"packages/bus/tests/lifecycle-revoke.test.mjs": 122,
"packages/bus/tests/message-filter.test.mjs": 122,
"packages/bus/tests/name-migration.test.mjs": 121,
"packages/bus/tests/schema.test.mjs": 121,
"packages/bus/tests/sessions.test.mjs": 122,
"packages/bus/tests/spec-migration.test.mjs": 121,
"packages/bus/tests/tester-writes.test.mjs": 122,
"packages/bus/tests/thread-claims.test.mjs": 121,
"packages/bus/tests/thread-migration.test.mjs": 122,
"packages/bus/tests/threads.test.mjs": 124,
"packages/bus/tests/usage-migration.test.mjs": 121
},
"fallback_seconds": 900,
"kill_after_seconds": 10,
"outer_wait_slack_seconds": 30,
"source": "run3-focused/MEASURED-FILE-LIMITS.json"
}
@@ -1,461 +0,0 @@
# Deferred list
One line per item, written from evidence, never from inertia. Add a line
when a gap is found and not fixed in the current piece; strike it (move to
Done at the bottom with the commit) when a piece closes it. Nothing here
starts on its own; a line becomes a piece only when Jason calls it. Check it
at every gate. Started 2026-09-12 during the control board MVP.
## Open
- **Inherited Discord engine test intermittency during #1512 review.** Frozen
serialized run recorded a tool-turn engine assertion failure and then timed
out at 120s. A later full TAP run with a 15s test deadline passed 351/351.
Rocko isolated the busy/settled-event test race and missing failure cleanup;
Darkwing independently reproduced the event-split assertion. Rocko also reported
a timeout-log test race and a separate engine followUp-after-timeout defect.
No live incident established. Row21 owner orch-01 notified; no Discord source
takeover. Not assumed identical to earlier concurrent hangs.
Integration held pending owner disposition; logs and scope in
`agents/darkwing/work/relaunch-activity/r1-review.md`. (2026-09-15)
Outcome 2026-09-26: 1685deb4 fixed the busy/settled race and the engine no
longer sends a follow-up (held prompts). A re-run of the frozen candidate
against 21e3e908 passed the serial acceptance command 397/397 three times.
Two engine failures remain when tests run concurrently, and they reproduce on
clean HEAD without the #1512 files, so they are #1509's (entry below). Sage
ruled that #1512 integrates on the serial evidence; it landed in af4203ca.
Record: `agents/darkwing/work/relaunch-activity/rerun-2026-09-26.md`.
- **Registration has no "set by" field.** `mosaic seat task` records the
task and `updatedAt` but not who set it, so a human session and the
orchestrator look the same on the board. One-line record change; rides
with the orchestrator assignment piece. (2026-09-12, #1504)
- **Usage-limit notice was missed.** Jason reports Rocko exhausted usage without
Jason seeing the notice, and is now available again. Investigate where the
notice appeared and what operator notification is missing as part of Claude
console integration. User-reported, not independently reproduced; no root
cause established. Keep outside #1512's relaunch-display scope. (2026-09-15)
- **Claude Code seats register but get no board row.** Rocko has no Pi
session log, and rows come only from Pi logs. Needs a second row source
or a Claude-side log reader. (2026-09-12, darkwing's #1504 report)
2026-09-15 #1511 live-test preflight reconfirmed no Rocko row: expected
`.pi/state/rocko/sessions` discovery directory absent. Its existing repo
registration names dead PID 185602, while the verified native lock owner is
3707667. No registration repair, discovery expansion or relaunch performed;
test write refused before mutation. Evidence:
`agents/darkwing/work/task-attribution/backend-live-verification.json`.
- **Fleet seats (`~/.mosaic`) stay on the old launchers.** No registration,
no board reply, no `seat task`. Moving them is the stress test and comes
after the WebUI and fleet launch work. (Jason, 2026-09-12)
- **Comms skill has no rule for senders without a pane.** The board trailer
covers it today; if another pane-less sender appears, put the rule in
`skills/ms-communications` instead of a second trailer. (2026-09-12, #1505)
- **Board: multiple sessions per seat, stopping seats, auth beyond
loopback.** Out of scope for the MVP by the plan; listed so they are not
forgotten. (plan page, Boundaries)
- **`MOSAIC-STACK-D-001` is Answered, not Completed.** Mark `--completed` in
jarvis-brain once Jason says the board answers "who is waiting on me".
(2026-09-12)
- **Uncommitted working tree.** `AGENTS.md` autonomy section,
`agents/README.md` Sage row, Dewey's `work/wui` docs and evidence,
`docs/plans/reviews/2026-09-08..10_*`, `skills/ms-{arena,blast-radius,
board,executive-update,git-operations,technical-writing}`. Jason's edits
from before 2026-09-12; they ride along in the next commit that touches
them unless staged on purpose. (2026-09-12)
- **Fleet debt (orch-01, `~/.mosaic`):** `sweep-commit.sh` deny-list regex
skips `launch.env`; needs the fix plus a regression test. (2026-09-12)
- **Parked, not cancelled:** Dewey D05 dashboard mockup work beyond design 3,
registry increment 3 (headless identity-env leak), new seat directories.
(plan page, 2026-09-12 parking note)
- **Gitea helper response-file hardening.** Predictable `/tmp/gitea-api-response.$$`
has no explicit 0600 mode; use `mktemp` with cleanup in a separately reviewed
pass. Credential validation and later reread also have a race window. Other
non-blocking ledger review notes remain in
`reviews/2026-09-12_ledger-verdict.md`. No change beyond the reviewed GET
cleanup fix in this piece. Filbert, LEDGER-1506-R1, 2026-09-12, #1506.
- **One shared checkout for every seat and the coordinator.** Any `git add`
of a shared file (CURRENT.md, SESSIONS.md, the plan page) commits another
seat's in-progress edit under the wrong commit; it happened at f80404f5
(dewey's CURRENT.md status note went in with the piece 5 brief, harmless
but unowned). Fix candidates: per-seat worktrees, or shared files edited
only by the seat named in CURRENT.md. (2026-09-12)
- **Console mockup fields absent from the board API.** Controller, generation,
config fingerprint, turn count, goal/report and activity stream are omitted.
Workspace paths are shown as board data, not invented registered workspace
nodes. Launch, observe, stop, full transcript navigation, command palette and
other screens are absent. Existing palette/appearance switches remain on the
single page. Revisit only with a new brief. (2026-09-13 UTC, #1507)
- **Board send can turn into a Pi slash command.** `tools/tmux`
`send-message.sh` pastes onto whatever is already in Pi's composer without
clearing it. On Jason's 2026-09-26 20:10:57Z send, a leftover "/" landed in
front of the agent-send header. Pi read it as plain text that time, but the
same path could run a board reply as a slash command. Belongs to CHAT-03I/B3.
No tools/tmux change until that piece. Found by Dewey in the CHAT-02 brief
evidence. (2026-09-26, #1507)
- **Discord wedge restarts have no cumulative stop.** Since 6b R2, a wedged
pi ends the connector with exit 1, and the unit restarts it. With
turnTimeoutSeconds 600, one cycle takes at least 645 s, longer than the
600 s StartLimit window. So a pi that wedges on every prompt restarts once
per prompt and never hits the limit. It only happens when someone sends a
message, and that person gets an error reply. Sage accepted it for now. The
fix is a separate policy (a wedge count kept across restarts). (2026-09-26,
#1509, Rocko 6b R2 review)
- **Connector does not journal the source it started from.** The unit runs
from the live checkout, so an automatic restart loads whatever Discord
source is in the tree, even uncommitted source. Rocko recommends a startup
journal line with HEAD, the dirty state of `packages/discord` and a digest
of the runtime files. Until then the rule is that unreviewed Discord source
stays uncommitted in the tree only as long as a review needs it.
(2026-09-26, #1509)
- **Index-export suite runs leave Docker networks behind.** Each export
directory becomes a compose project, and its `<dir>_default` network
stays after the run. On 2026-09-26 the host ran out of address pools. The
task and release suites then failed 31 and 3 tests with "all predefined
address pools have been fully subnetted". Sage removed its own five stale
networks. The dw-* ones are still there. Fix: the export recipe, or the
suites, removes the project network on exit. (2026-09-26)
2026-09-27: it happened again on the Piece D candidate (task 31 and
release 3 failures). Sage removed 12 leftover export networks from
other seats, each checked for zero attached containers first, and both
suites then passed. Still open.
- **Ledger T3 read: snapshot isolation has no test.** `readT3` runs every
query in one read transaction, but no test proves a write landing during
the read stays invisible. Filbert's note 4 on the Gate F build (e47ec6da).
Notes 1 to 3 go to Darkwing's follow-up. (2026-09-26, #1506)
- **A T3 sender role with spaces counts as human.** The SetSpark lead's
reply to Sage (21:41:24Z) used the role "main content-engine agent". The
6a rule requires a one-word role, so the ledger counts that message as
human in Sage's row. Gate F reads only Filbert's row, so it is not
affected. Filbert found it through the live diagnostic (14 went to 15).
Sage asked the SetSpark lead to use a one-word role. The rule stays as
it is. (2026-09-26, #1506)
- **Queue tests: the cold-start failure came back (reopened).** Row 33
closed this as not reproduced in 20 idle cold runs. On 2026-10-04 Sage's
index export of the row 33 candidate ran the nine suites, then
`node --test packages/queue/tests/`, and got 147 passed, 1 failed. Sage's
recipe threw the output away, so again nobody knows which test failed.
Ten more runs all passed 148/148: three reruns in the same export, four
under parallel suite load, and three fresh exports run straight after
`test-queue.sh`. It needs a first run on a fresh tree after a full
suite pass, and even then it's rare. Darkwing's n2 on row 33 (the cold
runs were idle) was right. Next time: Sage's export recipe keeps the
node test output in a file, so the first failure has a name. Then fix it
as its own row. (2026-10-04, #1508)
- **Queue: `add --issue N` also sets closes to N.** Follow-up rows under
an umbrella issue, like rows 31 and 33 under #1508, would close the issue
when they finish. Sage narrowed both by hand (revs 36 and 43). Either
`add` takes `--closes` or the lead checks closes on every add. Not
urgent while Sage adds the rows. (2026-10-04, #1508)
- **Ledger metric call can orphan curl.** The metric call in
`packages/ledger/src/ledger.mjs` still relies on execFileSync's
timeout, which kills the helper but can leave curl running. E's queue
calls already use `timeout -s KILL`; the metric call should match.
(2026-09-27, #1508)
- **Ledger: a missing `timeout` command reads as a Gitea failure.** With
no `timeout` on PATH the queue issue calls report "credential or Gitea
request failure". Optional test and clearer message. Filbert, E r2.
(2026-09-27, #1508)
- **`review record` doesn't check which issue the verdict comment is on.**
Row 5 round 1's request (26671) and Filbert's verdict (26683) are on
#1507. Darkwing's verdict 26681 went to #1508, and `review record`
accepted it, because only `review resolve` reads the comment back
(`checkComment` in `packages/queue/src/review.mjs`). The receipt
points at a real comment on the wrong issue. Sage posted a pointer on
#1507 (comment 26685). Fix: have `record` check the comment's issue and author the way
`resolve` does, or refuse a comment id it can't verify. Found by
Filbert. (2026-10-04, #1508)
- **Host seats launch Pi with `--approve`.** A future `.pi/SYSTEM.md` or
`.pi/settings.json` in the checkout would load in every host seat. This
is latent, since neither file exists today. Fix in
`scripts/agent-host-dev.sh`: pass `--no-approve` and an explicit system
prompt. Filbert, meta-harness survey section 8.4. (2026-10-04)
- **Workers can read provider keys.** The worker container passes
`ZAI_API_KEY` and `ANTHROPIC_API_KEY` in the environment, which Pi's
`bash` inherits, and Pi's `read` can open the mounted `auth.json`.
Slice 1 role tokens stay in the broker (lead decision 47). The workers'
own exposure needs a separate piece. Filbert, survey section 8.6.
(2026-10-04)
- **CHAT-03 I1 round 2 follow-ups, for the I3 brief.** Neither reviewer
held any of these as blocking (Darkwing comment 26690, Filbert comment
26694 on #1507). Four of the five are closed by slice 1 S5 (see Done);
this one stays open.
- No test proves that the processes are listed only after the cgroup
is frozen. The code waits for `frozen 1` in `cgroup.events`, and
both Filbert and Dewey confirmed it by reading it. Mutants r2-B5b and
C4 survive. Proposed: a test-only hold after `members`, or a FUSE or
privileged fixture. Filbert F1.
(2026-10-04, #1507)
- **CHAT-03 `engine.envKeys` accepts founder credential names.**
`CREDENTIAL_NAME` in `packages/conversation/src/pi-pin.mjs` matches any
`*_API_KEY` or `*_TOKEN`, including `GITEA_TOKEN` and `GH_TOKEN`, which
the S6 runner refuses as founder credentials (REQ-CRED-2). Nothing
builds a Controller outside the tests yet. Whoever wires the entry point
narrows the pattern to provider names or checks the founder list.
Filbert N2 on #1522. (2026-10-10)
- **`test-task.sh` makes a live Pi call without the Docker guard.** The
"user recall" block near line 478 runs `run-task.sh` with no
availability check, so a run with Docker unavailable fails "user recall
run succeeds" and "recalled user name" (26 pass, 2 fail). The other
live groups skip. Rocko hit it during the S2 suites. Wrap the block in
the same guard as the other live groups, so a seat without provider
access gets clean skips. (2026-10-04, #1519)
- **`node --test <dir>/` fails on Node 24.** Node 24 reads a directory
argument as a module, so every package's test command in that form
fails there, while Node 26 accepts it. The glob form
(`node --test 'packages/x/tests/*.test.mjs'`) works on both. Package
READMEs, `docs/TOOLS.md` and Sage's suite recipe should use the glob
form wherever Node 24 is a target. Darkwing, S1 packet. (2026-10-04,
#1518)
- **`stack-upgrade.sh` doesn't check `$REPO`.** Nothing confirms the
source checkout is root-owned or that its tree is clean before the
upgrade builds from it. Filbert N2 on row 233 (29922, #1735).
(2026-10-21)
- **`stack-upgrade.sh` has no signal handling between A2 and A5.** A HUP,
INT or TERM in that stretch leaves the run failed closed, but recovery
is by hand. A trap that writes the stop point and the recovery step to
the log would make it a sheet step. Filbert N3 on row 233 (29922,
#1735). (2026-10-21)
- **`checkCreative` doesn't refuse `credentials`.** Row 213's
`checkCreative` (packages/business/src/role.mjs:143 at 2d2ad2760)
limits a creative role's tools, authority and messaging, but a role
file that gave companion or research a credential would still load.
The business file could then map a credential to that instance
(business.mjs:157). Nobody has traced whether a mapped credential
reaches the run's environment. The decision paths stay closed (M6),
but a creative role with `web` holding a live token is a wider channel
than design 4 means. The fix: refuse any `credentials` in
`checkCreative`, with a role.test.mjs case. It goes after row 213
lands, as a follow-up row or in the next row that edits role.mjs.
Webby N1 on row 213 r3 (REVIEW-r3.md, 3ab3c010b), mos-1160.
(2026-10-21)
- **The boot host check orders only one business's bus.**
`mosaic-hostcheck.service` (row 240) has
`[email protected]`. `case2-unit.sh` enables the
bus as `mosaic-bus@<business>.service`, so on a host whose business id
isn't `mosaic-stack` nothing orders the two. The bus can start before
the boot stop runs, and after a boot-time drift role sessions can use
bwrap until it does. The fix: `After=mosaic-hostcheck.service` in
`packages/cli/systemd/mosaic-bus.service.in`, which orders every
instance and does nothing where the unit isn't installed. It goes in
before the first install whose business id isn't `mosaic-stack`.
Filbert N1 on row 240 (29945, #1746). (2026-10-21)
- **The run sheet writes the apt hook before the first `--check`.**
Section 4 installs `80mosaic-hostcheck`, then runs
`hostcheck.sh --check`. If that check fails, the next package run
closes bwrap before anyone has read the failure. It fails closed, but
it surprises an installer. The fix: run `--check` first, or say what a
failure at that step means. Doc-only, for the next row that edits
`docs/guides/host-run-sheet.md`. Filbert N2 on row 240 (29945, #1746).
Filbert's N3, that a closed bwrap appends to `DRIFT` on every later
apt run and boot, is by design; ops-01's STATE read should expect the
repeats. (2026-10-21)
- **The sso-config first-step hint has no business.** When the target
copy has no `console.sso`, `stack-release.sh` (`:938`) refuses with
"activate $TO without --sso first, then ... activate $TO --sso". The
usage line is `activate ID BUSINESS` (`:47`), so pasting either command
as printed gives a usage error, not the first step. It fails safe,
since the usage error changes nothing. The fix: add `$BUSINESS`
(set at `:2138`) to both commands, and the business (`acme`)
to the three expected strings in `stack-release.test.mjs`
(`:1538`, `:1689`, `:1691`). For a later row that edits
`stack-release.sh`. Webby N1 on row 238 r1 (REVIEW-r1.md, 433a95292,
#1744). (2026-10-21)
- **Lane image prep inspects a base that BuildKit never tagged.**
(N7 of the row 197 proof.) After the Compose build,
`scripts/testing/lane-image.mjs:41` (`prepareImage`) runs
`docker image inspect node:24-bookworm-slim` to put the base digest
in the receipt. BuildKit resolves the base from the registry
without tagging it in the local store, so on a host without the tag
the inspect throws "No such image" after a good build. That
happened in Sage's proof try 1 (2026-10-21 09:03Z, run cf48efccf083).
The throw fails closed: the run's terminal record is unclean, both
lanes quarantine, and `gate-lane.sh recover` clears them. Pulling the
base first fixes it, and the receipt digest then matches what the
build used (91acba91 both ways). The fix: before the build, inspect
the base and refuse with a named reason if it's missing, or pull it.
After the build, refuse if the base digest changed. Add a test with a
fake Docker that has no base tag. Kept out of row 197 r6 by Sage
(sage-1883). Mos asked for this record in mos-1162. (2026-10-21)
- **Case 1 acceptance script: root parses with the node it's checking, and the header says less than root does.**
(Duckworth N1-N3 on the row 195 manifest, REVIEW-m1 6afcee2a5.)
N1: F9 runs `$ROOT$NODE` as root (`scripts/case1-accept.sh:298`)
before F10 checks that `--node` is the `/opt/mosaic-stack/node-<pin>`
path (:303-304). On a host installed as designed, `case2-unit.sh:128-133`
refused a non-root-owned `--node` and the args file is root's, so no
escalation path exists today. Run F9's parse through `AS_USER`, or
apply F10's path check first. N2: the header (:10) and the TOOLS.md
entry say root only reads, but root also runs `curl` (F2), `sudo -l`
(P1, P3), the jarvis probe (P9), `hostcheck.sh --check` and
`systemctl`. Doc-only. N3: F6's prefix match (:261) accepts a
`--claude` with `..` after `claude-<pin>/`. That claude runs only as
`mosaic`, and F10 uses equality. Darkwing has a follow-up on
`darkwing/row195-n1` 1e3c4767c, not yet run; it gets its own row now
that 195 has landed (b65014647), Duckworth reviews. (2026-10-21)
- **Morgana's row 213 mutation set: the B3 pattern no longer matches `bundle.mjs`.**
(Webby N3 on row 221 round 1, REVIEW-r1 a0a00630e.) Row 221's
candidate changes `packages/harness/src/bundle.mjs`, and the B3
anchor at Morgana's 213 `mutants.py:48` stops matching on that
base. This matters only if someone reruns that set. Fix the anchor
in that tooling before any rerun, and check that a stale anchor
refuses instead of skipping the mutant (sage-1941). (2026-10-21)
- **Grant executor test X5 starts a `sleep 30` that can outlive its test.**
(Webby N1 on row 248 r1, webby-0080; deferred by Duckworth in the row
249 review, REVIEW-r1 28174fe08, #1751 comment 30070.) The test at
`packages/bus/tests/grant-executor.test.mjs:989` (:934 before row
249) runs `sleep 30` on the sshd side, where an abort doesn't reach it. Fix it with a stub
that exits once its test dir is gone, as row 248 did for the hold
stubs, or with a shorter sleep. This binds the next row that touches
that test file. Until then, a lane cleanup that runs after this file
waits 30 s before it calls a lone `sleep 30` a leak. The row 197
lanes scan only at lane end, about 15 minutes after pkg-bus, so they
can't see it (mos-1169). (2026-10-21)
- **Row 66 live checks: the unslop notice on a live managed session, both harnesses.**
(mos-1186, Vikunja #454.) Row 66's gate asked for one landing session per
harness with a prompt that draws a known tell. Mos changed that gate. The
Claude Code session ran before landing on a scratch root from the
candidate tree (run r4ab24630d323, landed 2c38325ec). Pi ran launch only there (run r1dd94171bede):
this host has no credential pi reads until P19 #383 settles. Two live
checks remain. Claude Code: in the first re-pin whose tree includes row 66,
backup first (Q173). Pi: one full session with a tell after #383. Each
attaches the operator note on the RESULT, the next-turn correction and the
`unslop.jsonl` counts. (2026-10-21)
- **Console tester listener: `testerNoSignIn` is a dead parameter.**
(Duckworth N2 on the row 196 manifest, REVIEW-m1; carried as C1 in
REVIEW-m2 cdbf7b64e, #1694 comment 30156.) Row 196 landed 7247d80c7
with the parameter and its comment still at `serve.mjs:472-475` and
passed from `webui/src/cli.mjs:109` and `sso.test.mjs:392`. Nothing
in `serve.mjs` reads it after the destructure. Remove it, or wire it
with a test. This binds the next row that touches
`packages/webui/src/serve.mjs`. (2026-10-21)
- **Console library routes: the tester listener and `library.sync`.**
(Duckworth N5 on row 196 round 2, REVIEW-m2 cdbf7b64e.) At 7247d80c7
`startServer` (`serve.mjs:475`) takes none of `libraryFile`,
`libraryStatus` or `librarySync`, so the callbacks `cli.mjs` passes
do nothing yet. The row that adds the Console library routes decides
whether the tester listener gets `library.sync`. Duckworth's view is
that it shouldn't, because sync is a human write. (2026-10-21)
- **Console library routes: four library codes still map to 502.**
(Dewey N2 on the row 254 manifest, #1759 comment 30173.) Row 254
gave six library codes a STATUS entry in `packages/webui/src/bus.mjs`.
`library-mapping-invalid`, `library-remote-invalid`,
`library-state-invalid` and `library-git` still fall through
`busStatus` to 502. The row that adds the Console library routes
decides their statuses and gives its `humanCall` for `library.sync`
a timeout over 130 seconds. (2026-10-21)
- **T27 sweep test leaves two `sleep 60` children running.**
(Darkwing row 251 slot, darkwing-0113 and darkwing-0117; Dewey on
r2, comment 30168.) The sweep test spawns two detached shells,
`planted` at `host-run.test.mjs:528` and `decoy` at `:536`, each
running `sleep 60; :` with the run dir as `$0`. `sweepRunDir` kills
the planted shell by pid (`host-run.mjs:238`), and `t.after` kills
only the decoy shell. Neither kill reaches the `sleep` child, so each
one lives for up to 60 s after the test and trips any busy check that
follows. The fix is to kill the process group in both `t.after`
hooks: `process.kill(-pid, 'SIGKILL')`. Each shell is spawned
detached, so it leads its own group. `exec sleep 60` doesn't work.
The sweep matches the run dir in `/proc/<pid>/cmdline`
(`host-run.mjs:236`), and exec drops the `$0` that carries it, so the
planted process would survive and the "planted process killed" assert
would fail. This binds the next row that touches
`packages/bus/tests/host-run.test.mjs`. (2026-10-21)
- **Real-model suite cases run outside the gate.** Row 244 moved
`test-task.sh` fork recall (M11) and user recall (M16) behind
`MOSAIC_LIVE_PROVIDER=1`, and `test-release.sh` activations to the mock
adapter. Before any re-pin whose tree changes `packages/harness` or the
session fork code (`scripts/mosaic-task.mjs` session and
`sessionForkFrom` handling, the pi adapter), an operator runs
`MOSAIC_LIVE_PROVIDER=1 bash scripts/test-task.sh` with the real pi
credential and a real `release.sh activate`, and records both results on
the re-pin row. Morgana's wording (row 244 BUILD.md), Webby N2 on row 244
r1 (#1757). (2026-10-21)
## Queue
Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
## Done
- Ledger (piece 3, #1506): shipped cd0aa5fb, Gate D written 2026-09-12 (18.9 human messages per closed issue).
- Relaunched seat shows the old last message (darkwing, Step 3 log, 2026-09-12): closed by #1512 relaunch activity, af4203ca. Filbert authored it; Darkwing and Dewey reviewed. The live board restart and Gate F remain separate.
- Sage launcher hook uncommitted (2026-09-12): closed by the agents/sage launch files commit, Dewey review R1 revise, R2 approve (`agents/dewey/work/sage-launch-review-2026-09-26.md`), launcher test now covers sage.
- Ledger counts T3 agent messages as human (2026-09-26, #1506): fixed in ef0020ad (Darkwing 6a, Filbert reviewed). `messageKind` now counts a T3 header as agent and the control-board sender as board. It changes no current count, because no source the ledger reads has a T3 header yet. That waits for the T3 thread source (Gate F).
- Control board checked the bind address but not Host or Origin (2026-09-26, #1507, found by Dewey): closed by the board guard commit. `foreignRequest` refuses a non-loopback Host, a wrong port, userinfo or a path in Host, and any foreign Origin on every route. Dewey authored it, and Rocko approved it (`agents/rocko/work/board-guard-review-2026-09-26.md`, 5a12f08e). Rocko's one low finding: URL normalization accepts a root slash, empty userinfo and an empty query in Host, so the notes' wording overstates what the raw-syntax check refuses. No bypass was found.
- Discord engine: leaked fake pi and the timeout gap in `busy` (2026-09-26, #1509): fixed in 3edb15eb (Darkwing 6b R2, Rocko approved, review ed5510a0). Tests stop the engine in `finally`. A turn pi never started now stops pi with exit 1 instead of guessing, and the unit restarts it. Known limit: see the open entry on wedge restarts.
- Combined concurrent test hangs (2026-09-14, #1509): closed on evidence from 3edb15eb. At default concurrency the union passed 406/406 three times on the R2 snapshot and 408/408 on the commit's index, where clean HEAD had hung four runs out of four. The inherited-intermittency entry from 2026-09-15 stays open until a later run shows it is the same cause.
- A nested `node --test` passes even when its tests fail (2026-09-26, #1508): closed. Queue A1 (34a72af9) clears NODE_TEST_CONTEXT on its own path. N13 fixes `test-foundation.sh` and `test-discord.sh`, each with a check that fails the suite if the clearing is lost. Darkwing wrote it (n13.patch 00868b2f) and Filbert approved it (e464be6c). Sage's grep on 2026-09-27 finds no other nested `node --test`: in scripts, only the two suites, `test-queue.sh` and `queue-commit.sh`, and in JS, only the queue test that covers the fix.
- SetSpark service accepted any string as a decision approver (2026-09-26, #1509): closed. Fixed in shared-signals cc74d92 (Rocko approved R2, lead decision 24). The operations seat (T3 12fe8cda) deployed it to VM 1022 at 00:23Z on 2026-09-27. The running `service.py` hash matches cc74d92, and `/healthz` returns 200 inside the network and through the public tunnel. The read-only README survey found one row breaking the rule (DEC-009, Proposed, version 1), one open request that isn't ready (DEC-009's), and no Accepted decision with more than 16 approvers. No rows changed. DEC-009 stays as stored (lead decisions 21 and 28). The service now refuses approvals on it, so nothing can go wrong silently. Side effect: `up -d` also recreated caddy, about 20 s of edge downtime for all hosts. That is for the shared-signals deploy procedure to fix, not Mosaic.
- Queue suite failed its live checks outside the canonical checkout
(2026-09-27, #1508): closed by Piece D, f539466f. `test-queue.sh` now
skips `verify` and `render --check` with a printed reason when the
toplevel isn't the recorded `canonicalRoot`. An index export passed
27/0, and the canonical checkout 29/0.
- Queue: an owner listed as a reviewer blocked the row (2026-09-27,
#1508): closed by row 31, bb2fa970. `add`, `set reviewers` and `assign`
now refuse the owner as a reviewer with exit 2 and write nothing. Rocko
built it in the Gate G run, and Filbert approved round 1 (#1508 comment
26629, 8 of 8 mutants caught).
- Queue follow-ups from rows 12, 13 and 31 (2026-10-04, #1508): closed by
row 33, 6fb50cc0. Filbert built it, and Darkwing approved round 1
(#1508 comment 26651).
- `genesis` refuses a map row whose owner is one of its reviewers (exit
2, nothing written). The check is on the map, not in replay, so a log
from before the rule still loads.
- The `assign` refusal now reads "can't assign row N to SEAT: SEAT is
one of its reviewers".
- `queue-commit.sh` compares HEAD with H before each canary and again if
the clean run fails. A moved HEAD exits 2 with "HEAD moved since H was
recorded"; the guard message is kept for a real guard failure.
- Every time and date the validator accepts must format back to itself,
so month 13, day 32, 2026-02-30, 2027-02-29 and 24:00 are refused.
The live log at rev 45 still replays.
- The cold-start failure (2026-09-27): not reproduced in 20 cold runs.
Each ran `node --test packages/queue/tests/` in a fresh tree, 10
shared clones and 10 exports, one after another. All 20 passed
148/148 (2960 of 2960). The 300 ms deadline test is unchanged. Counts:
`agents/filbert/work/queue-33/cold-runs.txt`.
- CHAT-03 I1 round 2 follow-ups (2026-10-04, #1507): four of five closed
by slice 1 S5 (row 40, #1522; Dewey authored, Darkwing and Filbert
reviewing). The commit is named in the row's review record when it
lands.
- Engine command and `preArgs`: `engine` takes only `extraArgs`, `cwd`
and `envKeys`. The controller always launches the pinned Pi and
checks that at construction and at bind. The fake engine comes in
through a symbol key that JSON config can't carry (N24b).
- Engine environment: an explicit list (`ENGINE_ENV` in `pi-pin.mjs`)
plus `*_API_KEY` and `*_TOKEN` names from `engine.envKeys`. Nothing
else is inherited. `HOME` passes, so Pi reads `~/.pi/agent` unless
`PI_CODING_AGENT_DIR` is set (Darkwing F3).
- `escalating` is cleared when the force-stop fence throws (Darkwing F2,
races.test.mjs).
- Takeover then Enter in one chunk: input after Ctrl-T or Ctrl-O waits
for that action, in the same chunk or a later one, and also while an
earlier action is still pending, so it is judged as if typed one key
at a time (Filbert F2; Darkwing and Filbert T1 on #1522;
flows.test.mjs).
The frozen-cgroup test (Filbert F1) stays open above.
- Gate runs mount the host's pi auth file (Webby N4 on the row 197 proof
driver, 059577886): closed by row 244 (#1757), landing e69260a3c. The
suites and the lane's `cleanEnv` export a 0600 fixture `PI_AUTH_FILE`,
`guardDocker` refuses a Compose run without one, and
`packages/cli/tests/gate-lane-auth.test.mjs` proves no gate step mounts
the host default. The real-model cases this moved off the gate are open
above.
## Row253 F5: legacy Case AS_USER supervision
Tracked as row 276, Gitea #1781 (sage-2331), owner Gander, reviewer Duckworth, after row253.
Brief: docs/plans/2026-10-22_case-sudo-legacy-followup.md, “Brief: supervised legacy AS_USER calls”.
case1-accept.sh's legacy timeout330 around AS_USER talk and its AS_USER precheck, Claude version, PM launches and Node version diagnostics predate the new socket metadata/probe helpers. sudo's pty monitor can move them to a separate session; the old outer timeout does not prove their descendants are killed. The C4 repair in row253 covers only its new config query and peer probe. These legacy paths need the reviewed supervisor or a header explicitly explaining why each remains unsupervised. The follow-up must preserve the 300-second talk/reply contract, concurrent F5 scan, diagnostics and secret-output refusals, with fake-only session-hop tests and Sage's slot. No host or live unit change is authorized.
@@ -1,10 +0,0 @@
{
"agents/gander/work/queue-253/c4/case1-accept.sh.txt": {
"source": "scripts/case1-accept.sh",
"sha256": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f"
},
"agents/gander/work/queue-253/c4/case2-unit.sh.txt": {
"source": "scripts/case2-unit.sh",
"sha256": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd"
}
}
File diff suppressed because it is too large. Load diff
@@ -1,44 +0,0 @@
# C4 repair for the short static pass
Prepared under duckworth-0111 and sage-2325/2331. No tests, syntax execution, probe, real sudo or host experiment ran on this repair. No passing claim applies to these new product bytes. DESIGN.md and the approved DESIGN-C4.md remain unchanged. The original frozen snapshots remain unchanged.
REPAIR.diff compares the repaired scripts against their original frozen source, and tests/docs against 968db0bcd. The *.repair.txt files are non-executable exact snapshots of all seven pending source/test/doc paths. REPAIR-SHAS.json pins them. The live source/test paths remain uncommitted until the gate permits the product commit.
## Conditions mapped to the diff
C1: fixed setsid without -f/-w; confirm the direct child's kernel SID equals its PID. M38 drops setsid; the named row253 C4 signals query killer must fail the exact numeric PID/SID equality assertion on the first query-pids line, in each kind and both sudo modes, without a timeout. A blocking child can still reach inner-timeout without setsid, so the refusal reason alone does not prove session creation (mos-1298).
C2: exact inner/outer/output-limit refusals in both fake modes, query 5/7 s and probe 2/4 s. Outer cases require at least the outer duration and less than outer + 2 s. Root discards the valid-looking stdout of a stalled supervisor. A trusted notice plus status 137 identifies the outer timer.
C3: read complete stat files, strip through the last closing parenthesis followed by space, skip vanished entries, refuse on other errors. The setpgid member has newline, closing parenthesis and space in comm. M36 kills only the leader group and must fail that case. The existing 250 ms cleanup budget and 50 x 10 ms post-return test polling remain.
C4: DEFERRED.md names row 276 / #1781 and the brief from sage-2331. It is an intended manifest path. UPSTREAM-PATHS.json records the requested comparison. Current upstream DEFERRED.md was preserved before our entry.
C5: the test compares both inline payloads byte for byte; M39 adds a comment to only one. Both payloads were prepared from supervisor-source.txt.
N1: the fake reads its mode with the Bash file-read builtin form. N2: fixture cleanup rechecks SID before each kill, including members discovered by an owned-session scan. N3: exec mode cannot safely scan the runner's session, so it checks exact PIDs and says so; hop mode checks all members of self-led recorded sessions. N4: the fake still makes no synthetic HUP on its own death. N5: polling remains bounded at 500 ms. N6: the packet must be published before the manifest round.
## Line references in matching repair snapshots
- scripts/case2-unit.sh:162: `stat_fields()`
- scripts/case2-unit.sh:180: `clean_session()`
- scripts/case2-unit.sh:220: `"$setsid_bin" "$@"`
- scripts/case2-unit.sh:221: `for ((attempt=`
- scripts/case2-unit.sh:240: `tester_tools()`
- scripts/case2-unit.sh:270: `tester_capture()`
- packages/cli/tests/case2-unit.test.mjs:192: `for(const kind`
- packages/cli/tests/case2-unit.test.mjs:219: `C4 stalled supervisor`
- packages/cli/tests/case2-unit.test.mjs:233: `C4 inline supervisor`
- packages/cli/tests/case-sudo-fixture.mjs:21: `case_sudo_run()`
- packages/cli/tests/case-sudo-fixture.mjs:84: `function sessionPairs`
- packages/cli/tests/case-sudo-fixture.mjs:75: `function killPairs`
- packages/cli/tests/case1-accept.test.mjs:309: `C4 acceptance query inner`
- packages/cli/tests/case1-accept.test.mjs:322: `C4 acceptance query outer`
- packages/cli/tests/case1-accept.test.mjs:345: `for(const fault`
- docs/plans/DEFERRED.md:456: `## Row253 F5`
## Proposed next gate
On Sage CLEAR after static approval: both touched files whole, M1-M39 with passing focused bases and byte-identical restoration, CLI whole, WebUI whole and the ten shell files. Previous completed results are historical only because product blobs changed. Use a gXXXXXX TMPDIR under /mnt/storage/scratch/tmp, pipefail and tee, a busy check before every launch, and log/wrapper names that do not match the busy detector. Stop and report according to the new grant.
REPAIR.diff uses zero context so blank context lines in the stored patch do not trigger whitespace checks on the record itself. The full matching snapshots provide surrounding context.
@@ -1,30 +0,0 @@
{
"scripts/case2-unit.sh": {
"frozen_before": "a84bb266d52a5c078171582e2f9a511becf56421ae218266e3238d704727f6dd",
"repair": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe"
},
"scripts/case1-accept.sh": {
"frozen_before": "1302cfc659ff9073035b99d7b4071aa2c75070da142bb201d2f58a66e113d71f",
"repair": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7"
},
"packages/cli/tests/case-sudo-fixture.mjs": {
"frozen_before": "5c2cf5408944f8c1715355f75e2510a8deee814b90e2b44befb5a7dff411d6b7",
"repair": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008"
},
"packages/cli/tests/case2-unit.test.mjs": {
"frozen_before": "83cd18f05e60d489e4b0e581d9d977fbf86f897ea3d0bddaa13a63e86f22321f",
"repair": "72a07e9ad9d5c8bb01851eef18f792f2fe9b95d56803e313fad6aa1f1d6c3e8c"
},
"packages/cli/tests/case1-accept.test.mjs": {
"frozen_before": "3ed99803a7e3f81d8dce62e0577cd6c86b70a0339e688c0124bcb10d149e8de9",
"repair": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901"
},
"packages/webui/README.md": {
"frozen_before": "04b7c1165c4fa73137a72e8373d70fd7d96a5897b5511170e6d41908a7667e96",
"repair": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9"
},
"docs/plans/DEFERRED.md": {
"frozen_before": "cadfb704f03e1bfcd36e56ca7a1a3636955004e7d96be9b8765f35ea750da053",
"repair": "67bbf7bf57aa70dcade36ca4642f01cccb71ce01c130a88b84e1edb7ea674377"
}
}
-684
View File
@@ -1,684 +0,0 @@
--- a/scripts/case2-unit.sh
+++ b/scripts/case2-unit.sh
@@ -19 +19,4 @@
-# dependencies. It renders the default instance only.
+# dependencies. The inner query supervisor runs after the uid drop: 5 s inside,
+# 7 s on root's wait. Fixed system Bash, setsid, sleep, timeout and head are
+# resolved and root-validated. Only the config query uses this supervisor.
+# It renders the default instance only.
@@ -153,0 +157,113 @@
+# row253-supervisor-begin
+IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
+set +m
+seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
+child="" timer="" verified=0
+stat_fields() {
+ local pid="$1" raw rest
+ if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
+ [ -e "/proc/$pid/stat" ] && return 1
+ return 3
+ fi
+ [ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
+ rest="${raw##*) }"
+ [ "$rest" != "$raw" ] || return 1
+ read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
+ [[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
+}
+clock_cs() {
+ local boot spare
+ read -r boot spare < /proc/uptime || return 1
+ [[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
+ CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
+}
+clean_session() {
+ local entry pid rc live until
+ clock_cs || return 1
+ until=$((CLOCK_CS + 25))
+ while :; do
+ live=0
+ for entry in /proc/[0-9]*/stat; do
+ pid="${entry%/stat}"; pid="${pid##*/}"
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" != 3 ] || continue
+ [ "$rc" = 0 ] || return 1
+ [ "$ST_SESSION" = "$child" ] || continue
+ case "$ST_STATE" in Z|X) continue ;; esac
+ live=1
+ if ! kill -KILL "$pid" 2>/dev/null; then
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
+ fi
+ done
+ [ "$live" != 0 ] || return 0
+ clock_cs || return 1
+ [ "$CLOCK_CS" -lt "$until" ] || return 1
+ "$sleep_bin" 0.01
+ done
+}
+finish() {
+ local rc="$?"
+ trap - EXIT HUP INT TERM
+ if [ "$verified" = 1 ]; then
+ clean_session || rc=86
+ elif [ -n "$child" ]; then
+ kill -KILL "$child" 2>/dev/null || :
+ fi
+ [ -z "$child" ] || wait "$child" 2>/dev/null || :
+ if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
+ exit "$rc"
+}
+trap finish EXIT
+trap 'exit 88' HUP INT TERM
+"$sleep_bin" "$seconds" & timer=$!
+"$setsid_bin" "$@" & child=$!
+for ((attempt=0; attempt<100; attempt++)); do
+ rc=0; stat_fields "$child" || rc=$?
+ [ "$rc" = 0 ] || exit 85
+ if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.002
+done
+[ "$verified" = 1 ] || exit 85
+while kill -0 "$child" 2>/dev/null; do
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.01
+done
+kill -0 "$timer" 2>/dev/null || exit 81
+rc=0; wait "$child" || rc=$?
+[ "$rc" = 0 ] || exit 87
+exit 0
+MOSAIC_TESTER_SUPERVISOR
+# row253-supervisor-end
+
+tester_tools() {
+ local input target p st magic
+ TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
+ for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
+ target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
+ case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
+ [ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
+ p="$target"
+ while :; do
+ st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
+ [ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
+ [ "$p" != / ] || break
+ p="${p%/*}"; [ -n "$p" ] || p=/
+ done
+ magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
+ [ "$magic" = 7f454c46 ] || return 1
+ case "$input" in
+ /bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
+ /usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
+ /usr/bin/head) TS_HEAD="$target" ;;
+ esac
+ done
+}
+tester_capture_reason() {
+ case "$1" in
+ 81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
+ 84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
+ *) echo inner-command ;;
+ esac
+}
@@ -155,5 +271,11 @@
- local seconds="$1" maximum="$2" file child="" rc=0; shift 2
- file="$(mktemp)" || exit 1
- trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
- trap 'exit 1' HUP INT TERM
- timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
+ local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
+ tester_tools || exit 84
+ file="$(mktemp)" || exit 88
+ notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
+ trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
+ trap 'exit 88' HUP INT TERM
+ LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
+ set -o pipefail
+ maximum="$1"; head_bin="$2"; shift 2
+ "$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
+ ' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
@@ -163 +285,4 @@
- [ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
+ # Only GNU timeout and the trusted head can write this root-side channel.
+ if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
+ [ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
+ [ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
@@ -165,0 +291 @@
+
@@ -259 +385 @@
- query="$(tester_capture 5 4096 sudo -u "$USER_NAME" -H env -i "HOME=$HOME_DIR" "PATH=$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query failed"
+ query="$(tester_capture 5 7 4096 "$USER_NAME" "$HOME_DIR" "$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query $(tester_capture_reason "$?")"
--- a/scripts/case1-accept.sh
+++ b/scripts/case1-accept.sh
@@ -18,0 +19,4 @@
+# The config/probe supervisor is fixed root-validated system Bash plus setsid
+# and sleep, after the uid drop: query inner/outer 5/7 s, probe 2/4 s. Root
+# independently bounds its sudo wait. Legacy AS_USER talk and diagnostics below
+# retain their old behavior and are NOT covered by this supervisor (F5 follow-up).
@@ -110,0 +115,113 @@
+# row253-supervisor-begin
+IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
+set +m
+seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
+child="" timer="" verified=0
+stat_fields() {
+ local pid="$1" raw rest
+ if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
+ [ -e "/proc/$pid/stat" ] && return 1
+ return 3
+ fi
+ [ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
+ rest="${raw##*) }"
+ [ "$rest" != "$raw" ] || return 1
+ read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
+ [[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
+}
+clock_cs() {
+ local boot spare
+ read -r boot spare < /proc/uptime || return 1
+ [[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
+ CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
+}
+clean_session() {
+ local entry pid rc live until
+ clock_cs || return 1
+ until=$((CLOCK_CS + 25))
+ while :; do
+ live=0
+ for entry in /proc/[0-9]*/stat; do
+ pid="${entry%/stat}"; pid="${pid##*/}"
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" != 3 ] || continue
+ [ "$rc" = 0 ] || return 1
+ [ "$ST_SESSION" = "$child" ] || continue
+ case "$ST_STATE" in Z|X) continue ;; esac
+ live=1
+ if ! kill -KILL "$pid" 2>/dev/null; then
+ rc=0; stat_fields "$pid" || rc=$?
+ [ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
+ fi
+ done
+ [ "$live" != 0 ] || return 0
+ clock_cs || return 1
+ [ "$CLOCK_CS" -lt "$until" ] || return 1
+ "$sleep_bin" 0.01
+ done
+}
+finish() {
+ local rc="$?"
+ trap - EXIT HUP INT TERM
+ if [ "$verified" = 1 ]; then
+ clean_session || rc=86
+ elif [ -n "$child" ]; then
+ kill -KILL "$child" 2>/dev/null || :
+ fi
+ [ -z "$child" ] || wait "$child" 2>/dev/null || :
+ if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
+ exit "$rc"
+}
+trap finish EXIT
+trap 'exit 88' HUP INT TERM
+"$sleep_bin" "$seconds" & timer=$!
+"$setsid_bin" "$@" & child=$!
+for ((attempt=0; attempt<100; attempt++)); do
+ rc=0; stat_fields "$child" || rc=$?
+ [ "$rc" = 0 ] || exit 85
+ if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.002
+done
+[ "$verified" = 1 ] || exit 85
+while kill -0 "$child" 2>/dev/null; do
+ kill -0 "$timer" 2>/dev/null || exit 81
+ "$sleep_bin" 0.01
+done
+kill -0 "$timer" 2>/dev/null || exit 81
+rc=0; wait "$child" || rc=$?
+[ "$rc" = 0 ] || exit 87
+exit 0
+MOSAIC_TESTER_SUPERVISOR
+# row253-supervisor-end
+
+tester_tools() {
+ local input target p st magic
+ TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
+ for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
+ target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
+ case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
+ [ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
+ p="$target"
+ while :; do
+ st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
+ [ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
+ [ "$p" != / ] || break
+ p="${p%/*}"; [ -n "$p" ] || p=/
+ done
+ magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
+ [ "$magic" = 7f454c46 ] || return 1
+ case "$input" in
+ /bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
+ /usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
+ /usr/bin/head) TS_HEAD="$target" ;;
+ esac
+ done
+}
+tester_capture_reason() {
+ case "$1" in
+ 81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
+ 84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
+ *) echo inner-command ;;
+ esac
+}
@@ -112,5 +229,11 @@
- local seconds="$1" maximum="$2" file child="" rc=0; shift 2
- file="$(mktemp)" || exit 1
- trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file"' EXIT
- trap 'exit 1' HUP INT TERM
- timeout --signal=KILL "${seconds}s" /bin/bash -c 'set -o pipefail; maximum="$1"; shift; "$@" | head -c "$((maximum + 1))"' -- "$maximum" "$@" > "$file" 2>/dev/null &
+ local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
+ tester_tools || exit 84
+ file="$(mktemp)" || exit 88
+ notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
+ trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
+ trap 'exit 88' HUP INT TERM
+ LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
+ set -o pipefail
+ maximum="$1"; head_bin="$2"; shift 2
+ "$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
+ ' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
@@ -120 +243,4 @@
- [ "$rc" = 0 ] && [ "$(wc -c < "$file")" -le "$maximum" ] || exit 1
+ # Only GNU timeout and the trusted head can write this root-side channel.
+ if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
+ [ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
+ [ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
@@ -122,0 +249 @@
+
@@ -196 +323 @@
-QUERY="$(tester_capture 5 4096 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query failed"
+QUERY="$(tester_capture 5 7 4096 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query $(tester_capture_reason "$?")"
@@ -536 +663 @@
- peer="$(tester_capture 3 128 sudo -u "$S_USER" -H env -i "HOME=$S_HOME" "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why=probe
+ peer="$(tester_capture 2 4 128 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why="probe-$(tester_capture_reason "$?")"
@@ -540 +667 @@
- else why=probe; fi
+ else [ -n "$why" ] || why=probe; fi
--- a/packages/cli/tests/case-sudo-fixture.mjs
+++ b/packages/cli/tests/case-sudo-fixture.mjs
@@ -3,0 +4,3 @@
+import { spawn } from 'node:child_process';
+import { readFileSync, readdirSync } from 'node:fs';
+
@@ -4,0 +8,13 @@
+case_sudo_entry() {
+ local record="$1" here sid; shift
+ here="$BASHPID"
+ read -r sid < <(/bin/ps -o sid= -p "$here")
+ printf "%s %s\n" "$here" "$sid" >> "$record/session-pids"
+ if { [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
+ { [ -f "$record/stall-probe" ] && [[ "$*" == *SO_PEERCRED* ]]; }; then
+ trap '' TERM HUP
+ [ ! -f "$record/stall-output" ] || /bin/cat "$record/stall-output"
+ exec /bin/sleep 60
+ fi
+ exec "$@"
+}
@@ -6,2 +22,3 @@
- local record="$1" child="" rc=0; shift
- if [ "$(cat "$record/session-mode" 2>/dev/null)" != hop ]; then exec "$@"; fi
+ local record="$1" child="" rc=0 mode=exec; shift
+ [ ! -f "$record/session-mode" ] || mode="$(<"$record/session-mode")"
+ if [ "$mode" != hop ]; then case_sudo_entry "$record" "$@"; return "$?"; fi
@@ -13,7 +30,2 @@
- /usr/bin/setsid /bin/bash -c '
- record="$1"; shift
- here="$BASHPID"
- read -r pid sid < <(/bin/ps -o pid=,sid= -p "$here")
- printf "%s %s\n" "$pid" "$sid" >> "$record/session-pids"
- exec "$@"
- ' -- "$record" "$@" &
+ export -f case_sudo_entry
+ /usr/bin/setsid /bin/bash -c 'case_sudo_entry "$@"' -- "$record" "$@" &
@@ -31 +43,3 @@
- const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>Number(line.trim().split(/\s+/)[1])));
+ // Exec fake entries share the test runner's session. Only self-led sessions
+ // are isolated and safe to scan here; exec coverage also checks exact PIDs.
+ const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number)).filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
@@ -41,0 +56,87 @@
+
+export function recordedPids(file) {
+ try{return readFileSync(file,'utf8').trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number));}
+ catch(error){if(error.code==='ENOENT')return [];throw error;}
+}
+
+export function recordedLivePids(file) {
+ return sessionPairs(recordedPids(file)).filter(([pid,sid])=>{
+ try{
+ const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
+ return Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X';
+ }catch(error){if(error.code==='ENOENT'||error.code==='ESRCH')return false;throw error;}
+ }).map(([pid])=>pid);
+}
+
+export function killRecordedPids(file) {
+ killPairs(recordedPids(file));
+}
+
+function killPairs(pairs) {
+ for(const [pid,sid] of sessionPairs(pairs)){
+ try{
+ const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
+ if(Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X')process.kill(pid,'SIGKILL');
+ }catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
+ }
+}
+
+function sessionPairs(pairs) {
+ // Only self-led records authorize a session scan. Exec-mode records belong
+ // to the runner's session and authorize checking their exact PIDs only.
+ const sessions=new Set(pairs.filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
+ const found=new Map(pairs.map(pair=>[pair[0],pair]));
+ if(sessions.size)for(const entry of readdirSync('/proc')){
+ if(!/^\d+$/.test(entry))continue;
+ try{
+ const raw=readFileSync(`/proc/${entry}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' '),sid=Number(fields[3]);
+ if(sessions.has(sid))found.set(Number(entry),[Number(entry),sid]);
+ }catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
+ }
+ return [...found.values()];
+}
+
+// Safety net for a removed-outer-bound change. It is fixture cleanup, never
+// product supervision. The guard reads only fixture records and checks sid
+// immediately before every kill. Normal outer-bound cases return before it.
+export function fixtureGuard(t,files,milliseconds=9500) {
+ const program=`const fs=require('node:fs');setTimeout(()=>{for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}},Number(process.argv[2]));`;
+ const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds)],{stdio:'ignore'});
+ const stop=async()=>{
+ if(child.exitCode!==null||child.signalCode!==null)return;
+ const exited=new Promise(resolve=>child.once('exit',resolve));
+ child.kill('SIGKILL');await exited;
+ };
+ t.after(stop);return stop;
+}
+
+export function cleanupFixture(t,files) {
+ let captured=[];
+ const capture=()=>{captured=files.flatMap(recordedPids);};
+ // tmp(t)'s directory-removal hook may run first. Retain PID/SID pairs in
+ // memory before assertions, then verify the kernel sid again before killing.
+ t.after(()=>killPairs([...captured,...files.flatMap(recordedPids)]));
+ return capture;
+}
+
+export async function pollFixtureGone(files) {
+ for(let n=0;n<50;n++){
+ if(files.every(file=>recordedLivePids(file).length===0))return [];
+ await new Promise(resolve=>setTimeout(resolve,10));
+ }
+ return files.flatMap(recordedLivePids);
+}
+
+export function blockingQuerySource(records,kind='signals',member,proof) {
+ const py=`import os,sys\nos.setpgid(0,0)\nwith open(sys.argv[2],"a") as f:f.write(str(os.getpid())+" "+str(os.getsid(0))+chr(10))\nos.execv(sys.argv[1],[sys.argv[1],"60"])`;
+ return `import fs from 'node:fs';import {spawn} from 'node:child_process';
+const records=${JSON.stringify(records)};
+function record(pid){const s=fs.readFileSync('/proc/'+pid+'/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);fs.appendFileSync(records,pid+' '+sid+String.fromCharCode(10));}
+record(process.pid);
+export function loadSystem(){
+${kind==='sleep'?`const c=spawn('/usr/bin/sleep',['60'],{stdio:'inherit'});record(c.pid);`:''}
+${kind==='signals'?`process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});`:''}
+${kind==='member'?`const c=spawn('/usr/bin/python3',['-I','-S','-c',${JSON.stringify(py)},${JSON.stringify(member)},records],{stdio:'inherit'});for(let n=0;n<200;n++){try{const s=fs.readFileSync('/proc/'+c.pid+'/stat','utf8');if(s.includes('('+${JSON.stringify(member?.split('/').at(-1))}+') ')){fs.writeFileSync(${JSON.stringify(proof)},s);break;}}catch{}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,5);}`:''}
+Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
+}`;
+}
--- a/packages/cli/tests/case2-unit.test.mjs
+++ b/packages/cli/tests/case2-unit.test.mjs
@@ -16 +16 @@
-import { CASE_SUDO_SESSION, liveRecordedSessions } from "./case-sudo-fixture.mjs";
+import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
@@ -96 +96 @@
- (trap '' TERM; exec sleep 60) </dev/null >/dev/null 2>&1 &
+ (trap '' TERM; here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here"); printf '%s %s\\n' "$here" "$sid" >'${root}/query-owned'; exec sleep 60) </dev/null >/dev/null 2>&1 &
@@ -189,2 +189,4 @@
-// Draft counterexample for DESIGN-C4. Written after sage-2307, not run.
-test('row253 C4 query cleanup handles both exec and sudo session-hop fakes', async t=>{
+// C4 cases run through both sudo process models. Exec shares the runner's
+// session, so its session-wide check would be vacuous; exact recorded PIDs
+// are checked in both modes. Hop additionally isolates the fake session.
+for(const kind of ['sleep','signals','member']) test(`row253 C4 ${kind} query has exact inner reason and cleans all members`,async t=>{
@@ -192 +194 @@
- const h=hub(t),pids=join(h.root,'session-pids'),queryPids=join(h.root,'query-pids'),copy=join(copyDir(h),'packages/cli/src/config.mjs');
+ const h=hub(t),sessions=join(h.root,'session-pids'),queries=join(h.root,'query-pids'),files=[sessions,queries];
@@ -194,21 +196,10 @@
- writeFileSync(pids,'');
- writeFileSync(copy,`import fs from 'node:fs';process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});fs.appendFileSync(${JSON.stringify(queryPids)},process.pid+'\\n');const s=fs.readFileSync('/proc/self/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);if(sid===process.pid)fs.appendFileSync(${JSON.stringify(pids)},process.pid+' '+sid+'\\n');export function loadSystem(){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);}`);
- t.after(()=>{
- if(!existsSync(pids))return;
- // Explicit fixture cleanup is safe only for these recorded fixture PIDs.
- for(const line of [read(pids),existsSync(queryPids)?read(queryPids):''].join('\n').trim().split('\n')){
- const pid=Number(line.trim().split(/\s+/)[0]);
- if(pid>0)try{process.kill(pid,'SIGKILL');}catch{}
- }
- });
- const r=unit(h,{profile:'factory'});refuses(r,/query/);
- for(let n=0;n<50;n++){
- if(!liveRecordedSessions(pids,read,()=>readdirSync('/proc')).length)break;
- await new Promise(resolve=>setTimeout(resolve,10));
- }
- assert.deepEqual(liveRecordedSessions(pids,read,()=>readdirSync('/proc')),[],mode);
- for(const pid of read(queryPids).trim().split('\n')){
- let state='gone';try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{}
- assert.ok(state==='gone'||state==='Z',`${mode}: query ${pid} still ${state}`);
- }
- assert.equal(existsSync(h.log),false,mode);
+ const member=join(h.root,'q ) '+String.fromCharCode(10)+'mem'),proof=join(h.root,'comm-proof');
+ if(kind==='member')symlinkSync('/usr/bin/sleep',member);
+ writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),blockingQuerySource(queries,kind,member,proof));
+ const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
+ const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
+ refuses(r,/^case2-unit: tester config query inner-timeout\n$/);
+ assert.ok(seconds<7,`${kind}/${mode}: ${seconds}s exceeds outer bound`);
+ assert.deepEqual(await pollFixtureGone(files),[],mode);
+ if(kind==='member')assert.ok(read(proof).includes('('+member.split('/').at(-1)+') '),'member comm with newline and closing parenthesis was observed');
+ assert.equal(existsSync(h.log),false,mode);await stopGuard();
@@ -215,0 +207,30 @@
+});
+
+test('row253 C4 overflow is output-limit in both sudo modes',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=hub(t),files=[join(h.root,'session-pids')];writeFileSync(join(h.root,'session-mode'),mode);const capture=cleanupFixture(t,files);
+ writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:${JSON.stringify(h.dataRoot)}});`);
+ const r=unit(h,{profile:'factory'});capture();refuses(r,/^case2-unit: tester config query output-limit\n$/);
+ assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
+ assert.deepEqual(await pollFixtureGone(files),[]);
+ }
+});
+
+test('row253 C4 stalled supervisor uses the outer reason and explicit residual cleanup',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=hub(t),file=join(h.root,'session-pids'),files=[file];writeFileSync(join(h.root,'session-mode'),mode);
+ writeFileSync(join(h.root,'stall-config'),'');writeFileSync(join(h.root,'stall-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
+ const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
+ const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
+ refuses(r,/^case2-unit: tester config query outer-timeout\n$/);
+ assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s slack includes root preflight`);
+ assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
+ if(mode==='hop')assert.ok(recordedLivePids(file).length>0,'R3 residual exists before fixture cleanup');
+ killRecordedPids(file);assert.deepEqual(await pollFixtureGone(files),[], 'explicit fixture cleanup of R3 residual');await stopGuard();
+ }
+});
+
+test('row253 C4 inline supervisor is byte-identical in both host scripts',()=>{
+ const extract=s=>s.split('# row253-supervisor-begin\n')[1]?.split('# row253-supervisor-end')[0];
+ const a=extract(read(UNIT)),b=extract(read(join(REPO,'scripts/case1-accept.sh')));
+ assert.ok(a?.length>100);assert.equal(a,b);
@@ -222,2 +243,2 @@
- t.after(()=>{if(pid)try{process.kill(pid,'SIGKILL');}catch{}});
- const r=unit(h,{profile:'factory'});
+ const capture=cleanupFixture(t,[join(h.root,'query-owned')]);
+ const r=unit(h,{profile:'factory'});capture();
@@ -235,2 +256,2 @@
-test('row253 config parser is strict and output capture is bounded',()=>{
- const source=read(UNIT), body=source.slice(source.indexOf('tester_capture() ('),source.indexOf('tester_location() {'));
+test('row253 config parser is strict',()=>{
+ const source=read(UNIT), body=source.slice(source.indexOf('tester_facts() {'),source.indexOf('tester_location() {'));
@@ -244,2 +264,0 @@
- const r=spawnSync('/bin/bash',['-c',body+'\ntester_capture 1 32 /usr/bin/head -c 100 /dev/zero']);
- assert.notEqual(r.status,0);assert.equal(r.stdout.length,0);
--- a/packages/cli/tests/case1-accept.test.mjs
+++ b/packages/cli/tests/case1-accept.test.mjs
@@ -14 +14 @@
-import { CASE_SUDO_SESSION } from "./case-sudo-fixture.mjs";
+import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
@@ -90 +90,2 @@
-case_sudo_run "$HC" "$ROOT_DIR$cmd" "\${args[@]}"`,
+[ ! -e "$ROOT_DIR$cmd" ] || cmd="$ROOT_DIR$cmd"
+case_sudo_run "$HC" "$cmd" "\${args[@]}"`,
@@ -100 +101,5 @@
-[ -f "$HC/peer-hang" ] && { echo "$BASHPID" > "$HC/peer-pid"; exec sleep 60; }
+[ -f "$HC/peer-hang" ] && {
+ here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here")
+ echo "$here" > "$HC/peer-pid"; printf '%s %s\\n' "$here" "$sid" >> "$HC/query-pids"
+ trap '' TERM HUP; exec sleep 60
+}
@@ -303,0 +309,58 @@
+test('row253 C4 acceptance query inner reason holds under both sudo models',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];
+ put(join(h.hc,'session-mode'),mode);
+ put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),blockingQuerySource(files[1]));
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
+ assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query inner-timeout\n');
+ assert.ok(seconds<7,`${mode}: ${seconds}s`);assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
+ assert.deepEqual(await pollFixtureGone(files),[]);await stop();
+ }
+});
+
+test('row253 C4 acceptance query outer reason discards stdout and names residual cleanup',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-config'),'');
+ put(join(h.hc,'stall-output'),JSON.stringify({dataRoot:HOME+'/.mosaic-dev',socket:true}));
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
+ assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query outer-timeout\n');
+ assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s preflight slack`);
+ assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
+ if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'R3 session remains before fixture cleanup');
+ killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit R3 fixture cleanup');await stop();
+ }
+});
+
+test('row253 C4 acceptance query overflow is output-limit in both models',async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);const capture=cleanupFixture(t,files);
+ put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:'${HOME}/.mosaic-dev'});`);
+ const r=run(h);capture();assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query output-limit\n');
+ assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);assert.deepEqual(await pollFixtureGone(files),[]);
+ }
+});
+
+for(const fault of ['inner','outer','overflow'])test(`row253 C4 P10 ${fault} reason holds under both sudo models`,async t=>{
+ for(const mode of ['exec','hop']){
+ const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];put(join(h.hc,'session-mode'),mode);
+ const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files,7000);
+ if(fault==='inner')put(join(h.hc,'peer-hang'),'');
+ if(fault==='outer'){put(join(h.hc,'stall-probe'),'');put(join(h.hc,'stall-output'),'200 997 997\n');}
+ if(fault==='overflow'){put(join(h.hc,'peer-output'),'200 997 997\n'+'X'.repeat(256));put(join(h.hc,'peer-status'),'0');}
+ const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
+ assert.equal(r.status,0,r.out+r.err);
+ const reason=fault==='inner'?'inner-timeout':fault==='outer'?'outer-timeout':'output-limit';
+ assert.match(r.out,new RegExp('P10 tester-socket-squat unnamed boundary=failed reason=probe-'+reason+' '));
+ assert.doesNotMatch(r.out,/boundary=observed/);
+ if(fault==='inner')assert.ok(seconds<4,`${mode}: ${seconds}s includes functional checks`);
+ if(fault==='outer'){
+ assert.ok(seconds>=4&&seconds<6,`${mode}: ${seconds}s; 2s functional-check slack`);
+ if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'stalled supervisor is R3 residual');
+ for(const file of files)killRecordedPids(file);
+ }
+ assert.deepEqual(await pollFixtureGone(files),[],fault==='outer'?'explicit R3 cleanup':mode);await stop();
+ }
+});
+
--- a/packages/webui/README.md
+++ b/packages/webui/README.md
@@ -128 +128 @@
-need `--port`, and 7330, 7332 or 7333 as `--port` or `--tester-port` exits 4
+need `--port`, and 7330, 7332 or 7333 as the main or legacy tester port exits 4
@@ -130 +130 @@
-`--tester-port` that another installed Console unit in the unit directory
+legacy tester port that another installed Console unit in the unit directory
@@ -144 +144 @@
-`serve --sso [--send] [--tester-port N]` makes the Console an OpenID Connect
+`serve --sso [--send] [--tester-socket]` makes the Console an OpenID Connect
@@ -176 +176 @@
-- With `--tester-port`, the tester listener refuses any request carrying an
+- With `--tester-socket`, the tester listener refuses any request carrying an
--- a/docs/plans/DEFERRED.md
+++ b/docs/plans/DEFERRED.md
@@ -273,14 +272,0 @@
-- **Gate runs mount the host's pi auth file.** `compose.yaml:43` mounts
- `${PI_AUTH_FILE:-/home/jwoltje/.pi/agent/auth.json}` read-only into
- the agent container. No suite sets `PI_AUTH_FILE`, so the
- `test-task.sh` cases that call `scripts/run-task.sh`, and
- `test-release.sh` through `release.sh` `health_check` (`:41-54`), run
- Compose with the host file. Row 197's lane `cleanEnv`
- (`scripts/testing/gate-lane.mjs:109`) keeps the default too: it neither
- sets nor passes `PI_AUTH_FILE`. The mount is read-only, and the legacy
- gate already worked this way before row 197. `test-auth.sh` uses its
- own fixtures but calls `auth.sh`, not Compose. The fix: a fixture
- `PI_AUTH_FILE` for the Compose-reaching cases, set by the suites or by
- the lane's `cleanEnv`, and a test that no gate step mounts the host
- default. It needs its own row. Webby N4 on the row 197 proof driver
- (bba870ee9), traced by Herb (herb-0051). (2026-10-21)
@@ -362,0 +349,34 @@
+- **Console library routes: four library codes still map to 502.**
+ (Dewey N2 on the row 254 manifest, #1759 comment 30173.) Row 254
+ gave six library codes a STATUS entry in `packages/webui/src/bus.mjs`.
+ `library-mapping-invalid`, `library-remote-invalid`,
+ `library-state-invalid` and `library-git` still fall through
+ `busStatus` to 502. The row that adds the Console library routes
+ decides their statuses and gives its `humanCall` for `library.sync`
+ a timeout over 130 seconds. (2026-10-21)
+- **T27 sweep test leaves two `sleep 60` children running.**
+ (Darkwing row 251 slot, darkwing-0113 and darkwing-0117; Dewey on
+ r2, comment 30168.) The sweep test spawns two detached shells,
+ `planted` at `host-run.test.mjs:528` and `decoy` at `:536`, each
+ running `sleep 60; :` with the run dir as `$0`. `sweepRunDir` kills
+ the planted shell by pid (`host-run.mjs:238`), and `t.after` kills
+ only the decoy shell. Neither kill reaches the `sleep` child, so each
+ one lives for up to 60 s after the test and trips any busy check that
+ follows. The fix is to kill the process group in both `t.after`
+ hooks: `process.kill(-pid, 'SIGKILL')`. Each shell is spawned
+ detached, so it leads its own group. `exec sleep 60` doesn't work.
+ The sweep matches the run dir in `/proc/<pid>/cmdline`
+ (`host-run.mjs:236`), and exec drops the `$0` that carries it, so the
+ planted process would survive and the "planted process killed" assert
+ would fail. This binds the next row that touches
+ `packages/bus/tests/host-run.test.mjs`. (2026-10-21)
+- **Real-model suite cases run outside the gate.** Row 244 moved
+ `test-task.sh` fork recall (M11) and user recall (M16) behind
+ `MOSAIC_LIVE_PROVIDER=1`, and `test-release.sh` activations to the mock
+ adapter. Before any re-pin whose tree changes `packages/harness` or the
+ session fork code (`scripts/mosaic-task.mjs` session and
+ `sessionForkFrom` handling, the pi adapter), an operator runs
+ `MOSAIC_LIVE_PROVIDER=1 bash scripts/test-task.sh` with the real pi
+ credential and a real `release.sh activate`, and records both results on
+ the re-pin row. Morgana's wording (row 244 BUILD.md), Webby N2 on row 244
+ r1 (#1757). (2026-10-21)
@@ -427,0 +448,14 @@
+- Gate runs mount the host's pi auth file (Webby N4 on the row 197 proof
+ driver, 059577886): closed by row 244 (#1757), landing e69260a3c. The
+ suites and the lane's `cleanEnv` export a 0600 fixture `PI_AUTH_FILE`,
+ `guardDocker` refuses a Compose run without one, and
+ `packages/cli/tests/gate-lane-auth.test.mjs` proves no gate step mounts
+ the host default. The real-model cases this moved off the gate are open
+ above.
+
+## Row253 F5: legacy Case AS_USER supervision
+
+Tracked as row 276, Gitea #1781 (sage-2331), owner Gander, reviewer Duckworth, after row253.
+Brief: docs/plans/2026-10-22_case-sudo-legacy-followup.md, “Brief: supervised legacy AS_USER calls”.
+
+case1-accept.sh's legacy timeout330 around AS_USER talk and its AS_USER precheck, Claude version, PM launches and Node version diagnostics predate the new socket metadata/probe helpers. sudo's pty monitor can move them to a separate session; the old outer timeout does not prove their descendants are killed. The C4 repair in row253 covers only its new config query and peer probe. These legacy paths need the reviewed supervisor or a header explicitly explaining why each remains unsupervised. The follow-up must preserve the 300-second talk/reply contract, concurrent F5 scan, diagnostics and secret-output refusals, with fake-only session-hop tests and Sage's slot. No host or live unit change is authorized.
@@ -1,275 +0,0 @@
[
{
"id": "M1",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "ec09134dd5e735d5e2d3c9a92162a6d0dd9cc2e1718cf2e2efd3d508a7db3d13"
},
{
"id": "M2",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "5757898fb9003312b67c98037a524dcf06a8dc43e07572a21aae1afb0067e0ff"
},
{
"id": "M3",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "e9df1616060cb45e228e8c3a633299cbd2cc8e8d913544678eab3299bb3a8548"
},
{
"id": "M4",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "3cfe29b917726d13819fabc390845df1367423bb251d3cce2cd270164fe143d0"
},
{
"id": "M5",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "41f91ef6413543602ef46f412e8e289a6dd5ce24bd271cebba05c819242ebe56"
},
{
"id": "M6",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "19d71aed6a102e519f97151ca83d01dc9534b7c4722b1365b0fc365daac4bba9"
},
{
"id": "M7",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "7c629e0da6101109cd9ae7f8028fd68bb16a43e2b799ee0c8de87f13e1055652"
},
{
"id": "M8",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "dcc4a888a21c8a6cbfbfbe48df892c81b8fd038868e0dcd9b1acabf2dceb5313"
},
{
"id": "M9",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "2dccf96df3518c4e604e36245e62a2d446719e1ae1bd5bd47890fc692acad43c"
},
{
"id": "M10",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "ba59a37dac46254f148a0f40367186172ca9e8bbb98020a0c97d7d325c5e86e0"
},
{
"id": "M11",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "58f9e51f48d41c0887894f34e278937911c380d18a63770342a8e1afd1f5dc59"
},
{
"id": "M12",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "3a755dd95af1d11f0cc0bf1ae59a676b62ee7d7b72174ebb88388c6d48fa41ff"
},
{
"id": "M13",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "b230bc14a1197c9986ddb99689a0b0dc71453903f1a6a07328c9d10ceec6ff30"
},
{
"id": "M14",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "0179e9d29cc9461b665e807b00e683ce30f30e2903efc1f8ec4a1599876681f4"
},
{
"id": "M15",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "1d93cd35e92a079cbe31b71a69314fd98d1547e35d62c9a152a5fa4a67c873eb"
},
{
"id": "M16",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5c60833fa9e7fa40bfdd10290cc3e91c7476b5b1f0975367760bbedddbf91c19"
},
{
"id": "M17",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "845c7ccf5c178fb783cbf8c88d329780a0f9e4c63a2c262b6473f0eebd0f0c5e"
},
{
"id": "M18",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "ab32a811142fd84de78fe93ac541b6ee823d7d9422f816e6bd15035149a32ac2"
},
{
"id": "M19",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5ee29b36b15b45f031a6651e82898d6bb1a5ebeff4b47c98b5777f00ab535ed3"
},
{
"id": "M20",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "1c85dc5f63d4766226bb13b701b0358536e7a2a918b224841650acf244a0638a"
},
{
"id": "M21",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "b13942590fb6f518446302340c66465a16ba9ab9e4429f6fcc01714cbc53c63e"
},
{
"id": "M22",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5a87dc77e3bba09ecb9f6b90c653cbfa38117c13674c770c0af0d941c2c63fe1"
},
{
"id": "M23",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "2b7d4d59bfe4ec8f2c9d7c5562be1c57a4ecc6e4d0b4960c1ae44958f2ad8dd9"
},
{
"id": "M24",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "e0f388ef748d5e6a3df40ac486891c14d91fcafdb8ce03b36ef20f93aebbdfe2"
},
{
"id": "M25",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "5ba47cf8b8a045967887d108d71a7456cba89e73c096cbdfbf76e010b4d3c5f2"
},
{
"id": "M26",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "cca6f1d39fde22603cbd649d6712c0e05041bd1a86eb7cd9b1c410f58c734a09"
},
{
"id": "M27",
"file": "scripts/case1-accept.sh",
"occurrences": 1,
"before": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
"after": "8b6b71c84ca96f3caf6e832988573a608fccd3c74fa7798468565d0b8b202c5a"
},
{
"id": "M28",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "25e1d996be7c62091b1a3a837fd8fe95871bc01f2a2d509eb1ae09785b51ea48"
},
{
"id": "M29",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "eb5e9d9a069ce226ab70baef11dc7897ae4e3d44ac4807cbed838ba1ded47494"
},
{
"id": "M30",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "280ba5c20d3b42423d681e69312731aa60b2396b1605e875a55d3e87d908eba1"
},
{
"id": "M31",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "2884a7d730d0357e4545cfccd446cad1fb5a98f7f1d86698f778063b0025b451"
},
{
"id": "M32",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "51b7bce4b8f88de20c68797afac71e4dd43d29119e68bcf65cb056f630a87011"
},
{
"id": "M33",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "dc86d9be155f56667ddabf5183d757b55fa339ff86af40ce039f0d63e6e228a6"
},
{
"id": "M34",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "3ef45b6044db0722ff25bd85260c818be9217cf43ea26be0e3f77872af0e70b1"
},
{
"id": "M35",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "c3de12c5a897407a2e0443536bfe5c97d4cefb757f23a5ba301b9479400c09d5"
},
{
"id": "M36",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "d5326f6da46ca4eae3b4043d2efa0a66d280627e4e652da5a62b860d6c6f2a72"
},
{
"id": "M37",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "f7d758bcd9752e6d7e6580ac935982ec9e9ff68b212c0be3d584a9b9757b9d5f"
},
{
"id": "M38",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "bf1796db389b3381dd5cec1fb9a64f493b1e36188462e531cad6fbba08775cd2"
},
{
"id": "M39",
"file": "scripts/case2-unit.sh",
"occurrences": 1,
"before": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
"after": "dbff45cd7878b4feae890a5b5d8bfdf9d95e151a9583e0d11a6802a4a7c83a45"
}
]
@@ -1,12 +0,0 @@
{
"968db0bcd..ddfb29af7": [
"docs/plans/DEFERRED.md",
"packages/cli/tests/case-sudo-fixture.mjs",
"packages/cli/tests/case1-accept.test.mjs",
"packages/cli/tests/case2-unit.test.mjs"
],
"c3d88840c..ddfb29af7": [
"docs/plans/DEFERRED.md"
],
"interpretation": "The branch-to-refactor comparison includes our unlanded test additions. From the actual branch base, only DEFERRED.md overlaps. Preserved upstream DEFERRED.md and appended the row 276 / #1781 entry. No rebase performed."
}
@@ -1,142 +0,0 @@
// Shared fake process model for the two Case host-script tests. No real sudo.
// Call case_sudo_run RECORD_DIR COMMAND... after the caller's argv checks.
// Missing session-mode preserves the original exec-style fake.
import { spawn } from 'node:child_process';
import { readFileSync, readdirSync } from 'node:fs';
export const CASE_SUDO_SESSION = String.raw`
case_sudo_entry() {
local record="$1" here sid; shift
here="$BASHPID"
read -r sid < <(/bin/ps -o sid= -p "$here")
printf "%s %s\n" "$here" "$sid" >> "$record/session-pids"
if { [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
{ [ -f "$record/stall-probe" ] && [[ "$*" == *SO_PEERCRED* ]]; }; then
trap '' TERM HUP
[ ! -f "$record/stall-output" ] || /bin/cat "$record/stall-output"
exec /bin/sleep 60
fi
exec "$@"
}
case_sudo_run() {
local record="$1" child="" rc=0 mode=exec; shift
[ ! -f "$record/session-mode" ] || mode="$(<"$record/session-mode")"
if [ "$mode" != hop ]; then case_sudo_entry "$record" "$@"; return "$?"; fi
# The fake stays as parent, just as sudo does on its pty path. KILL cannot
# be caught or forwarded. The child creates the separate terminal session.
trap '[ -z "$child" ] || kill -TERM -- "-$child" 2>/dev/null || :' TERM
trap '[ -z "$child" ] || kill -HUP -- "-$child" 2>/dev/null || :' HUP
trap '[ -z "$child" ] || kill -INT -- "-$child" 2>/dev/null || :' INT
export -f case_sudo_entry
/usr/bin/setsid /bin/bash -c 'case_sudo_entry "$@"' -- "$record" "$@" &
child=$!
# An interrupted wait returns while the child still runs; keep waiting.
while :; do
wait "$child"; rc=$?
kill -0 "$child" 2>/dev/null || break
done
return "$rc"
}
`;
export function liveRecordedSessions(recordFile, readFile, procDirs) {
// Exec fake entries share the test runner's session. Only self-led sessions
// are isolated and safe to scan here; exec coverage also checks exact PIDs.
const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number)).filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
const live=[];
for(const entry of procDirs()){
if(!/^\d+$/.test(entry))continue;
try{
const stat=readFile(`/proc/${entry}/stat`), fields=stat.slice(stat.lastIndexOf(') ')+2).split(' ');
if(sessions.has(Number(fields[3])) && fields[0]!=='Z') live.push(Number(entry));
}catch{}
}
return live;
}
export function recordedPids(file) {
try{return readFileSync(file,'utf8').trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number));}
catch(error){if(error.code==='ENOENT')return [];throw error;}
}
export function recordedLivePids(file) {
return sessionPairs(recordedPids(file)).filter(([pid,sid])=>{
try{
const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
return Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X';
}catch(error){if(error.code==='ENOENT'||error.code==='ESRCH')return false;throw error;}
}).map(([pid])=>pid);
}
export function killRecordedPids(file) {
killPairs(recordedPids(file));
}
function killPairs(pairs) {
for(const [pid,sid] of sessionPairs(pairs)){
try{
const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
if(Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X')process.kill(pid,'SIGKILL');
}catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
}
}
function sessionPairs(pairs) {
// Only self-led records authorize a session scan. Exec-mode records belong
// to the runner's session and authorize checking their exact PIDs only.
const sessions=new Set(pairs.filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
const found=new Map(pairs.map(pair=>[pair[0],pair]));
if(sessions.size)for(const entry of readdirSync('/proc')){
if(!/^\d+$/.test(entry))continue;
try{
const raw=readFileSync(`/proc/${entry}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' '),sid=Number(fields[3]);
if(sessions.has(sid))found.set(Number(entry),[Number(entry),sid]);
}catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
}
return [...found.values()];
}
// Safety net for a removed-outer-bound change. It is fixture cleanup, never
// product supervision. The guard reads only fixture records and checks sid
// immediately before every kill. Normal outer-bound cases return before it.
export function fixtureGuard(t,files,milliseconds=9500) {
const program=`const fs=require('node:fs');setTimeout(()=>{for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}},Number(process.argv[2]));`;
const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds)],{stdio:'ignore'});
const stop=async()=>{
if(child.exitCode!==null||child.signalCode!==null)return;
const exited=new Promise(resolve=>child.once('exit',resolve));
child.kill('SIGKILL');await exited;
};
t.after(stop);return stop;
}
export function cleanupFixture(t,files) {
let captured=[];
const capture=()=>{captured=files.flatMap(recordedPids);};
// tmp(t)'s directory-removal hook may run first. Retain PID/SID pairs in
// memory before assertions, then verify the kernel sid again before killing.
t.after(()=>killPairs([...captured,...files.flatMap(recordedPids)]));
return capture;
}
export async function pollFixtureGone(files) {
for(let n=0;n<50;n++){
if(files.every(file=>recordedLivePids(file).length===0))return [];
await new Promise(resolve=>setTimeout(resolve,10));
}
return files.flatMap(recordedLivePids);
}
export function blockingQuerySource(records,kind='signals',member,proof) {
const py=`import os,sys\nos.setpgid(0,0)\nwith open(sys.argv[2],"a") as f:f.write(str(os.getpid())+" "+str(os.getsid(0))+chr(10))\nos.execv(sys.argv[1],[sys.argv[1],"60"])`;
return `import fs from 'node:fs';import {spawn} from 'node:child_process';
const records=${JSON.stringify(records)};
function record(pid){const s=fs.readFileSync('/proc/'+pid+'/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);fs.appendFileSync(records,pid+' '+sid+String.fromCharCode(10));}
record(process.pid);
export function loadSystem(){
${kind==='sleep'?`const c=spawn('/usr/bin/sleep',['60'],{stdio:'inherit'});record(c.pid);`:''}
${kind==='signals'?`process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});`:''}
${kind==='member'?`const c=spawn('/usr/bin/python3',['-I','-S','-c',${JSON.stringify(py)},${JSON.stringify(member)},records],{stdio:'inherit'});for(let n=0;n<200;n++){try{const s=fs.readFileSync('/proc/'+c.pid+'/stat','utf8');if(s.includes('('+${JSON.stringify(member?.split('/').at(-1))}+') ')){fs.writeFileSync(${JSON.stringify(proof)},s);break;}}catch{}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,5);}`:''}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
}`;
}
@@ -1,707 +0,0 @@
#!/usr/bin/env bash
# Row 195 (#1693): the Case 1 acceptance on 10.1.1.53, design r7 section 5
# (agents/darkwing/work/queue-195/DESIGN.md). Deviations and decisions: agents/darkwing/work/queue-195/BUILD.md.
#
# case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N
#
# Root runs it on the Case 1 host after the install window, from a root-owned checkout. It reads the
# user, home, copy, node, claude, main port and tester socket marker from the args file, which must pass stack-upgrade.sh's
# rules and be a factory file for --business. Every command that runs the stack's code runs as
# `sudo -u USER -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin` from /. Root itself reads ss,
# /proc, the account database and file metadata, and runs these: systemctl show and is-enabled (F1,
# P11), curl on the Console's loopback healthz (F2), sudo -l (P1, P3), sudo -u jarvis bwrap --version
# and hostcheck.sh --check (P9), and node to parse managed-settings.json (F9). F9 runs node only when
# --node is the pinned path and it and every directory above it are root's with no group or other
# write. The script changes nothing on the host but the message F3 sends and its reply, and it never
# opens claude-session.env. A bounded config/target query imports the copy only as the stack
# user, with sudo and an empty environment. Root parses its two facts in bash. P10 uses the
# fixed system Python, also as the stack user, for a read-only SO_PEERCRED observation.
# The config/probe supervisor is fixed root-validated system Bash plus setsid
# and sleep, after the uid drop: query inner/outer 5/7 s, probe 2/4 s. Root
# independently bounds its sudo wait. Legacy AS_USER talk and diagnostics below
# retain their old behavior and are NOT covered by this supervisor (F5 follow-up).
#
# One line per check, `<id> <path> <mark> <evidence>`, then `count <mark> N` for each mark and a
# result line. F checks are `pass` or `FAIL`. P checks are `closed`, `accepted <ruling>` or
# `unnamed`, which means no Q182, Q183 or Mos ruling names the path; each unnamed line goes to Mos.
#
# Exit 0 when every F check passes, 1 when one fails, 2 on usage or a refusal (a bad args file, or
# --root misused). A P check never changes the exit.
#
# Test seam, for packages/cli/tests/case1-accept.test.mjs only. Root takes none:
# --root DIR prefixes the args file, /etc, /proc, the stack's home, the host check and node, and
# reads the account database from DIR/etc/passwd and DIR/etc/group instead of getent
set -uo pipefail
usage() { echo "usage: case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N" >&2; exit 2; }
refuse() { echo "case1-accept: refusing: $*" >&2; exit 2; }
BUSINESS="" ARGS_FILE="" CLAUDE_PIN="" NODE_PIN="" ROOT=""
while [ $# -gt 0 ]; do
case "$1" in
--business) [ $# -ge 2 ] && [[ "$2" =~ ^[a-z0-9][a-z0-9-]*$ ]] || usage; BUSINESS="$2"; shift 2 ;;
--args) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] || usage; ARGS_FILE="$2"; shift 2 ;;
--claude-pin) [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; CLAUDE_PIN="$2"; shift 2 ;;
--node-pin) [ $# -ge 2 ] && [[ "$2" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; NODE_PIN="$2"; shift 2 ;;
--root) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [ -d "$2" ] || usage; ROOT="${2%/}"; shift 2 ;;
*) usage ;;
esac
done
[ -n "$BUSINESS" ] && [ -n "$ARGS_FILE" ] && [ -n "$CLAUDE_PIN" ] && [ -n "$NODE_PIN" ] || usage
if [ "$(id -u)" = 0 ]; then
[ -z "$ROOT" ] || refuse "--root is a test seam, and root takes none"
PATH=/usr/sbin:/usr/bin:/sbin:/bin
else
[ -n "$ROOT" ] || refuse "run it as root"
fi
cd / || refuse "cannot cd to /"
# --- The args file, by stack-upgrade.sh's read_args rules, plus the serve ports ---
f="$ROOT$ARGS_FILE"
[ -f "$f" ] && [ ! -L "$f" ] || refuse "no regular file $ARGS_FILE"
st="$(stat -c '%u %a' -- "$f")" || refuse "cannot stat $ARGS_FILE"
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) ||
refuse "$ARGS_FILE must be $(id -un)'s with no group or other write (it is uid ${st% *}, mode ${st#* })"
mapfile -t ARGS < "$f" || refuse "cannot read $ARGS_FILE"
S_USER="" S_HOME="" A_BUSINESS="" NODE="" COPY_ID="" CLAUDE="" PROFILE="" PORT="" TESTER_SOCKET=0
copies=0 past=""
for ((i = 0; i < ${#ARGS[@]}; i++)); do
a="${ARGS[i]}" v="${ARGS[i + 1]:-}"
if [ -n "$past" ]; then
case "$a" in
--port) PORT="$v"; i=$((i + 1)) ;;
--business) [ "$v" = "$BUSINESS" ] || refuse "serve business differs"; i=$((i + 1)) ;;
--tester-socket) TESTER_SOCKET=$((TESTER_SOCKET + 1)) ;;
--tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;;
*) refuse "unknown serve flag $a" ;;
esac
continue
fi
case "$a" in
--) past=1 ;;
--pm) ;;
--user) S_USER="$v"; i=$((i + 1)) ;;
--home) S_HOME="$v"; i=$((i + 1)) ;;
--business) A_BUSINESS="$v"; i=$((i + 1)) ;;
--node) NODE="$v"; i=$((i + 1)) ;;
--claude) CLAUDE="$v"; i=$((i + 1)) ;;
--profile) PROFILE="$v"; i=$((i + 1)) ;;
--copy) COPY_ID="$v"; copies=$((copies + 1)); i=$((i + 1)) ;;
*) refuse "$ARGS_FILE has $a before --, which case2-unit.sh does not take" ;;
esac
done
[ "$copies" = 1 ] || refuse "$ARGS_FILE needs exactly one --copy before --, not $copies"
[[ "$S_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] && [[ "$S_HOME" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$A_BUSINESS" =~ ^[a-z0-9][a-z0-9-]*$ ]] &&
[[ "$NODE" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] && [[ "$CLAUDE" =~ ^/[A-Za-z0-9/._+@-]+$ ]] ||
refuse "$ARGS_FILE lacks a plain --user, --home, --business, --node, --claude or --copy"
[ "$PROFILE" = factory ] || refuse "$ARGS_FILE has --profile '${PROFILE}', not factory"
[ "$A_BUSINESS" = "$BUSINESS" ] || refuse "$ARGS_FILE is for business $A_BUSINESS, not $BUSINESS"
[[ "$PORT" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$PORT" -le 1023 ] || refuse "$ARGS_FILE lacks a privileged numeric --port after --"
[ "$TESTER_SOCKET" = 1 ] || refuse "$ARGS_FILE needs one bare --tester-socket after --"
COPY="$S_HOME/.local/share/mosaic-stack/copies/$COPY_ID"
DATA_ROOT="$S_HOME/.mosaic-dev"
CRED="$S_HOME/.config/mosaic-dev/secrets/claude-session.env"
BUSINESS_FILE="$S_HOME/.config/mosaic-dev/businesses/$BUSINESS.json"
BUS_UNIT="mosaic-bus@$BUSINESS.service" CON_UNIT=mosaic-webui.service
HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh
SETTINGS=/etc/claude-code/managed-settings.json
LABEL_OUTER='bwrap (enforce)' LABEL_CHILD='bwrap//&unpriv_bwrap (enforce)'
AS_USER=(sudo -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin")
WORK="$(mktemp -d)" || refuse "cannot make a temp dir"
trap 'rm -rf -- "$WORK"' EXIT
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
# row253-supervisor-begin
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
stat_fields() {
local pid="$1" raw rest
if ! raw="$(<"/proc/$pid/stat")" 2>/dev/null; then
[ -e "/proc/$pid/stat" ] && return 1
return 3
fi
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
[[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]]
}
clock_cs() {
local boot spare
read -r boot spare < /proc/uptime || return 1
[[ "$boot" =~ ^([0-9]+)\.([0-9]{2})$ ]] || return 1
CLOCK_CS=$((10#${BASH_REMATCH[1]} * 100 + 10#${BASH_REMATCH[2]}))
}
clean_session() {
local entry pid rc live until
clock_cs || return 1
until=$((CLOCK_CS + 25))
while :; do
live=0
for entry in /proc/[0-9]*/stat; do
pid="${entry%/stat}"; pid="${pid##*/}"
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" != 3 ] || continue
[ "$rc" = 0 ] || return 1
[ "$ST_SESSION" = "$child" ] || continue
case "$ST_STATE" in Z|X) continue ;; esac
live=1
if ! kill -KILL "$pid" 2>/dev/null; then
rc=0; stat_fields "$pid" || rc=$?
[ "$rc" = 3 ] || { [ "$rc" = 0 ] && { [ "$ST_STATE" = Z ] || [ "$ST_STATE" = X ]; }; } || return 1
fi
done
[ "$live" != 0 ] || return 0
clock_cs || return 1
[ "$CLOCK_CS" -lt "$until" ] || return 1
"$sleep_bin" 0.01
done
}
finish() {
local rc="$?"
trap - EXIT HUP INT TERM
if [ "$verified" = 1 ]; then
clean_session || rc=86
elif [ -n "$child" ]; then
kill -KILL "$child" 2>/dev/null || :
fi
[ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
"$setsid_bin" "$@" & child=$!
for ((attempt=0; attempt<100; attempt++)); do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 85
if [ "$ST_SESSION" = "$child" ]; then verified=1; break; fi
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
tester_tools() {
local input target p st magic
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
p="$target"
while :; do
st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1
[ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1
[ "$p" != / ] || break
p="${p%/*}"; [ -n "$p" ] || p=/
done
magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
case "$input" in
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
/usr/bin/head) TS_HEAD="$target" ;;
esac
done
}
tester_capture_reason() {
case "$1" in
81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;;
84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;;
*) echo inner-command ;;
esac
}
tester_capture() (
local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6
tester_tools || exit 84
file="$(mktemp)" || exit 88
notice="$(mktemp)" || { rm -f -- "$file"; exit 88; }
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT
trap 'exit 88' HUP INT TERM
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
set -o pipefail
maximum="$1"; head_bin="$2"; shift 2
"$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))"
' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" &
child=$!
wait "$child" 2>/dev/null || rc=$?
kill -KILL -- "-$child" 2>/dev/null || :
# Only GNU timeout and the trusted head can write this root-side channel.
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi
[ "$(wc -c < "$file")" -le "$maximum" ] || exit 83
[ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; }
cat -- "$file"
)
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
[[ "$1" =~ $pattern ]] || return 1
TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}"
}
tester_location() {
local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st
TS_REASON="" TS_SOCKET=""
case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac
case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac
case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac
[ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; }
case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac
case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac
TS_SOCKET="$data/console/tester.sock"
[ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; }
local -a parts
IFS=/ read -r -a parts <<< "$data"
for part in "${parts[@]}"; do
[ -n "$part" ] || continue
at="$at/$part"
[ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; }
if [ -e "$prefix$at" ]; then
[ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; }
fi
done
[ "${5:-}" != observe ] || return 0
local parent="$prefix$data/console"
[ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; }
if [ -e "$parent" ]; then
[ -d "$parent" ] || { TS_REASON=parent-type; return 1; }
st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; }
[ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; }
[[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; }
fi
}
# tester-location-end
# --- Output ---
MARKS=(pass FAIL closed "accepted Q182+Q183" "accepted Q183" "accepted mos-1096" unnamed)
declare -A COUNT=()
F_FAILED=0
# Text the stack's user wrote, made safe to print: printable ASCII, one line, at most 200 bytes.
clean() { tr -c '[:print:]' ' ' <<< "$1" | cut -c1-200 | sed 's/ *$//'; }
line() { local id="$1" path="$2" mark="$3"; shift 3; echo "$id $path $mark $*"; COUNT[$mark]=$(( ${COUNT[$mark]:-0} + 1 )); }
fpass() { line "$1" "$2" pass "${@:3}"; }
ffail() { line "$1" "$2" FAIL "${@:3}"; F_FAILED=1; }
# --- The account database: getent on the host, the seam's files under --root ---
db() {
if [ -n "$ROOT" ]; then
if [ $# -gt 1 ]; then awk -F: -v k="$2" '$1 == k' "$ROOT/etc/$1"; else cat "$ROOT/etc/$1"; fi
else
getent "$@"
fi
}
uid_of() { db passwd "$1" | cut -d: -f3; }
gid_of() { db passwd "$1" | cut -d: -f4; }
# The group ids USER belongs to, as `id -G USER` prints them: the primary gid, then each group
# whose member list names USER.
groups_of() {
local g; g="$(gid_of "$1")"
[ -n "$g" ] || return 0
{ echo "$g"; db group | awk -F: -v u="$1" '{ n = split($4, m, ","); for (i = 1; i <= n; i++) if (m[i] == u) print $3 }'; } | awk '!seen[$0]++'
}
names_of_groups() { local g; for g in "$@"; do db group | awk -F: -v g="$g" '$3 == g { print $1 }'; done; }
MOSAIC_UID="$(uid_of "$S_USER")"
[[ "$MOSAIC_UID" =~ ^[1-9][0-9]*$ ]] || refuse "stack account has no non-root uid"
[ "$(db passwd "$S_USER" | cut -d: -f6)" = "$S_HOME" ] || refuse "saved home differs from account home"
QUERY="$(tester_capture 5 7 4096 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query $(tester_capture_reason "$?")"
tester_facts "$QUERY" || refuse "tester config query has invalid facts"
[ "$TS_MARKED" = true ] || refuse "target does not support --tester-socket"
tester_location "$TS_DATA" "$S_HOME" "$MOSAIC_UID" "$ROOT" observe || refuse "tester path refuses $TS_REASON"
TESTER_PATH="$TS_SOCKET"
# --- systemd, /proc and listeners ---
# KEY=VALUE lines for UNIT's properties.
unit_show() { local u="$1"; shift; systemctl show "$u" "${@/#/-p}" 2>/dev/null; }
prop() { sed -n "s/^$2=//p" <<< "$1" | tail -n 1; }
# The real, effective, saved and filesystem uids of PID, space-separated.
proc_uids() { awk '$1 == "Uid:" { print $2, $3, $4, $5 }' "$ROOT/proc/$1/status" 2>/dev/null; }
proc_label() { local l; l="$(cat "$ROOT/proc/$1/attr/current" 2>/dev/null)" || return 1; printf '%s' "${l%$'\n'}"; }
# One `ADDRESS PIDS` line per TCP listener on port $1, PIDS comma-separated.
listeners() {
ss -Hltnp 2>/dev/null | awk -v p="$1" '{
n = split($4, a, ":"); if (a[n] != p) next
addr = substr($4, 1, length($4) - length(p) - 1); pids = ""; s = $0
while (match(s, /pid=[0-9]+/)) { pids = pids (pids == "" ? "" : ",") substr(s, RSTART + 4, RLENGTH - 4); s = substr(s, RSTART + RLENGTH) }
print addr, pids
}'
}
# 0 when port $1 has a listener and every one is on 127.0.0.1, held by pid $2 alone.
loopback_by() {
local addr pids p any=0
[ -n "$2" ] && [ "$2" != 0 ] || return 1
while read -r addr pids; do
[ -n "$addr" ] || continue
any=1
[ "$addr" = 127.0.0.1 ] || return 1
for p in ${pids//,/ }; do [ "$p" = "$2" ] || return 1; done
[ -n "$pids" ] || return 1
done <<< "$(listeners "$1")"
[ "$any" = 1 ]
}
listener_text() { local t; t="$(listeners "$1" | tr '\n' ';')"; echo "${t:-none}"; }
BUS_SHOW="$(unit_show "$BUS_UNIT" ActiveState User MainPID)"
CON_SHOW="$(unit_show "$CON_UNIT" ActiveState User MainPID)"
CON_PID="$(prop "$CON_SHOW" MainPID)"
# --- F checks ---
check_f1() {
local u show state user pid uids why=""
for u in "$BUS_UNIT" "$CON_UNIT"; do
if [ "$u" = "$BUS_UNIT" ]; then show="$BUS_SHOW"; else show="$CON_SHOW"; fi
state="$(prop "$show" ActiveState)" user="$(prop "$show" User)" pid="$(prop "$show" MainPID)"
uids="$(proc_uids "${pid:-0}")"
[ "$state" = active ] || { why+="$u is ${state:-unknown}; "; continue; }
[ "$user" = "$S_USER" ] || { why+="$u has User=${user:-unset}; "; continue; }
[ -n "$MOSAIC_UID" ] && [ "$uids" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] ||
why+="$u MainPID ${pid:-none} has uids '${uids:-unreadable}', not $S_USER's ${MOSAIC_UID:-unknown}; "
done
if [ -z "$why" ]; then fpass F1 units-user "$BUS_UNIT and $CON_UNIT are active as $S_USER (uid $MOSAIC_UID)"
else ffail F1 units-user "${why%; }"; fi
}
check_f2() {
local rc=0
curl -fsS --max-time 10 -o /dev/null "http://127.0.0.1:$PORT/healthz" 2>/dev/null || rc=$?
if [ "$rc" != 0 ]; then ffail F2 console-loopback "curl http://127.0.0.1:$PORT/healthz exited $rc"
elif ! loopback_by "$PORT" "$CON_PID"; then ffail F2 console-loopback "port $PORT listeners are '$(listener_text "$PORT")', not 127.0.0.1 by the Console's MainPID ${CON_PID:-none}"
else fpass F2 console-loopback "healthz answers, and $PORT is bound to 127.0.0.1 only, by MainPID $CON_PID"; fi
}
# F5's scan: an outer /usr/bin/bwrap owned by the stack's user with a claude descendant. Sets F5_OK
# when one pair has both labels exactly, F5_WHY to the last mismatch otherwise.
F5_OK="" F5_WHY="no /usr/bin/bwrap process of $S_USER with a claude descendant was seen while the reply was pending"
scan_f5() {
local pid ppid uids kids q c argv0 comm lo lc
local -A parent=() children=() bw=()
# One read of every PPid line and one of every exe link, so a scan stays short on a busy host.
while read -r pid ppid; do
parent[$pid]="$ppid" children[$ppid]+=" $pid"
done < <(grep -H '^PPid:' "$ROOT"/proc/[0-9]*/status 2>/dev/null | sed -n 's#^.*/proc/\([0-9]*\)/status:PPid:[[:space:]]*\([0-9]*\)$#\1 \2#p')
while read -r pid; do
[[ "$pid" =~ ^[0-9]+$ ]] && [ -n "${parent[$pid]:-}" ] && bw[$pid]=1
done < <(find "$ROOT/proc" -mindepth 2 -maxdepth 2 -name exe -lname /usr/bin/bwrap -printf '%h\n' 2>/dev/null | sed 's#.*/##')
for pid in "${!bw[@]}"; do
[ -z "${bw[${parent[$pid]}]:-}" ] || continue
uids="$(proc_uids "$pid")"
[ -n "$MOSAIC_UID" ] && [ "${uids%% *}" = "$MOSAIC_UID" ] || continue
# Every descendant, breadth first.
kids=() q=("$pid")
while [ ${#q[@]} -gt 0 ]; do
for c in ${children[${q[0]}]:-}; do kids+=("$c") q+=("$c"); done
q=("${q[@]:1}")
done
for c in "${kids[@]}"; do
argv0="$(tr '\0' '\n' < "$ROOT/proc/$c/cmdline" 2>/dev/null | sed -n 1p)"
comm="$(cat "$ROOT/proc/$c/comm" 2>/dev/null)"
[[ "${argv0##*/}" == claude* ]] || [[ "$comm" == claude* ]] || continue
lo="$(proc_label "$pid")" lc="$(proc_label "$c")"
if [ "$lo" = "$LABEL_OUTER" ] && [ "$lc" = "$LABEL_CHILD" ]; then
F5_OK="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc'"; return 0
fi
F5_WHY="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc', not '$LABEL_OUTER' and '$LABEL_CHILD'"
done
done
return 1
}
# F3 and F5 together: the message goes out in the background and the scan runs until it is answered.
check_f3_f5() {
local marker text rc=0 out err n tpid
marker="case1-accept-$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"
text="$marker: this is the Case 1 acceptance; reply with one short line."
timeout 330 "${AS_USER[@]}" "$COPY/scripts/mosaic" talk chief "$text" --business "$BUSINESS" --tester --wait 300 > "$WORK/talk.out" 2> "$WORK/talk.err" &
tpid=$!
while :; do
scan_f5 && break
kill -0 "$tpid" 2>/dev/null || break
sleep 0.5
done
wait "$tpid" || rc=$?
# Both the sent line and the reply are on stdout; an error is on stderr.
out="$(cat "$WORK/talk.out")" err="$(cat "$WORK/talk.err")"
n="$(grep -c . <<< "$out")"
if [ "$rc" = 0 ] && grep -q "^sent [^ ]* to chief" <<< "$out" && [ "$n" -ge 2 ]; then
fpass F3 tester-reply "chief replied to $marker"
else
ffail F3 tester-reply "talk exited $rc with $n lines on stdout; last: $(clean "$(tail -n 1 <<< "${err:-$out}")")"
fi
if [ -n "$F5_OK" ]; then F5_LINE=(fpass "$F5_OK"); else F5_LINE=(ffail "$F5_WHY"); fi
}
check_f4() {
local rc=0 out
out="$("${AS_USER[@]}" "$COPY/scripts/case2-precheck.sh" --business "$BUSINESS" --profile factory 2>&1)" || rc=$?
if [ "$rc" = 0 ]; then fpass F4 precheck "case2-precheck.sh --profile factory exits 0"
else ffail F4 precheck "case2-precheck.sh --profile factory exited $rc; last: $(clean "$(tail -n 1 <<< "$out")")"; fi
}
check_f6() {
local want="/opt/mosaic-stack/claude-$CLAUDE_PIN/" v
[[ "$CLAUDE" == "$want"* ]] || { ffail F6 claude-pin "--claude $CLAUDE is not under $want"; return; }
[[ "$CLAUDE/" != */../* ]] || { ffail F6 claude-pin "--claude $CLAUDE has a .. segment"; return; }
v="$("${AS_USER[@]}" "$CLAUDE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$CLAUDE_PIN (Claude Code)" ]; then fpass F6 claude-pin "$CLAUDE prints '$v'"
else ffail F6 claude-pin "$CLAUDE --version prints '$(clean "$v")', not '$CLAUDE_PIN (Claude Code)'"; fi
}
# The PM check confirm runs (stack-release.sh pm_external): exactly one live launches list entry for
# launch.by, for this business. The stack's user runs the whole check and prints one result line.
F7_JS='
const {readFileSync}=require("node:fs");const {spawnSync}=require("node:child_process");
const [file,cli,b]=process.argv.slice(1);
let by;
try{by=JSON.parse(readFileSync(file,"utf8")).launch?.by}catch(e){console.log(`fail cannot read ${file}: ${e.code??"not JSON"}`);process.exit(0)}
if(typeof by!=="string"||!by){console.log("fail the business file has no launch.by");process.exit(0)}
const r=spawnSync(cli,["launches","list","--json"],{encoding:"utf8"});
if(r.status!==0){console.log(`fail launches list exited ${r.status}`);process.exit(0)}
let l;try{l=JSON.parse(r.stdout)}catch{console.log("fail launches list printed no JSON");process.exit(0)}
const live=Array.isArray(l)?l.filter((x)=>x&&x.instance===by&&x.live===true):[];
console.log(live.length!==1?`fail ${live.length} live launches of ${by}`:live[0].business!==b?`fail the live launch of ${by} is for ${live[0].business}`:`ok ${by} run ${live[0].run}`);'
check_f7() {
local out
out="$("${AS_USER[@]}" "$NODE" -e "$F7_JS" -- "$BUSINESS_FILE" "$COPY/scripts/mosaic" "$BUSINESS" 2>/dev/null | sed -n 1p)"
if [[ "$out" =~ ^ok\ [a-z0-9][a-z0-9-]*\ run\ [A-Za-z0-9._-]+$ ]]; then fpass F7 pm-live "one live launch: ${out#ok }"
else ffail F7 pm-live "$(clean "${out#fail }")"; fi
}
check_f8() {
local s
s="$(stat -c '%U %a %F' -- "$ROOT$CRED" 2>/dev/null)" || s="missing"
if [ ! -L "$ROOT$CRED" ] && [ "$s" = "$S_USER 600 regular file" ]; then fpass F8 credential-file "$CRED is '$s'"
else ffail F8 credential-file "$CRED is '$s', not '$S_USER 600 regular file'"; fi
}
# Empty when $1 and every directory above it, up to /, are the running user's (root's on the host) with
# no group or other write: case2-unit.sh's root_only rule for --node, checked again before root runs it.
# Otherwise the first path that breaks the rule. A symlink breaks it, because its mode is 777.
not_root_only() {
local p="$1" st
while :; do
st="$(stat -c '%u %a' -- "$ROOT$p" 2>/dev/null)" || { echo "$p (cannot stat)"; return; }
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) || { echo "$p (uid ${st% *}, mode ${st#* })"; return; }
[ "$p" = / ] && return
p="$(dirname -- "$p")"
done
}
check_f9() {
local s v bad
s="$(stat -c '%U:%G %a %F' -- "$ROOT$SETTINGS" 2>/dev/null)" || s="missing"
[ ! -L "$ROOT$SETTINGS" ] && [ "$s" = "root:root 644 regular file" ] || { ffail F9 autoupdater-off "$SETTINGS is '$s', not 'root:root 644 regular file'"; return; }
# Root runs node here, so it runs only the pinned path, and only when nothing but root can replace it.
[ "$NODE" = "/opt/mosaic-stack/node-$NODE_PIN/bin/node" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: it is not the --node-pin path"; return; }
bad="$(not_root_only "$NODE")"
[ -z "$bad" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: $bad is not root's alone"; return; }
v="$("$ROOT$NODE" -e 'const s=JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(JSON.stringify(s?.env?.DISABLE_AUTOUPDATER??null))' -- "$ROOT$SETTINGS" 2>/dev/null)" || v="unreadable"
if [ "$v" = '"1"' ]; then fpass F9 autoupdater-off "$SETTINGS is root:root 0644 with env.DISABLE_AUTOUPDATER \"1\""
else ffail F9 autoupdater-off "$SETTINGS env.DISABLE_AUTOUPDATER is $v, not \"1\""; fi
}
check_f10() {
local want="/opt/mosaic-stack/node-$NODE_PIN/bin/node" v
[ "$NODE" = "$want" ] || { ffail F10 node-pin "--node $NODE is not $want"; return; }
v="$("${AS_USER[@]}" "$NODE" --version 2>/dev/null | sed -n 1p)"
if [ "$v" = "$NODE_PIN" ]; then fpass F10 node-pin "$NODE prints $v"
else ffail F10 node-pin "$NODE --version prints '$(clean "$v")', not $NODE_PIN"; fi
}
check_f11() {
local d="$ROOT$DATA_ROOT/discord" found
if [ -L "$d" ]; then ffail F11 no-discord "$DATA_ROOT/discord is a symlink"; return; fi
[ -d "$d" ] || { fpass F11 no-discord "$DATA_ROOT/discord is absent"; return; }
found="$(find "$d" -maxdepth 1 -name '*.json' -printf '%f ' 2>/dev/null)"
if [ -z "$found" ]; then fpass F11 no-discord "$DATA_ROOT/discord holds no binding"
else ffail F11 no-discord "$DATA_ROOT/discord holds bindings: ${found% }"; fi
}
# --- P checks ---
check_p1() {
local out
out="$(sudo -l -U "$S_USER" 2>&1)"
if grep -q "is not allowed to run sudo" <<< "$out"; then line P1 "$S_USER-sudo" closed "sudo -l: $(clean "$(sed -n 1p <<< "$out")")"
else line P1 "$S_USER-sudo" unnamed "sudo -l: $(clean "$(tr '\n' ' ' <<< "$out")")"; fi
}
check_p2() {
local gs keys="$ROOT$S_HOME/.ssh/authorized_keys" kstate n
gs="$(groups_of "$S_USER" | tr '\n' ' ')"; gs="${gs% }"
n="$(wc -w <<< "$gs")"
if [ ! -e "$keys" ] && [ ! -L "$keys" ]; then kstate=absent
elif [ -f "$keys" ] && [ ! -L "$keys" ] && [ ! -s "$keys" ]; then kstate=empty
else kstate=present; fi
if [ "$n" = 1 ] && [ "$kstate" != present ]; then line P2 "$S_USER-groups-keys" closed "id -G is '$gs'; authorized_keys is $kstate"
else line P2 "$S_USER-groups-keys" unnamed "id -G is '$gs'; authorized_keys is $kstate"; fi
}
check_p3() {
local n
n="$(sudo -l -U localadmin 2>&1 | grep -c NOPASSWD)"
line P3 localadmin-sudo "accepted Q183" "sudo -l -U localadmin lists $n NOPASSWD lines"
}
check_p4() { line P4 pve-root-pbs "accepted Q182+Q183" "root on pve over VM 1008, and PBS backups that hold $S_HOME with the credential; this script can't see pve"; }
check_p5() {
local gs reach
if [ -z "$(uid_of jarvis)" ]; then line P5 jarvis-reach closed "no jarvis account"; return; fi
gs="$(names_of_groups $(groups_of jarvis) | tr '\n' ' ')"
reach="$(tr ' ' '\n' <<< "$gs" | grep -xE 'sudo|docker|lxd' | tr '\n' ' ')"
if [ -z "$reach" ]; then line P5 jarvis-reach closed "jarvis is in none of sudo, docker and lxd (groups: ${gs% })"
else line P5 jarvis-reach unnamed "jarvis is in ${reach% }"; fi
}
check_p6() {
local g m members bad=0
for g in sudo docker lxd; do
members="$(db group "$g" | cut -d: -f4)"
for m in ${members//,/ }; do
[ "$g" = sudo ] && [ "$m" = localadmin ] && continue
line P6 "group-$g" unnamed "$m is a member of $g"; bad=1
done
done
[ "$bad" = 1 ] || line P6 group-members closed "sudo holds only localadmin, and docker and lxd have no members"
}
check_p7() {
local found
found="$(grep -rlw -- "$S_USER" "$ROOT/etc/sudoers.d" 2>/dev/null | sed "s#^$ROOT##" | tr '\n' ' ')"
if [ -z "$found" ]; then line P7 sudoers-d closed "no file under /etc/sudoers.d names $S_USER"
else line P7 sudoers-d unnamed "files naming $S_USER: ${found% }"; fi
}
check_p8() {
local other
other="$(listeners 7331)$(listeners 7333)"
if loopback_by "$PORT" "$CON_PID" && [ -z "$other" ]; then
line P8 lan-listeners closed "$PORT is on 127.0.0.1 only by MainPID $CON_PID; no TCP listener on 7331 or 7333; tester uses $TESTER_PATH"
else
line P8 lan-listeners unnamed "$PORT: $(listener_text "$PORT"); 7333: $(listener_text 7333); 7331: $(listener_text 7331); Console MainPID ${CON_PID:-none}"
fi
}
check_p9() {
local out n missing=() rc=0 err mg others
out="$("$ROOT$HOSTCHECK" --check 2>/dev/null)"
for n in 1 2 3 4 5 6; do grep -q "^ok $n " <<< "$out" || missing+=("hostcheck item $n has no ok line")
done
err="$(sudo -u jarvis /usr/bin/bwrap --version 2>&1 >/dev/null)" || rc=$?
[ "$rc" != 0 ] && grep -q "Permission denied" <<< "$err" || missing+=("sudo -u jarvis /usr/bin/bwrap --version exited $rc without Permission denied")
mg="$(gid_of "$S_USER")"
[ -n "$mg" ] && [ -z "$(db group "$S_USER" | cut -d: -f4)" ] || missing+=("group $S_USER lists members, or $S_USER has no group")
others="$(db passwd | awk -F: -v g="$mg" -v u="$S_USER" '$4 == g && $1 != u { print $1 }' | tr '\n' ' ')"
[ -z "$others" ] || missing+=("other accounts have $S_USER's gid $mg: ${others% }")
if [ ${#missing[@]} = 0 ]; then
line P9 bwrap-userns "accepted mos-1096" "hostcheck items 1-6 ok, jarvis is denied bwrap, and group $S_USER has no other member"
else
for n in "${missing[@]}"; do line P9 bwrap-userns unnamed "$n"; done
fi
}
# P10 is an observation of mutable same-uid state, never a closure ruling.
TESTER_PEER='import socket,struct,sys
s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
try:
s.settimeout(1)
s.connect(sys.argv[1])
p=struct.unpack("3i",s.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
print(*p)
finally:
s.close()'
tester_snapshot() {
local parent="${TESTER_PATH%/*}" p s pid
[ ! -L "$ROOT$parent" ] && [ ! -L "$ROOT$TESTER_PATH" ] || return 1
p="$(stat -c '%u %a %F %d %i' -- "$ROOT$parent" 2>/dev/null)" || return 1
s="$(stat -c '%u %a %F %d %i' -- "$ROOT$TESTER_PATH" 2>/dev/null)" || return 1
[[ "$p" =~ ^$MOSAIC_UID\ 700\ directory\ [0-9]+\ [0-9]+$ ]] || return 1
[[ "$s" =~ ^$MOSAIC_UID\ 600\ socket\ [0-9]+\ [0-9]+$ ]] || return 1
pid="$(prop "$(unit_show "$CON_UNIT" MainPID)" MainPID)"
[[ "$pid" =~ ^[1-9][0-9]*$ ]] && [ "$pid" = "$CON_PID" ] || return 1
[ "$(proc_uids "$pid")" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] || return 1
printf '%s|%s|%s' "$pid" "$p" "$s"
}
tester_python() {
local real bad magic
# Resolve the system entry, then validate the target and every ancestor.
[ -f "$ROOT/usr/bin/python3" ] && [ -x "$ROOT/usr/bin/python3" ] || return 1
real="$(realpath -e -- "$ROOT/usr/bin/python3" 2>/dev/null)" || return 1
[ -z "$ROOT" ] || real="${real#"$ROOT"}"
case "$real" in /home/*|/root/*|/run/user/*|"$S_HOME"/*) return 1 ;; esac
bad="$(not_root_only "$real")"; [ -z "$bad" ] || return 1
bad="$(not_root_only /usr/bin)"; [ -z "$bad" ] || return 1
magic="$(od -An -tx1 -N4 -- "$ROOT$real" 2>/dev/null | tr -d ' \n')"
[ "$magic" = 7f454c46 ] || return 1
printf '%s' "$real"
}
check_p10() {
local floor before after python peer pid uid gid why="" pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})$'
floor="$(cat "$ROOT/proc/sys/net/ipv4/ip_unprivileged_port_start" 2>/dev/null)"
[[ "$floor" =~ ^[0-9]{1,5}$ ]] && [ "$floor" -gt "$PORT" ] || why=floor
before="$(tester_snapshot)" || why=metadata
python="$(tester_python)" || why=interpreter
if [ -z "$why" ]; then
peer="$(tester_capture 2 4 128 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why="probe-$(tester_capture_reason "$?")"
if [[ "$peer" =~ $pattern ]]; then
pid="${BASH_REMATCH[1]}" uid="${BASH_REMATCH[2]}" gid="${BASH_REMATCH[3]}"
[ "$pid" = "$CON_PID" ] && [ "$uid" = "$MOSAIC_UID" ] && [ "$gid" -le 2147483647 ] || why=peer
else [ -n "$why" ] || why=probe; fi
after="$(tester_snapshot)" || why=metadata
[ "$before" = "$after" ] || why=changed
fi
if [ -z "$why" ]; then
line P10 tester-socket-squat unnamed "boundary=observed path=$TESTER_PATH parent=0700 owner=$MOSAIC_UID socket=0600 peer_pid=$pid peer_uid=$uid floor=$floor port=$PORT; hostile same-uid substitution remains"
else
line P10 tester-socket-squat unnamed "boundary=failed reason=$why path=$TESTER_PATH MainPID=${CON_PID:-none}; hostile same-uid substitution remains"
fi
}
check_p11() {
local en before
en="$(systemctl is-enabled mosaic-hostcheck.service 2>/dev/null)"
before="$(prop "$(unit_show mosaic-hostcheck.service Before)" Before)"
if [ "$en" = enabled ] && grep -qxF -- "$BUS_UNIT" <<< "${before// /$'\n'}"; then
line P11 hostcheck-before-bus closed "mosaic-hostcheck.service is enabled, and its Before= names $BUS_UNIT"
else
line P11 hostcheck-before-bus unnamed "mosaic-hostcheck.service is '${en:-unknown}', Before='$before'; it needs enabled and $BUS_UNIT"
fi
}
check_f1
check_f2
F5_LINE=()
check_f3_f5
check_f4
"${F5_LINE[0]}" F5 bwrap-label "${F5_LINE[@]:1}"
check_f6
check_f7
check_f8
check_f9
check_f10
check_f11
check_p1; check_p2; check_p3; check_p4; check_p5; check_p6
check_p7; check_p8; check_p9; check_p10; check_p11
for m in "${MARKS[@]}"; do echo "count $m ${COUNT[$m]:-0}"; done
if [ "$F_FAILED" = 0 ]; then echo "case1-accept: every F check passes; send the whole output to Duckworth, and each unnamed line to Mos"; exit 0; fi
echo "case1-accept: an F check failed, so the row fails"
exit 1
Loaded 100 of 1674 files, more files were not shown because too many files have changed in this diff. Show more