Compare commits
103
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b6367503 | ||
|
|
b8461266cd | ||
|
|
8ae9d8acd9 | ||
|
|
9aa6d52d94 | ||
|
|
e7078e6a85 | ||
|
|
d49b1f2bd3 | ||
|
|
86249ac552 | ||
|
|
59f817c321 | ||
|
|
c515273db7 | ||
|
|
da3051ccc6 | ||
|
|
060a17b549 | ||
|
|
ffd90e7fc4 | ||
|
|
02709125a6 | ||
|
|
c771a36f4b | ||
|
|
caa4b59440 | ||
|
|
57f59e0c39 | ||
|
|
5465fd52d5 | ||
|
|
b00d7d94d0 | ||
|
|
642a9b87de | ||
|
|
16019b80e6 | ||
|
|
9b53620185 | ||
|
|
7662b04902 | ||
|
|
04ce8dfd56 | ||
|
|
fc1cb856b0 | ||
|
|
c0c4907af9 | ||
|
|
5fb736b92d | ||
|
|
96bac588fb | ||
|
|
5d4a617762 | ||
|
|
e65a3ab4d2 | ||
|
|
0444acd084 | ||
|
|
052ba95c7c | ||
|
|
a52d64ed41 | ||
|
|
8fede5fc6a | ||
|
|
c084d8d423 | ||
|
|
f5753c0e8d | ||
|
|
9d165be8c7 | ||
|
|
c25785d3d5 | ||
|
|
ad95c888c3 | ||
|
|
584e2bd827 | ||
|
|
0adf8ddfe2 | ||
|
|
d7d9431990 | ||
|
|
a0e3d7cfdd | ||
|
|
9d24ff9b28 | ||
|
|
6c04b89dae | ||
|
|
6f7dacb417 | ||
|
|
d21b2615c1 | ||
|
|
816d48b03e | ||
|
|
011d41ba6a | ||
|
|
c8bc5cf4cb | ||
|
|
390ea5261c | ||
|
|
cb5fdb064f | ||
|
|
76134dfde1 | ||
|
|
9e10858160 | ||
|
|
7221367728 | ||
|
|
a304dad947 | ||
|
|
919a84738b | ||
|
|
2d8687bcfd | ||
|
|
1c46d0abf6 | ||
|
|
f519fb1d26 | ||
|
|
b8236a7e13 | ||
|
|
c7675476b1 | ||
|
|
9de7ad8292 | ||
|
|
c40d846cb8 | ||
|
|
01b40d3576 | ||
|
|
ad6f0be7aa | ||
|
|
ffc124bf68 | ||
|
|
8a14931fa4 | ||
|
|
0272383f58 | ||
|
|
00d4cc1f69 | ||
|
|
99a406ed3c | ||
|
|
d0bbc5c1cb | ||
|
|
c9f374284b | ||
|
|
4385b9ec18 | ||
|
|
afad4993f6 | ||
|
|
f41729178e | ||
|
|
305857c8f9 | ||
|
|
176a6bf4b9 | ||
|
|
5b8def2f14 | ||
|
|
4429a104f6 | ||
|
|
32b54ac3b0 | ||
|
|
6025ddd9cf | ||
|
|
ecbfd6deb6 | ||
|
|
0498c0613f | ||
|
|
49d1f3a231 | ||
|
|
628e2ca5cd | ||
|
|
088087ac96 | ||
|
|
a0fbefd48d | ||
|
|
f24f2cd40d | ||
|
|
9ba31b5b3f | ||
|
|
9db578ff54 | ||
|
|
8eb9ddd12c | ||
|
|
ae3c36f248 | ||
|
|
a2af7ab495 | ||
|
|
aa1bd209e3 | ||
|
|
ace27dc5eb | ||
|
|
a4a42dbac2 | ||
|
|
a31f1a0f8b | ||
|
|
992650232f | ||
|
|
a7661ada06 | ||
|
|
f871cb3096 | ||
|
|
0bbd23c619 | ||
|
|
0217b71c8f | ||
|
|
c6f528a32f |
@@ -25,10 +25,15 @@ green at every step. Not production software — a proven foundation.
|
||||
1. **Root is bootstrap-only.** First-class system configuration lives at the
|
||||
repository root; everything else gets a dedicated directory (`roles/`,
|
||||
`contracts/`, `missions/`, `tasks/`, `docs/`). Do not add new files to root.
|
||||
2. **Configuration**: `~/.config/mosaic-dev/config.json` is the sole system
|
||||
config — created only by `scripts/bootstrap.sh`, never overwritten,
|
||||
fail-closed on any problem. Repo-scoped role authority lives in
|
||||
`roles/*.json` (versioned, reviewed commits only).
|
||||
2. **Configuration**: each stack instance has exactly one system config. The
|
||||
default instance's is `~/.config/mosaic-dev/config.json`. Instance NAME's
|
||||
is `~/.config/mosaic-NAME/config.json`, and its data root carries a marker
|
||||
that names it. `~` is the home of the OS user that runs the instance. A
|
||||
process reads only the config `MOSAIC_CONFIG` names, or the default's when
|
||||
that is unset. Configs are created only by
|
||||
`scripts/bootstrap.sh [--instance NAME]`, never overwritten, fail-closed on
|
||||
any problem. Repo-scoped role authority lives in `roles/*.json`
|
||||
(versioned, reviewed commits only).
|
||||
3. **Secrets** never enter the repository or container images; auth is
|
||||
runtime-only (read-only mount or environment variable).
|
||||
4. **Contracts** (`contracts/`) are immutable and image-baked. Missions and
|
||||
@@ -177,8 +182,8 @@ Full reference — usage, fields, exit codes, safety notes:
|
||||
|
||||
## Data map (canon)
|
||||
|
||||
- `~/.config/mosaic-dev/config.json` — system config (user-authored; never
|
||||
auto-written).
|
||||
- `~/.config/mosaic-dev/config.json`, or `~/.config/mosaic-NAME/config.json`
|
||||
per named instance: system config (user-authored; never auto-written).
|
||||
- `<dataRoot>` (from config; default `~/.mosaic-dev`):
|
||||
- `runs/` — write-once run evidence (`result.json`, snapshots, `stderr.txt`)
|
||||
- `sessions/` — pi JSONL session trees, one directory per named session
|
||||
|
||||
+3
-10
@@ -5174,13 +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-20 19:09:44 UTC | Gander row 196 #1694 design phase | Before: tester TCP reaches every local uid and human proofs do not bind the socket peer. After: design draft at agents/gander/work/queue-196/DESIGN.md inventories listeners, proposes private tester HTTP and SO_PEERCRED binding, aligns the per-request principal contract with Morgana and Gizmoduck, and lists migration, tests, mutants and same-uid limits. Python versus other credential options and Console provenance require Duckworth ruling. Documentation only. No product code, suite, live probe, secret access or deployment.
|
||||
|
||||
2026-10-20 19:16:20 UTC | Gander row 196 #1694 design r2 | Before: Duckworth r1 requested concurrent helper-fd evidence (B1) and selected O1 with interpreter, proof-only and image conditions. After: design adds eight concurrent genuine proof processes plus a racing copier, M20 swapped socket and post-helper socket survival, root-owned system ELF pinning and activation evidence, safe gid validation, cap traffic without helpers, and unchanged P19 R2 text. Mos accepted socket-only tester access (mos-1015 via sage-1363); any authenticated browser relay is a separate row. Documentation coverage checks passed. No product code, suites or live action.
|
||||
|
||||
2026-10-20 19:21:19 UTC | Gander row 196 #1694 design approval and N5 | Before: Duckworth approved r2 (duckworth-0009, cb6d840df) with a nonblocking interpreter-upgrade note. After: r3 chooses startup safety revalidation, appends actual interpreter identity to bus/peercred-starts.jsonl and retains old activation evidence. Operator re-pin uses the next reviewed copy, respecting already-active refusal. Added task-by-task PLAN.md for inline implementation and independent manifest review. Documentation checks passed. No product code, suites or live action.
|
||||
|
||||
2026-10-20 19:34:28 UTC | Gander row 196 build paused at design mismatch | Before: approved design promised Unix tester cleanup only for the owned inode. After: scratch test shows Node/libuv close unlinks a replacement inode before JS close handlers. Evidence agents/gander/work/queue-196/evidence/tester-cleanup-blocker.txt. Sent Sage/Duckworth gander-0016/0017, recommending explicit same-uid residual with private 0700 parent retained. Interpreter, concurrent peer, principal, human, peer-transport, transport, tester and process single files passed. Partial code uncommitted, tester transport not wired. No suites, mutant batches or live actions.
|
||||
|
||||
2026-10-21 18:45 UTC | Gander row 196 preservation before rebase | Sage-2051, Mos order mos-1193, explicitly authorizes commit and fast-forward push of the current build before deferred full verification. Product and evidence preserved together; prior focused checks and stopped gates are recorded in the row196 packet, with no claim of full acceptance. Next: rebase onto a36523713 on new gander/row196-r2, report range-diff/patch-id/conflicts, then await Sage's clear for M1-M29 and all requested suites. No live action, force push, branch deletion or tests during hold.
|
||||
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.
|
||||
|
||||
@@ -20,6 +20,11 @@ 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 |
|
||||
| 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` |
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,287 @@
|
||||
# Row 232 (I1): stack-release external units and bundle copies
|
||||
|
||||
Brief: `docs/plans/2026-10-20_installer-rows.md` section I1, blob `f35e7e57daee` (commit ad6f0be7a). Gitea
|
||||
#1734, Vikunja P19 #432. Owner Darkwing, reviewer Filbert, gate owner Sage. Design input: row 195 DESIGN r6,
|
||||
`agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66 (approved in duckworth-0055). Where the brief and
|
||||
r6 differ, the brief wins. sage-1561 ruled where the stack user's `stack-release.sh` runs: from its own
|
||||
clone of the release bundle, so `check_target`, the plan's commit list and status drift are unchanged and
|
||||
nothing adds `safe.directory`.
|
||||
|
||||
## Files
|
||||
|
||||
Base: cb5fdb064 (row 199's landing, on top of row 200's 9e1085816). Branch `darkwing/row232-r1`. The first
|
||||
build, 0af0c73c6 on `darkwing/row232`, sat on 00d4cc1f6 (row 190); row 200 landed first, so this one is
|
||||
rebased onto it (see D9 and D10).
|
||||
|
||||
- `scripts/stack-release.sh`: `--units user|external`, the `sc` and `jctl` backend, `confirm`, `revert`,
|
||||
`gate --since`, `rollback --to`, `--plan --expect-from`, the external gate (start floor, PM check from
|
||||
`launches list`), `external-pm`, and the `awaiting units:` status line.
|
||||
- `scripts/stack-copy.sh`: `check --units external`, `copy --bundle FILE SHA`, `wrapper ID --create`.
|
||||
- `packages/cli/tests/stack-release-external.test.mjs` (new, 25 tests).
|
||||
- `packages/cli/tests/stack-release-fixture.mjs` (new): the helpers `stack-release.test.mjs` defined, moved
|
||||
so both files share them. See deviation D1.
|
||||
- `packages/cli/tests/stack-release.test.mjs`: the helpers leave and one import line comes in. The 74 tests
|
||||
have the same names and the same bodies as on the base.
|
||||
- `packages/cli/tests/stack-copy.test.mjs`: three new `row 232:` tests and a wider `node:fs` import.
|
||||
- `docs/TOOLS.md`: the stack-copy usage block and three paragraphs, the stack-release usage block, and a new
|
||||
subsection "Root-owned units (`--units external`, row 232)".
|
||||
- `packages/cli/README.md`: one paragraph.
|
||||
- This file.
|
||||
|
||||
The worktree's `node_modules` symlink is not part of the candidate.
|
||||
|
||||
## User mode is unchanged
|
||||
|
||||
Every `systemctl` call goes through `sc` and every `journalctl` call through `jctl`. Under `user` they
|
||||
are `systemctl --user "$@"` and `journalctl --user "$@"`. Under `external`, `sc` allows only `show`,
|
||||
`is-active` and `list-units`, and `jctl` returns 1, so an external run can't start, stop, install or read
|
||||
the journal even through a path the tests miss.
|
||||
|
||||
Two tests hold the brief's "every command line and every byte of output":
|
||||
|
||||
- `user mode: every systemctl call carries --user, and activate, rollback and check make the base's
|
||||
calls`. A fixture repo gets the base's two scripts from `git show cb5fdb064:`. The same activate,
|
||||
rollback and `stack-copy.sh check` run from both trees, and the recorded argv (with the fixture root and
|
||||
port replaced by placeholders) must match line for line. Every call carries `--user`.
|
||||
- `user mode: a default activate --plan prints the base's bytes`, for four plans.
|
||||
|
||||
The base output is produced live from the base blobs, not stored as a fixture file. See deviation D3.
|
||||
|
||||
## r6 section 7, test by test
|
||||
|
||||
All in `stack-release-external.test.mjs` unless named.
|
||||
|
||||
| r6 :742-812 item | Test |
|
||||
|---|---|
|
||||
| `units-running` (N4), store, log and TAKEN left | `units-running (N4) refuses while a unit is active...`; also checks that `failed` counts as down and that the Console is checked first |
|
||||
| activate leaves the pointer, writes `AWAITING-UNITS` and a backup | `activate stages: ...`, which also checks the set's file list, `UNITS.sha256`, no `--user`, no journal call and `status` |
|
||||
| `open-activation` with zero, two, the wrong copy, one not open | `confirm and revert refuse open-activation ...` |
|
||||
| `confirm` polls: 3 s fails at `--timeout 1`, passes at `--timeout 10` | `confirm polls: bus status good after 3 s fails ...` and `confirm polls to a pass ...` |
|
||||
| `stale-start` floor with `started` at .900 | `stale-start: @1792540799 fails ...` and `stale-start is a gate code ...` |
|
||||
| `confirm` pass writes pointer, ok, removes marker; timeout writes `FAILED` and exits 1 | the two polling tests |
|
||||
| `activation-failed` when `FAILED` exists | `confirm polls: ... FAILED then refuses activation-failed` |
|
||||
| lines carry the started op, from, to, activation and backup | `sameAsStarted` in every confirm test, and sequence 7 |
|
||||
| `gate` (N2) with and without `--since` | `gate (N2, A2f): ...` and the usage test |
|
||||
| `--plan --expect-from` (N5), activate and `rollback --to` | sequence 7 |
|
||||
| A3 order: capture-failed, marker-failed, wrapper failure after migrate | `A3 order: ...` and sequence 2 |
|
||||
| refusals log no activation; `interrupted` unchanged | `open-activation` test and sequence 5 |
|
||||
| PM check: one, none, two, early, not live, other business, other instance; no journal call | `the external PM check reads launches list ...` (modes '', coder, none, twolive, early, dead, other) and `journalCalls` in every external test |
|
||||
| `external-pm` | `pm-on and activate --pm-after-harness refuse external-pm` |
|
||||
| `revert` restores TAKEN only after a migration | `revert puts the wrapper back and leaves the store ...` and sequences 2 and 8 |
|
||||
| `confirm --rollback` per cause, `unrecorded`, `rollback-failed`, mismatch | sequences 1, 2, 3, 5 and 8, and `confirm --rollback refuses rollback-mismatch, and logs rollback-failed ...` |
|
||||
| sequences 1-8 | `sequence 1:` to `sequence 8:`, each ending with the `closed` check (one started and one result line, empty `interrupted`, no marker or `FAILED`, a following `--plan` that doesn't refuse `interrupted-run`) |
|
||||
| `rollback --to` refuses not-previous; `rollback --restore` finds an external backup | sequence 7, and `confirm --rollback after a --restore swap ...` |
|
||||
| `gate` writes nothing | the `gate (N2, A2f)` test and sequence 4 compare the log, the pointer and the state tree |
|
||||
| `confirm`, `revert`, `gate`, `--to` refuse without `--units external` | `confirm, revert, gate, --to and --expect-from need --units external ...` |
|
||||
| default path: every call carries `--user`; default `--plan` bytes | the two user-mode tests above |
|
||||
|
||||
The brief's additions, in `stack-copy.test.mjs`:
|
||||
|
||||
- `row 232: copy --bundle copies a SHA the bundle's refactor reaches, byte-identical to copy SHA, and
|
||||
refuses the rest`: a short SHA, a missing file, a symlink, a bundle that fails `git bundle verify`, a
|
||||
bundle with no `refs/heads/refactor`, a SHA not in the bundle, and a SHA the bundle carries on another
|
||||
branch that refactor doesn't reach. The bundle copy's tree matches `copy SHA`'s byte for byte, and the
|
||||
`release:` and `bundle sha256:` lines are checked.
|
||||
- `row 232: wrapper --create writes the first wrapper only, the same bytes wrapper ID writes, and refuses
|
||||
over any file`, a symlink included. Without `--create` a missing wrapper still refuses.
|
||||
- `row 232: check --units external reads the system units, the default reads the user units`.
|
||||
|
||||
## Mutants
|
||||
|
||||
`mutants.sh` in my scratch (not committed, it edits the worktree), 17 mutants: r6's re-pin list (:817-838)
|
||||
minus the three `case1-repin.sh` ones, plus the brief's two. Each one applies exact replacements after a
|
||||
checked backup, runs `bash -n`, runs its named killers, restores the file and checks its sha256. Two
|
||||
controls run first with no change: the whole external file, and stack-copy's `row 232:` tests.
|
||||
|
||||
| Mutant | Killers |
|
||||
|---|---|
|
||||
| m01 `confirm` writes the pointer on a gate failure | `confirm polls: bus status` |
|
||||
| m02 `gate_once` in place of `gate` | `confirm polls to a pass`, `stale-start is a gate code` |
|
||||
| m03 PM check counts any instance | `external PM check` |
|
||||
| m04 PM check drops the `startedAt` bound | `external PM check` |
|
||||
| m05 `confirm` takes the newest `AWAITING-UNITS` | `open-activation` |
|
||||
| m06 `rollback --to` skips the comparison | `sequence 7:` |
|
||||
| m07 the default path drops `--user` from one call (`is-active`) | `user mode: every systemctl` |
|
||||
| m08 the timeout writes r4's interim `rollback pending` line | `confirm polls: bus status`, `sequence 1:`, `sequence 5:` |
|
||||
| m09 `AWAITING-UNITS` last, after the wrapper | `sequence 2:` |
|
||||
| m10 a refusal names the open activation | `sequence 5:`, `open-activation` |
|
||||
| m11 `stale-start` is a refusal | `stale-start is a gate code` |
|
||||
| m12 `revert` restores without `MIGRATING` | `revert puts the wrapper back` |
|
||||
| m13 `confirm` logs op `confirm` | `sequence 7:`, `confirm polls to a pass` |
|
||||
| m14 `MIGRATING` after the migration | `sequence 8:` |
|
||||
| m15 `gate` takes its floor from the log without `--since` | `gate (N2` |
|
||||
| m16 `copy --bundle` skips the reachability check | stack-copy `row 232:` |
|
||||
| m17 `wrapper --create` overwrites | stack-copy `row 232:` |
|
||||
|
||||
## Deviations and choices
|
||||
|
||||
D1. The shared helpers moved to a new file, `packages/cli/tests/stack-release-fixture.mjs`. The brief's
|
||||
files list names `stack-release.test.mjs` "where a shared helper moves" but not a destination. Importing
|
||||
from the other test file would run its 74 tests twice, so the helpers need a module of their own.
|
||||
|
||||
D2. The fixture's T17 check (what a run may never remove) now exempts a rollback set's `AWAITING-UNITS` and
|
||||
`FAILED`. `confirm` removes both when it closes the activation (r6 A5 and R4). User mode writes neither, so
|
||||
user-mode runs check exactly what they did.
|
||||
|
||||
D3. The user-mode argv and `--plan` tests read the base's scripts with `git show cb5fdb064:` rather than
|
||||
storing their output as a fixture file. A later row that changes user-mode output on purpose moves the
|
||||
`BASE` constant in the test to its own base, or the test fails. I chose this over a stored fixture because
|
||||
a stored copy can drift from the base without anyone noticing.
|
||||
|
||||
D4. `marker-failed` is reached by making `state/stack-rollback` unwritable. r6 says "an existing `$RS`", but
|
||||
`next_activation` never picks a taken name, so no test can make the run meet an existing `$RS`. The
|
||||
exclusive `mkdir` is still what refuses.
|
||||
|
||||
D5. The stale-start polling case turns the enter time to `@1792540800`, not r6's `@1792540805`, with the
|
||||
started line at 1792540800.900. That also checks that the floor is the whole second, so .900 doesn't push it
|
||||
to 1792540801.
|
||||
|
||||
D6. No fake `sudo` or `case2-unit.sh`. r6's sequences ran under them because r6 had `case1-repin.sh` drive
|
||||
the steps, and that script is I3's. Here the test plays root: it stops and starts the fake system units and
|
||||
installs with the existing `bus-service.sh` and `webui-service.sh` stand-ins, between the real
|
||||
`stack-release.sh` calls, on one log.
|
||||
|
||||
D7. The fake marker in the `open-activation` test is named `20000101T000000Z-<C1 prefix>` so it sorts
|
||||
before the real one. A marker that sorted after it would leave m05 (newest marker) alive.
|
||||
|
||||
D8. m10 sets `ACT` inside `open_activation`, where r6's wording ("ACT set before the checks") lands in this
|
||||
code. `ACT` is set only after every check in `confirm`.
|
||||
|
||||
D9. Row 200's `--sso` is a usage error under `--units external`, with the proof flags, `--no-proof` and
|
||||
`--no-send`. It is a Console flag, and root's Console unit carries the Console flags; the script under
|
||||
external renders no Console line and never calls `console_for_target`, so `sso_checks` doesn't run there
|
||||
either. The usage text, the TOOLS.md bullet and the external usage test name it.
|
||||
|
||||
D10. Row 200's `config_checks` runs under `--units external` too, after `plugin_checks`, in both the
|
||||
`--plan` and run paths of `activate` and `rollback --to`. It checks the target's loader against the stack
|
||||
user's `config.json`, which the bus on a VM still reads, so a `console` key the target refuses would break
|
||||
root's bus start the same way. The cost is one more refusal (`config-console-key`) an external run can
|
||||
print; I took it over a bus that fails after root's start. The rebase changed no user-mode line: the
|
||||
conflicts were the `SSO=0` globals line, the `plan` body (row 200's `SSO_PLAN` line sits in the user
|
||||
branch) and the two `--plan` lines that gained `config_checks`. In the tests, row 200's fixture lines
|
||||
(the loaders, `sso-config.mjs` and the `--sso` cli.mjs line for C1 and C) moved into
|
||||
`stack-release-fixture.mjs` with the rest of the copy setup, and the `ALGS` import stays in
|
||||
`stack-release.test.mjs`, which T57 uses.
|
||||
|
||||
## duckworth-0055's notes on r6 (brief line 169)
|
||||
|
||||
Duckworth's r6 approval (REVIEW-r5.md at 23f5520b5) carried two notes.
|
||||
|
||||
- N1, A2's stop timeout under `gate --since`. A unit that never stopped keeps its old `ActiveEnterTimestamp`,
|
||||
so `gate --since` fails it with `stale-start`, and the run ends at exit 2 with root stopping the units.
|
||||
That is intended: a unit that won't stop is a fair reason to stop. In I1 this is the gate code and its
|
||||
polling (`stale-start: @1792540799 fails ...`, `gate (N2, A2f)`, m11). The run sheet's A2f line and the
|
||||
re-pin test that points "exits 1 with OLD's gate passing" at an A3 exit 3 belong to I3, which owns
|
||||
`scripts/stack-upgrade.sh` and `docs/guides/host-run-sheet.md`. I'll name both in the I3 build.
|
||||
- N2, the claude path under `/opt/mosaic-stack` (gosalyn-0058, sage-1522). I1 doesn't touch it. Under
|
||||
`--units external` the script checks no claude pin of its own (`no-claude-pin` applies only to user
|
||||
mode), because root's unit carries it. The `--claude` value, the managed-settings reasoning, F6 and the
|
||||
run sheet's claude step go to I2 and I3.
|
||||
|
||||
## Run environment
|
||||
|
||||
Worktree `/mnt/storage/scratch/darkwing-r232`, `TMPDIR=/mnt/storage/scratch/tmp/r232`, node from PATH.
|
||||
One external file run takes about 94 s.
|
||||
|
||||
## Results
|
||||
|
||||
Singles on the rebased candidate, before the gate:
|
||||
|
||||
| File | Tests | Pass | Fail |
|
||||
|---|---|---|---|
|
||||
| `packages/cli/tests/stack-release-external.test.mjs` | 25 | 25 | 0 |
|
||||
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 16 | 0 |
|
||||
| `packages/cli/tests/stack-release.test.mjs` (row 200's T57 included) | 192 | 192 | 0 |
|
||||
| `packages/cli/tests/stack-instances.test.mjs` | 23 | 23 | 0 |
|
||||
| `packages/webui/tests/pinned-copy.test.mjs` | 10 | 10 | 0 |
|
||||
| `packages/cli/tests/channel-recovery.test.mjs` (imports `channel-fixture.mjs`, which names the scripts) | 1 | 1 | 0 |
|
||||
| `packages/cli/tests/host.test.mjs` (reads `packages/cli/README.md`) | 42 | 42 | 0 |
|
||||
|
||||
These are every test file that reads `stack-release.sh` or `stack-copy.sh` (a grep of `packages/*/tests` and
|
||||
`scripts/`). None of them pins a sha256 of a changed file. `packages/cli/tests/host.test.mjs` reads
|
||||
`packages/cli/README.md` for a unit file name; this row only adds a paragraph there. The grep and the runs
|
||||
were repeated on the rebased tree on 2026-10-21; the reader set is the same. A dry run of `mutants.sh`
|
||||
matches each of the 17 replacements once on the rebased scripts.
|
||||
|
||||
### Gate, 2026-10-21, Sage's slot (sage-1618)
|
||||
|
||||
Candidate a0ffabc3f on `darkwing/row232-r1`, run by `~/darkwing-scratch/r232/gate.sh`: the cli suite, then
|
||||
every `scripts/test-*.sh`, one after another, stopping at the first failure. busy.sh passed at 01:07:49Z
|
||||
after Launchpad's single `stack-release.test.mjs` run ended. The gate stopped at the first one, so the
|
||||
chain waited for that PID to exit and checked again. The EXIT trap removed the compose network; no
|
||||
container was left.
|
||||
|
||||
| Step | Time (UTC) | Result |
|
||||
|---|---|---|
|
||||
| `packages/cli`, `node --test tests/*.test.mjs` | 01:07:50 to 01:17:53 | 561 pass, 0 fail, 0 skipped |
|
||||
| `scripts/test-auth.sh` | 01:17:53 to 01:17:54 | 15 passed, 0 failed |
|
||||
| `scripts/test-conductor.sh` | 01:17:54 to 01:18:00 | 17 passed, 0 failed |
|
||||
| `scripts/test-config.sh` | 01:18:00 to 01:18:02 | 31 passed, 0 failed |
|
||||
| `scripts/test-discord.sh` | 01:18:02 to 01:18:12 | 78 passed, 0 failed |
|
||||
| `scripts/test-extension-package.sh` | 01:18:12 to 01:18:13 | 18 passed, 0 failed |
|
||||
| `scripts/test-foundation.sh` | 01:18:13 to 01:19:01 | 44 passed, 0 failed |
|
||||
| `scripts/test-land-work.sh` | 01:19:01 to 01:19:14 | 86 passed, 0 failed |
|
||||
| `scripts/test-queue.sh` | 01:19:14 to 01:19:37 | 156 node tests pass, queue suite 28 passed, 0 failed |
|
||||
| `scripts/test-release.sh` | 01:19:37 to 01:19:51 | 14 passed, 0 failed |
|
||||
| `scripts/test-task.sh` | 01:19:51 to 01:20:44 | 98 passed, 0 failed |
|
||||
|
||||
### Mutants, 2026-10-21, same slot
|
||||
|
||||
`~/darkwing-scratch/r232/mutants.sh`, right after the gate. Both controls (the external file whole, and
|
||||
stack-copy's `^row 232: ` tests) exited 0 on the unmutated tree. Each mutant was applied after a checked
|
||||
backup and a syntax check, ran its killers from the table above, and was put back with its sha256
|
||||
checked. The script stops on any restore mismatch, and it ran to its end.
|
||||
|
||||
| Mutant | Killer exit | Result |
|
||||
|---|---|---|
|
||||
| m01 pointer on gate failure | 1 | killed |
|
||||
| m02 gate once | 1 | killed |
|
||||
| m03 PM from any instance | 1 | killed |
|
||||
| m04 no startedAt check | 1 | killed |
|
||||
| m05 newest marker | 1 | killed |
|
||||
| m06 --to not compared | 1 | killed |
|
||||
| m07 one call without --user | 1 | killed |
|
||||
| m08 interim pending line | 1 | killed |
|
||||
| m09 marker written last | 1 | killed |
|
||||
| m10 refusal names the open activation | 1 | killed |
|
||||
| m11 stale-start refuses | 1 | killed |
|
||||
| m12 revert always restores | 1 | killed |
|
||||
| m13 confirm op | 1 | killed |
|
||||
| m14 MIGRATING after migrate | 1 | killed |
|
||||
| m15 gate floor from the log | 1 | killed |
|
||||
| m16 bundle reachability unchecked | 1 | killed |
|
||||
| m17 --create overwrites | 1 | killed |
|
||||
|
||||
17 of 17 killed, 01:22:16 to 01:24:32Z. I read the logs for m01 and m09, two of the quick kills: each failed on
|
||||
its named test's assertion (m09 on `AWAITING-UNITS` in sequence 2), not on a load error.
|
||||
|
||||
## Round 2: rebase onto 8fede5fc6 (sage-1673)
|
||||
|
||||
Rebase only. Before the landing gate, a check of BASE..HEAD on the manifest paths found that row 230
|
||||
(c8bc5cf4c) had changed one of them after the round 1 base, cb5fdb064. Sage ruled a re-round with a
|
||||
scoped review, the same shape as row 210 round 3.
|
||||
|
||||
Branch `darkwing/row232-r2`: the two round 1 commits, a0ffabc3f and 84f1a91e6, cherry-picked onto
|
||||
8fede5fc6 with no conflict, then this section. No code or test changed.
|
||||
|
||||
- `git merge-tree --write-tree origin/refactor darkwing/row232-r1` (refactor at 8fede5fc6) gives tree
|
||||
837bde79236c with no conflict. The cherry-picked tip before this section has the same tree.
|
||||
- `packages/cli/README.md` is a clean 3-way merge. Row 230's hunk is at lines 710-717 (the session credential
|
||||
paragraph); this row's is a new paragraph at 773. With `git patch-id --stable`, the README diff from r1 to
|
||||
r2 is 02b679e2, the same as row 230's README diff `cb5fdb064..8fede5fc6`. This row's README diff is
|
||||
c395ecd2 on both bases (`cb5fdb064..r1` and `8fede5fc6..r2`).
|
||||
- The seven code, test and doc paths other than the README hash the same as in manifest 7a3b4ea1, which
|
||||
Filbert approved in round 1: `docs/TOOLS.md`, `packages/cli/tests/stack-copy.test.mjs`,
|
||||
`packages/cli/tests/stack-release-external.test.mjs`, `packages/cli/tests/stack-release-fixture.mjs`,
|
||||
`packages/cli/tests/stack-release.test.mjs`, `scripts/stack-copy.sh`, `scripts/stack-release.sh`.
|
||||
This file also matched until this section was appended. Its other lines are unchanged.
|
||||
- The one test that reads the README, `host.test.mjs:855`, looks for the unit's `EnvironmentFile=` line.
|
||||
That line is unchanged context in row 230's hunk.
|
||||
- No row 230 test reads `stack-release.sh` or `stack-copy.sh`, and no test of this row reads a row 230 file.
|
||||
|
||||
Nothing was run for this round. The landing gate runs on the r2 tip on Sage's slot.
|
||||
|
||||
Follow-up, doc-only (sage-1673): `scripts/case2-unit.sh:108` and `scripts/case2-precheck.sh:157` cite
|
||||
`scripts/stack-copy.sh:8` and `:22` for `COPIES` and `copy_root`. With this row those lines are `:10` and
|
||||
`:26`. Only comments are affected and no test reads them. Row 233 doesn't touch those scripts, so the fix
|
||||
goes on the #1732 carry list.
|
||||
@@ -0,0 +1,249 @@
|
||||
# Row 233 (I3): root's upgrade driver and the host run sheet
|
||||
|
||||
Brief: `docs/plans/2026-10-20_installer-rows.md` section I3, blob `e61291bcf1c6` (commit a0e3d7cfd), re-pinned
|
||||
by Sage (sage-1685, rev 2631) from `f35e7e57daee` (ad6f0be7a). The later blob only appends I4; the I3 text
|
||||
is the same. Gitea #1735, Vikunja P19
|
||||
#432. Owner Darkwing, reviewer Filbert, gate owner Sage. Design input: row 195 DESIGN r6,
|
||||
`agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66, sections 2, 4 and 7. Where the brief and r6
|
||||
differ, the brief wins. Sage's rulings during the build: sage-1650 and sage-1652 (`--backup last`, the
|
||||
`--no-rehearsal` mapping, `--rehearse-port`, hostcheck), and sage-1653 (`from_log newest`, the two external
|
||||
plan refusals, the `BACKUP_STORE` line).
|
||||
|
||||
## Files
|
||||
|
||||
Base: e65a3ab4d, row 232's landing, on `darkwing/row233-r1`. The build started on ea1e85c7b
|
||||
(`darkwing/row233`, row 232's candidate e6ca230d2 and its BUILD.md) before row 232 landed. The three row 232
|
||||
files this row changes are identical on the two bases, so the uncommitted diff moved across unchanged.
|
||||
|
||||
- `scripts/stack-upgrade.sh` (new): `upgrade` and `rollback`, root's side of r6 A1-A5, A2f and R1-R4.
|
||||
- `packages/cli/tests/stack-upgrade.test.mjs` (new, 20 tests).
|
||||
- `docs/guides/host-run-sheet.md` (new).
|
||||
- `docs/TOOLS.md`: a new subsection "Root's upgrade driver (`scripts/stack-upgrade.sh`, row 233)", plus the
|
||||
row 232 lines below.
|
||||
- Row 232 files, changed on Sage's rulings (the brief's files list doesn't name them):
|
||||
- `scripts/stack-release.sh`:
|
||||
- `rehearse --backup last` takes the newest backup the log records (`from_log newest`) and prints
|
||||
`PASS backup-last`. With none it fails `no-backup`, exits 3 and makes no rehearsal directory.
|
||||
A newest backup that isn't an activation id aborts `backup-unreadable`, exit 1.
|
||||
- The rehearsal prints `BACKUP_STORE <activation id> schema <version>` before its migration step.
|
||||
- `--plan` under `--units external` refuses `open-activation` when any rollback set holds
|
||||
`AWAITING-UNITS`, and `migrate-required` for a migrating target without `--migrate`. Root reads
|
||||
only the plan's exit, so these two have to be refusals there. User mode prints the same bytes.
|
||||
- `packages/cli/tests/stack-release.test.mjs`: three new `row 233:` tests for `--backup last`.
|
||||
- `docs/TOOLS.md`: `--backup ACTIVATION|PATH|last` in the rehearse usage, a paragraph on `last`, and a
|
||||
paragraph on the two external plan refusals.
|
||||
- This file.
|
||||
|
||||
## The brief, item by item
|
||||
|
||||
| Brief item | Where |
|
||||
|---|---|
|
||||
| Root runs from a root-owned checkout, never a copy | `head-mismatch` (A1); TOOLS.md; run sheet section 6 |
|
||||
| I2's render with `/etc/mosaic-stack/<instance>.args`, `--copy` the one changed value | `read_args`, `args_with`; the args test |
|
||||
| The args file changes only after a confirmed run | `prepare_args` before A2, `mv` after A5; sequences 1 and 7, m6 |
|
||||
| `copy --bundle` as the stack's user before A1 | the `fetch`, `ff`, `copy` steps; sequence 1's sudo keys |
|
||||
| A rehearsal before A2, a failed one refuses with nothing stopped | `rehearse`; three rehearsal tests, m4 |
|
||||
| The root lock, held to the end, rollback included | `flock -n 9`; the lock test and sequence 3's probe, m1, m2 |
|
||||
| A1 refuses `open-activation` | stack-release.sh's external plan; the AWAITING-UNITS test, m3 |
|
||||
| `migrate-required` without `--migrate` | stack-release.sh's external plan; its test, m5 |
|
||||
| The step log, 0640, one line per step | `logl`, `step`; every test checks lines against `STEP` |
|
||||
| Exit codes 0-3 | `finish`, `refuse`; exit 4 is usage |
|
||||
| duckworth-0055's notes | the next section |
|
||||
| Root parses no stack user output beyond exits | the step log test; the one exception is sage-1653's `BACKUP_STORE` |
|
||||
| The host run sheet | `docs/guides/host-run-sheet.md` |
|
||||
|
||||
## duckworth-0055's notes on r6 (brief line 169)
|
||||
|
||||
- N1, A2's stop timeout under `gate --since`. Row 232's BUILD.md left two pieces to I3. The run sheet's
|
||||
A2f line is in section 6 ("When a run doesn't exit 0", exit 2). The test is `N1: a bus that outlives
|
||||
A2's timeout keeps its old start`: a bus that ignores `stop` keeps its old `ActiveEnterTimestamp`,
|
||||
`wait-down` logs `rc=1 ... state: active`, A2f restarts and gates, the gate fails `stale-start`, and the
|
||||
driver stops both units and exits 2. Sequence 4 is the other half: an A3 exit 3 goes to A2f and exits
|
||||
1 with the old copy's gate passing.
|
||||
- N2, the claude path under `/opt/mosaic-stack`. Run sheet section 3 installs it there as root with
|
||||
managed settings. The args file's `--claude` carries it, and `case2-unit.sh` already refuses a binary
|
||||
that isn't root's or sits under the home. The driver passes the args file through and adds no claude
|
||||
check of its own. F6 is I2's.
|
||||
|
||||
## Sage's rulings during the build
|
||||
|
||||
- sage-1650 and sage-1652. Root always runs the rehearsal and maps its exit:
|
||||
- 0 goes on, or refuses `rehearsal-required` with `--no-rehearsal`.
|
||||
- 3 refuses `no-backup`, or with `--no-rehearsal` logs `rehearsal-skipped` and goes on.
|
||||
- Anything else refuses `rehearsal-failed`.
|
||||
|
||||
`--rehearse-port N`, default 7339. Hostcheck: a missing file or a nonzero `--check` refuses
|
||||
`profile-drift`.
|
||||
- sage-1653. `from_log newest`, and the external plan refusals `open-activation` and `migrate-required`.
|
||||
Residual: the `BACKUP_STORE` id and schema land in root's step log, but nothing ties that backup to the
|
||||
one the activation's own backup step takes later. A stopped backup before the upgrade would close that
|
||||
gap; it's a later row, not this one.
|
||||
|
||||
## Tests
|
||||
|
||||
`stack-upgrade.test.mjs` runs the driver as the test's user against a scratch root, with the test seams
|
||||
`STACK_UPGRADE_*`. The stand-ins are a fake `sudo` (logs its argv, checks the driver's form, then runs the
|
||||
real command as the test's user, r6's B1 pattern), a fake `case2-unit.sh` render that installs and starts
|
||||
the fixture units, a fake `systemctl` for root's system units, and a shell `hostcheck.sh`. The stack's
|
||||
user's steps run the real `stack-release.sh` and `stack-copy.sh` from a clone made from a real bundle. The
|
||||
units, copies and store are `stack-release-fixture.mjs`'s.
|
||||
|
||||
| Test | Covers |
|
||||
|---|---|
|
||||
| sequence 1 | confirm fails healthz, rollback with `--cause A5`, exit 1; every sudo call in the driver's form |
|
||||
| sequence 2 | `activate --migrate` fails at the wrapper, store restored, `--cause A3`, exit 1 |
|
||||
| sequence 3 | render fails, `--cause A4`; a probe run during `render-old` refuses `upgrade-running` |
|
||||
| sequence 4 | stage exits 3, A2f restarts and gates the old units, exit 1 |
|
||||
| N1 | a unit that won't stop: A2f's gate fails `stale-start`, exit 2, units stopped |
|
||||
| sequence 5 | confirm of the wrong copy refused, `--cause A5`, exit 1 |
|
||||
| sequence 6 | revert fails, exit 2; the next run refuses `interrupted-run` at its plan |
|
||||
| sequence 7 | two upgrades and a `rollback --to` with `--restore`; the args file after each confirm; `to-not-previous` |
|
||||
| sequence 8 | a migration that writes and fails, store back byte for byte, exit 1 |
|
||||
| lock | a second run while the lock is held: one log line, no call |
|
||||
| open-activation | any `AWAITING-UNITS` refuses at A1, nothing stopped |
|
||||
| migrate-required | refused without `--migrate`, no rehearsal, nothing stopped |
|
||||
| rehearsal-failed | the rehearsal's argv, exit 3, nothing stopped |
|
||||
| `BACKUP_STORE` | the line after a match; four malformed lines log `BACKUP_STORE none` |
|
||||
| no-backup | the real rehearsal with no backup refuses; `--no-rehearsal` logs the skip and goes on |
|
||||
| backup-last | the real rehearsal names the newest backup; `--no-rehearsal` with one refuses `rehearsal-required` |
|
||||
| args file | group write, other write, two `--copy`, an unknown flag, a symlink, `args-differ` |
|
||||
| hostcheck | nonzero `--check` and a missing file refuse `profile-drift` before the plan |
|
||||
| head-mismatch | checkout not at `--sha`; a commit the clone can't fast-forward to refuses `fetch-failed` |
|
||||
| usage | twelve bad argvs exit 4 with no log and no lock |
|
||||
|
||||
The step log check (`STEP`) runs in every test that reads the log: each line must be a step line or the
|
||||
result line, and none may hold the stack user's output (`stack-release:`, `PASS `, `FAIL `, `REHEARSAL`,
|
||||
the fake rehearsal's text).
|
||||
|
||||
## Mutants
|
||||
|
||||
`~/darkwing-scratch/r233/mutants.sh`, not committed because it edits the worktree. It has the brief's six
|
||||
mutants and two for sage-1650's mapping. The method is row 232's: a checked backup, exact replacements, a
|
||||
syntax check, the named killers, then the file put back with its sha256 checked. One control runs first:
|
||||
the whole `stack-upgrade.test.mjs` with no change. `DRY=1` matched each replacement once on the candidate
|
||||
(2026-10-21).
|
||||
|
||||
| Mutant | File | Killers |
|
||||
|---|---|---|
|
||||
| m1 drop the flock | stack-upgrade.sh | `a second run while the lock is held`, `sequence 3:` |
|
||||
| m2 release the lock when the rollback starts | stack-upgrade.sh | `sequence 3:` |
|
||||
| m3 skip A1's open-activation check | stack-release.sh | `A1 refuses open-activation` |
|
||||
| m4 skip the rehearsal | stack-upgrade.sh | `the rehearsal runs after the plan`, `sequence 1:` |
|
||||
| m5 accept a migrating target without `--migrate` | stack-release.sh | `migrate-required` |
|
||||
| m6 write the args file before confirm | stack-upgrade.sh | `sequence 7:`, `sequence 1:` |
|
||||
| m7 `--no-rehearsal` accepted with a backup | stack-upgrade.sh | `refuses rehearsal-required` |
|
||||
| m8 drop the `rehearsal-skipped` line | stack-upgrade.sh | `a real rehearsal with no backup` |
|
||||
|
||||
## Deviations
|
||||
|
||||
D1. The brief has A1 refuse `open-activation` for an open `AWAITING-UNITS` or a non-empty `interrupted`
|
||||
set. The interrupted case refuses with row 232's existing code, `interrupted-run`; only `AWAITING-UNITS`
|
||||
gives `open-activation`. Both reach root as `plan-refused`, exit 3 (sequence 6, the open-activation test).
|
||||
|
||||
D2. Root copies one line of the stack user's output into its log, `BACKUP_STORE`, on sage-1653. It
|
||||
matches a fixed pattern first, takes the last match, logs `BACKUP_STORE none` otherwise, and decides
|
||||
nothing on it.
|
||||
|
||||
D3. The stack user's steps carry no `--instance`. The driver takes `--instance dev` only, row 190's default
|
||||
instance, which the stack user's commands select with no flag. A named instance's units are a later row.
|
||||
|
||||
D4. Most tests fake the rehearsal in the fake `sudo`: the fixture's stub copies can't pass a real one. Two
|
||||
tests run the real rehearsal (`faults/real-rehearse`): no-backup and backup-last. `stack-release.test.mjs`
|
||||
covers `--backup last` itself.
|
||||
|
||||
D5. Any nonzero `--plan` exit refuses `plan-refused`, exit 3, whatever the code. The stack user's output
|
||||
above it names the refusal; root doesn't parse it.
|
||||
|
||||
D6. `rollback --restore ACT` is accepted without `--events`, as `stack-release.sh rollback` accepts it.
|
||||
The brief's usage pairs them. `--events` without `--restore` is a usage error.
|
||||
|
||||
D7. The refusal of any `STACK_UPGRADE_*` seam when run as root has no test, because the tests don't run
|
||||
as root. It is three lines at the top of the script.
|
||||
|
||||
D8. The fake `sudo` keeps the test's environment, where real `sudo` resets it. The driver passes
|
||||
everything the stack user needs through `env PATH=...` and `-H`, and the fake checks that form.
|
||||
|
||||
D9. Exit 3 means nothing stopped, not nothing changed. A refusal after `fetch` leaves the stack user's
|
||||
clone fast-forwarded and the new copy made. Both are the user's own state, and the next run reuses them.
|
||||
TOOLS.md and the run sheet say so.
|
||||
|
||||
D10. The brief's usage has neither `--no-rehearsal` nor `--rehearse-port`. Both come from sage-1650 and
|
||||
sage-1652.
|
||||
|
||||
## Decisions
|
||||
|
||||
- `rollback` has no HEAD check. Its target is an older copy than root's checkout, which already exists.
|
||||
It fetches, copies and rehearses nothing (sequence 7).
|
||||
- The new args file is written next to the old one (`.dev.args.XXXXXX`, same mode) before A2, so a full
|
||||
`/etc` refuses before anything stops. It moves into place after A5. A2f and the rollback remove it; a failed move keeps it and names it.
|
||||
- Every stack user step gets `--copies` explicitly, so the copies directory comes from the args file's
|
||||
`--home`, never from the user's environment.
|
||||
- The stack's user does the fetch, the fast-forward and the copy, so root never runs git on a tree that
|
||||
user can write.
|
||||
- A4 is the render, and `case2-unit.sh` starts the bus, then the Console. The forward path has no
|
||||
`systemctl start` of its own; only A2f does.
|
||||
|
||||
## Run environment
|
||||
|
||||
Worktree `/mnt/storage/scratch/darkwing-r233`, `TMPDIR=/mnt/storage/scratch/tmp/r233`, node from PATH. One
|
||||
whole `stack-upgrade.test.mjs` run takes about 56 s.
|
||||
|
||||
## Results
|
||||
|
||||
### Singles, run 1, 2026-10-21, Sage's slot (sage-1689)
|
||||
|
||||
`~/darkwing-scratch/r233/singles.sh` on `darkwing/row233-r1` (base e65a3ab4d, uncommitted tree), 03:48:20Z
|
||||
to 03:59:00Z. These are the test files that read a changed script. `stack-instances.test.mjs` reads
|
||||
`stack-release.sh`, so it joined the three named in the plan.
|
||||
|
||||
| File | Result |
|
||||
|---|---|
|
||||
| `stack-upgrade.test.mjs` | 20 pass, 0 fail |
|
||||
| `stack-release.test.mjs` | 194 pass, 1 fail |
|
||||
| `stack-release-external.test.mjs` | 25 pass, 0 fail |
|
||||
| `stack-instances.test.mjs` | 23 pass, 0 fail |
|
||||
|
||||
The failure was my test, not the script. "row 233: --backup last takes the newest backup..." compares
|
||||
`untouched(e)` before and after the run, and `untouched` includes `existsSync(e.log)`. The test took its
|
||||
before snapshot ahead of `rlog()`, which writes the log, so that flag went from false to true. Every hash in
|
||||
the snapshot matched. The fix takes the snapshot after `rlog()`. No other row 233 test calls `untouched`.
|
||||
|
||||
### Singles, run 2
|
||||
|
||||
`~/darkwing-scratch/r233/rerun.sh` on aacfa2d7a, Sage's slot (sage-1695), 04:13:05Z to 04:21:28Z:
|
||||
`stack-release.test.mjs` whole, 195 pass, 0 fail, 0 skipped. The fixed test passed. The other three files
|
||||
did not change after run 1. The gate and the mutants run next, in Sage's slot.
|
||||
|
||||
### Gate and mutants, 2026-10-21, Sage's slot (sage-1704)
|
||||
|
||||
`~/darkwing-scratch/r233/gate.sh` then `mutants.sh`, chained behind `busy.sh`, on d99166ade (code
|
||||
aacfa2d7a; d99166ade changes only this file). Counts are an awk sum over `gate-r1.out`.
|
||||
|
||||
| Part | Time (UTC) | Result |
|
||||
|---|---|---|
|
||||
| cli suite (`node --test tests/*.test.mjs`) | 04:29:01-04:37:27 | 616 pass, 0 fail, 0 skipped |
|
||||
| the 10 `scripts/test-*.sh` | 04:37:27-04:39:38 | selftests 429 passed, 0 failed; `test-queue`'s node part 156 pass, 0 fail |
|
||||
| gate total | | 1201 pass, 0 fail, 11 suites, `GATE_PASSED` |
|
||||
| c0 control | 04:39:38-04:40:35 | exit 0 |
|
||||
| m1-m8 | 04:40:35-04:41:25 | 8 of 8 killed, every one exit 1 |
|
||||
|
||||
Every restore passed its sha256 check, and `git diff` was clean at d99166ade afterwards. The EXIT trap
|
||||
removed the `darkwing-r233_default` network.
|
||||
|
||||
N1, found in the process sweep after the run: a `sleep 60` was left with its cwd in `packages/cli`.
|
||||
`stack-upgrade.test.mjs:312` spawns `flock LOCK sleep 60`, and its `t.after` SIGKILLs only the flock, so
|
||||
flock's child outlives the test by up to 60 s. It exited on its own. Row 232's T20
|
||||
(`stack-release.test.mjs:370`) spawns its holder `detached` and kills the group with `process.kill(-pid)`.
|
||||
The fix is the same two lines. That test is one of m1's killers.
|
||||
|
||||
Sage ruled it a fix now (sage-1707), and 08db6ac3f makes it, test only. After the gate's d99166ade
|
||||
the only change outside this file is that test:
|
||||
|
||||
```
|
||||
$ git diff --stat d99166ade 08db6ac3f
|
||||
packages/cli/tests/stack-upgrade.test.mjs | 4 ++--
|
||||
1 file changed, 2 insertions(+), 2 deletions(-)
|
||||
```
|
||||
|
||||
The gate stands on Sage's ruling. The rerun is the whole `stack-upgrade.test.mjs` singly plus m1, in
|
||||
Sage's slot, with a sweep for a leftover `sleep` afterwards.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Row 240 (I4): root's host check, its pins and the boot unit
|
||||
|
||||
Brief: `docs/plans/2026-10-20_installer-rows.md` section I4, blob `4a5a65020f00` (commit b00d7d94d, rev 2665,
|
||||
the pin row 240 carries; the I3 carry items came in with it). Sage split it out of I3 (sage-1650). Gitea #1746, Vikunja P19 #432. Owner Darkwing, reviewer Filbert, gate owner Sage.
|
||||
Design: row 195 DESIGN r6 B2, `agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66, :262-303 (the script,
|
||||
the three callers) and :839-860 (files, test cases, mutants). Where the brief and r6 differ, the brief wins.
|
||||
|
||||
## Files
|
||||
|
||||
Drafted on `darkwing/row240-r1` from d99166ade (row 233's candidate) while row 233's gate ran, then applied
|
||||
as one patch onto row 233's landing, 57f59e0c3, on `darkwing/row240-r2` (sage-1753). It applied cleanly; between
|
||||
the two bases the only change on this row's paths is 233's N1 test fix in `stack-upgrade.test.mjs`. Row 234
|
||||
then landed as c771a36f4 on 57f59e0c3. Sage preferred that the gate run with it in the tree (sage-1773), so both
|
||||
commits were picked onto c771a36f4 as `darkwing/row240-r3`. That was a clean pick: the 42 paths changed from
|
||||
57f59e0c3 to c771a36f4 share none with this row's 9.
|
||||
|
||||
- `scripts/case1-pins.sh` (new, 0644): `PIN_PROFILES_VERSION`, `PIN_PROFILE_SHA256`, `PIN_BUBBLEWRAP_VERSION`,
|
||||
r6 section 0's values.
|
||||
- `scripts/case1-hostcheck.sh` (new, 0755): the seven items and `--check`, `--enforce`, `--upstream`.
|
||||
- `packages/cli/systemd/mosaic-hostcheck.service` (new): the boot oneshot.
|
||||
- `packages/cli/tests/case1-hostcheck.test.mjs` (new, 16 tests). The cli suite's `tests/*.test.mjs` glob picks
|
||||
it up.
|
||||
- `docs/guides/host-run-sheet.md` section 4: the install lines that replace "Install them from row 240's run
|
||||
sheet lines once it lands", plus rollback, drift recovery and `--upstream` before a re-pin window. Sections
|
||||
3, 6, 8 and 10 for the I3 carry items below.
|
||||
- `scripts/stack-upgrade.sh` and `packages/cli/tests/stack-upgrade.test.mjs`: I3 N1 below.
|
||||
- `docs/TOOLS.md`: a new subsection "Root's host check (`scripts/case1-hostcheck.sh`, row 240)" after the
|
||||
upgrade driver's.
|
||||
|
||||
Brief line 257 said I3's `stack-upgrade.sh` doesn't change. Sage's ruling sage-1727 widened the row to it for
|
||||
N1 alone. Its default `STACK_UPGRADE_HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh` is where the run
|
||||
sheet installs this script.
|
||||
|
||||
## Carried from I3
|
||||
|
||||
Filbert's row 233 approve (Gitea 29922) left five notes. Sage ruled N1, N4 and N5 into this row and N2 and N3
|
||||
to DEFERRED.md (sage-1727).
|
||||
|
||||
| Note | Change |
|
||||
|---|---|
|
||||
| N1: `stack-upgrade.sh` exits 1 `args-failed` when the args move after A5 fails, but exit 1 means the old copy runs | The move's failure exits 5 `args-failed`. The header, TOOLS.md's step 5 and exit line, and run sheet section 6's exit list name exit 5 and what root does: move the new file the result line names into place. Test `row 240: I3 N1: a failed args move after A5 exits 5 ...` makes `/etc/mosaic-stack` read-only at confirm and checks exit 5, the step names, `args rc=1`, the result line, both units on the new copy, the old args file's bytes and the new file beside it. The step log regex takes `exit=5`. Mutant M8. |
|
||||
| N4: section 3's `npm install` has no npm on root's PATH, and the `--claude` path is unnamed while `node_modules/.bin/claude` is a symlink `case2-unit.sh:140` refuses | The install line runs with section 2's pinned node first on PATH (the postinstall runs `node` too). The `--claude` path is `node_modules/@anthropic-ai/claude-code/bin/claude.exe`, and a `--version` line catches the postinstall's 500-byte stub. |
|
||||
| N5: sections 8 and 10 lack precheck lines | Each gets "Precheck line: none" and its missing line. |
|
||||
|
||||
Exit 5 is new rather than 2 because 2 means both units are stopped, and here both run. Exit 4 stays usage.
|
||||
|
||||
The `claude.exe` path comes from the 2.1.304 package itself: `npm view` gives `bin.claude` as `bin/claude.exe`,
|
||||
and its `install.cjs` hardlinks the platform binary over that placeholder, or copies it across devices, then
|
||||
sets 0755. I downloaded the tarball into `/mnt/storage/scratch/tmp/r240/cc304` and read it; nothing ran. The
|
||||
run sheet says a newer pin needs the same check.
|
||||
|
||||
## The brief, item by item
|
||||
|
||||
| Brief line | Where |
|
||||
|---|---|
|
||||
| 247 `case1-pins.sh` holds the three pins | `scripts/case1-pins.sh`; the pins test checks the values against r6's |
|
||||
| 248-249 seven items, `--check` exits 0 or 1 | `items()` and the final dispatch; the check tests |
|
||||
| 250-251 `--enforce`: `apparmor_parser -R`, `root:root 0700`, `DRIFT`, a log line, exit 0 | `enforce()` and `stop_bwrap()`; the four enforce tests |
|
||||
| 252 `--upstream` compares the current deb to the pin | `upstream()`; the upstream test |
|
||||
| 253 the boot oneshot | `mosaic-hostcheck.service`; the boot unit test |
|
||||
| 254-255 fakes for every host tool, r6's seven case groups, seven mutants | the test file; Tests and Mutants below |
|
||||
| 257 the I3 run sheet item points here | run sheet section 4 |
|
||||
|
||||
## Tests
|
||||
|
||||
Fakes on PATH, each logging its argv to `$HC/calls.log`: `aa-status`, `dpkg`, `dpkg-statoverride`,
|
||||
`apparmor_parser`, `systemd-run`, `stat`, `chown`, `chmod`, `apt-get`, `dpkg-deb`. `host(t)` builds a scratch
|
||||
root (`/etc`, `/proc/sys`, `/usr/bin/bwrap`, `/var/log`) and a lib dir with a copy of the script and fixture
|
||||
pins that hold the fixture profile's sha256. Every run passes `--root`.
|
||||
|
||||
r6's case groups (:843-856) against the tests:
|
||||
|
||||
| r6 case | Tests |
|
||||
|---|---|
|
||||
| `--check` exits 0 with all seven, 1 with each failing alone, its line naming it | `check: exits 0 ...`, `check: exits 1 with each item failing alone ...`, plus one test per item for the item's edge cases |
|
||||
| item 3: a second file naming `bwrap` as a word, `/{usr/,}bin/bwrap` included; passes with the profile alone | `check: item 3 ...` |
|
||||
| item 4: `unpriv_bwrap (enforce)` or a trailing space fails; the argv carries `-p NoNewPrivileges=yes` and the 2a' properties | `check: item 4 ...` checks the whole argv |
|
||||
| item 7: the floor at 732 fails | `check: item 6 ...; item 7 ...` (733 passes as the control) |
|
||||
| `--enforce` exits 0 on a pass and every failure; unload, close, `DRIFT`, one log line; `DRIFT` present with every item passing still closes | `enforce: a pass ...`, `enforce: each failure ...`, `enforce: with DRIFT present ...` |
|
||||
| `--enforce` never calls `dpkg-statoverride` with `--update`, `--add` or `--remove` | `enforce: no unload ...; never writes the statoverride` (every call is `--list /usr/bin/bwrap`) |
|
||||
| a bubblewrap version other than the pin is a line, not a failure | `check: a bubblewrap version other than the install pin ...` |
|
||||
| `--upstream` against a differing deb prints the difference, exits 0 | `upstream: ...` (also: a match, a deb with no profile, a failed download) |
|
||||
| the unit: `Type=oneshot`, `After=apparmor.service`, `[email protected]`, runs `--enforce` | `boot unit: ...` |
|
||||
|
||||
The last test also checks that the repo script reads `scripts/case1-pins.sh` beside it, that bad usage exits
|
||||
2, and that a non-root run without `--root` refuses.
|
||||
|
||||
Singles are the two files that run a changed script: `case1-hostcheck.test.mjs` and `stack-upgrade.test.mjs`.
|
||||
Sage asked (sage-1759) whether `stack-release.test.mjs` or `stack-release-external.test.mjs`, which share
|
||||
`stack-release-fixture.mjs`, run `stack-upgrade.sh` or `case1-pins.sh`. They don't. `grep -cF` for
|
||||
`stack-upgrade`, `case1-pins`, `case1-hostcheck` and `hostcheck` gives 0 in all three files; the control
|
||||
`stack-release.sh` gives 2 in the fixture and 8 in `stack-release.test.mjs`. `scripts/stack-release.sh` names
|
||||
`stack-upgrade.sh` once, in a comment (:2061). The gate's cli suite runs both files anyway.
|
||||
|
||||
## Mutants
|
||||
|
||||
r6's seven (:858-860) and one for I3 N1, scripted in `~/darkwing-scratch/r240/mutants.sh` with r233's method: a checked backup,
|
||||
an exact replacement that must match once, `bash -n`, the named killer by `--test-name-pattern`, the restore
|
||||
and a sha256 check. A dry run matched all eight once each. Results below.
|
||||
|
||||
| Mutant | Change | Killer |
|
||||
|---|---|---|
|
||||
| M1 | `--enforce` exits 1 after the stop | `enforce: each failure exits 0 ...` |
|
||||
| M2 | `[ ! -e "$DRIFT" ] \|\| had=1` becomes `true \|\| had=1` | `enforce: with DRIFT present ...` |
|
||||
| M3 | item 1's `dpkg -S` becomes `owner=""; rc=1` | `check: item 1 fails when a package owns ...` |
|
||||
| M4 | item 3's `grep -rlw bwrap` becomes `grep -rlF /usr/bin/bwrap` | `check: item 3 ...` |
|
||||
| M5 | item 4's `[ "$label" = "$LABEL" ]` becomes a substring match | `check: item 4 ...` |
|
||||
| M6 | item 4 drops `-p NoNewPrivileges=yes` | `check: item 4 ...` (argv) |
|
||||
| M7 | item 7's `-gt` becomes `-ge` | `check: item 6 ...; item 7 fails with the floor at 732 ...` |
|
||||
| M8 | `stack-upgrade.sh`'s `finish 5 args-failed` becomes `finish 1 args-failed` (I3 N1) | `row 240: I3 N1: a failed args move ...` |
|
||||
|
||||
## Deviations
|
||||
|
||||
- Repo name `scripts/case1-hostcheck.sh` (r6), installed as `/usr/local/lib/mosaic-stack/hostcheck.sh`, the
|
||||
name in brief line 239 and I3's `stack-upgrade.sh` default. r6's apt hook line says `case1-hostcheck.sh`;
|
||||
the run sheet's hook uses the installed name.
|
||||
- `stat` is a fake too. r6 lists seven fakes; a test can't make a root-owned file, so item 5's `stat` reads a
|
||||
fixture.
|
||||
- Item 7 with no Console unit installed reads the port as 1023 and says so ("1023, because no Console unit
|
||||
is installed"). Until I2 installs the unit the floor must be above 1023. A unit with no `--port` fails.
|
||||
- Item 4 runs under `timeout 60`. A hung `systemd-run` would otherwise hang the apt hook and the boot unit.
|
||||
|
||||
## Decisions
|
||||
|
||||
- Fail closed where r6 is silent:
|
||||
- pins with no 64-hex sha fail item 1;
|
||||
- `dpkg -S` exit 1 (no owner) passes, exit 0 fails with the owner, any other exit fails with the code;
|
||||
- a failed `aa-status` fails item 2 and counts as loaded, so `--enforce` tries the unload.
|
||||
- `--root DIR` is the test seam (r6 :840). Root refuses it; a non-root user needs it. Both exit 2. Root runs
|
||||
with `PATH=/usr/sbin:/usr/bin:/sbin:/bin`.
|
||||
- `DRIFT` is append-only (`<stamp> <failed line>`), and `--enforce` never removes it. The log line is written
|
||||
only when the stop runs: `<stamp> enforce stop items=<ids or none> drift-before=<0|1> unload=<rc=N|no-file|skipped> chown=N chmod=N`.
|
||||
- `--upstream` exits 1 when the download or the unpack fails, since it then compared nothing. A deb without
|
||||
the profile file is a difference (exit 0), per r6 :302-303.
|
||||
- The bubblewrap note reads the version from `dpkg -s bubblewrap`.
|
||||
- The run sheet's recovery steps (`chown root:mosaic`, `chmod 0750`, `apparmor_parser -r`, then `--check`
|
||||
before root removes `DRIFT`) are mine. r6 says only that root removes `DRIFT` in a reviewed step.
|
||||
|
||||
## Run environment
|
||||
|
||||
Worktree `/mnt/storage/scratch/darkwing-r240b`, branch `darkwing/row240-r3` on c771a36f4, with `node_modules`
|
||||
linked to the canonical checkout's. Node v26.8.1, kernel 7.2.2-1-cachyos, 16 CPUs, TMPDIR
|
||||
`/mnt/storage/scratch/tmp/r240`, compose project `darkwing-r240b`. Sage's slot (sage-1773, 06:16Z). Each run is
|
||||
one chain behind `busy.sh`, launched from `/`: `~/darkwing-scratch/r240/run-all.sh` checks the HEAD pin and a
|
||||
clean tree, runs the singles, then the mutants (8 killed, none surviving, the tree clean after), then the gate
|
||||
(the cli suite and every `scripts/test-*.sh`), each stopping the chain on failure, then sweeps `/proc` cwd links
|
||||
for anything left in the r240 trees.
|
||||
|
||||
## Results
|
||||
|
||||
Run r1, 06:17:10Z on 47ec9fe2f, failed at the singles. `case1-hostcheck.test.mjs` passed 16/0/0, and
|
||||
`stack-upgrade.test.mjs` went 20 pass, 1 fail. The failure was the N1 test's own `t.after` hook, which ran
|
||||
`chmodSync(e.etc, 0o755)` after the fixture's cleanup had already removed the scratch root, so it threw ENOENT.
|
||||
Every assertion in the test body had passed, exit 5 included. The fixture's cleanup runs `chmod -R u+w` on the
|
||||
root before `rmSync`, so the 0555 dir never needed the hook. b45852daa drops it and changes nothing else. The
|
||||
mutants and the gate didn't run. The sweep found nothing left. Sage counted r1 as this slot's one failure
|
||||
(sage-1788).
|
||||
|
||||
Run r2, 06:20:41Z to 06:33:28Z on b45852daa, passed. The counts come from awk over the .out files
|
||||
(`~/darkwing-scratch/r233/sum.sh`):
|
||||
|
||||
| Step | Result |
|
||||
|---|---|
|
||||
| `case1-hostcheck.test.mjs` | 16/0/0 |
|
||||
| `stack-upgrade.test.mjs` | 21/0/0 |
|
||||
| c0 control | exit 0 |
|
||||
| M1-M8 | 8/8 killed, none survived, every restore's sha256 matched, tree clean after |
|
||||
| `packages/cli` suite | 639/0/0 |
|
||||
| `scripts/test-*.sh` | auth 15, conductor 17, config 31, discord 78, extension-package 18, foundation 44, land-work 86, queue 156 node + 28 selftest, release 14, task 98; 585 pass, 0 fail |
|
||||
| Gate total | 11 suites, 1224 pass, 0 fail |
|
||||
| Sweep | nothing left in the r240 trees; the compose network was removed |
|
||||
|
||||
This commit adds only these BUILD.md lines. No test reads BUILD.md.
|
||||
@@ -1,100 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,332 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,121 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,121 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,186 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,10 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,12 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,28 +0,0 @@
|
||||
✖ 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'
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
✔ 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)
|
||||
@@ -1,13 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,35 +0,0 @@
|
||||
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'
|
||||
@@ -1,13 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,12 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
✖ 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'
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
✖ 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'
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,50 +0,0 @@
|
||||
✖ 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'
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
✖ 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'
|
||||
@@ -1,13 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,35 +0,0 @@
|
||||
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/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)
|
||||
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/peercred.mjs'
|
||||
}
|
||||
|
||||
Node.js v26.8.1
|
||||
✖ packages/bus/tests/peercred.test.mjs (31.978232ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 37.606365
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/peercred.test.mjs:1:1
|
||||
✖ packages/bus/tests/peercred.test.mjs (31.978232ms)
|
||||
'test failed'
|
||||
@@ -1,15 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,12 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,35 +0,0 @@
|
||||
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/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)
|
||||
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/principal.mjs'
|
||||
}
|
||||
|
||||
Node.js v26.8.1
|
||||
✖ packages/bus/tests/principal.test.mjs (28.063196ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 33.574982
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/principal.test.mjs:1:1
|
||||
✖ packages/bus/tests/principal.test.mjs (28.063196ms)
|
||||
'test failed'
|
||||
@@ -1,16 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,11 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,12 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,2 +0,0 @@
|
||||
baseline human-required
|
||||
candidate human-required
|
||||
@@ -1,9 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,36 +0,0 @@
|
||||
✖ 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'
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,43 +0,0 @@
|
||||
✖ 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'
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,36 +0,0 @@
|
||||
✖ 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'
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,32 +0,0 @@
|
||||
✖ 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'
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"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
@@ -1,83 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
@@ -1,143 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
@@ -1,10 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,29 +0,0 @@
|
||||
✖ 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'
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,29 +0,0 @@
|
||||
✖ 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'
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,10 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,18 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,11 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,35 +0,0 @@
|
||||
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'
|
||||
@@ -1,22 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,13 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,18 +0,0 @@
|
||||
✔ 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
|
||||
@@ -1,65 +0,0 @@
|
||||
// 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});}
|
||||
@@ -1,20 +0,0 @@
|
||||
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;}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Gizmoduck
|
||||
|
||||
Design and builder seat, Claude Opus 5.5, a T3 thread (9312a896). Author of the federation design
|
||||
(`agents/gizmoduck/work/federation/DESIGN.md` on `gizmoduck/federation-design`, #414). Builds FED R1, the
|
||||
access.json loader and validator, under Mos's order mos-1023 item 1.
|
||||
|
||||
No launcher in this directory. The seat works in its own worktree and has no credentials of its own; until
|
||||
the credentials row settles per-agent identity, it pushes only its own `gizmoduck/*` branches through the
|
||||
repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,310 @@
|
||||
# Row 234 (#436a row a, Gitea #1740): host enrollment, store records and bus schema 3i to 3j
|
||||
|
||||
Gizmoduck, 2026-10-21. Reviewer Duckworth, gate owner Sage. Lands by 10-25, before the .53 and .54 installs
|
||||
(mos-1134).
|
||||
|
||||
- Brief: `docs/plans/2026-10-20_host-credentials.md`, section "Machine grant row a: enrollment, store records
|
||||
and migration 3i-3j", blob 64451be5 (queue rev 2617).
|
||||
- Design: `agents/gizmoduck/work/machine-grant/DESIGN.md` r6, f9039682e on `gizmoduck/machine-grant-design`,
|
||||
approved by Duckworth (duckworth-0064, REVIEW-r6.md at 8ea792256). Not reopened here. Cites "r6:" are lines
|
||||
of that file.
|
||||
- Grant event rule: #436b section 7, fa5658941 on `megavolt/row199`,
|
||||
`agents/megavolt/work/436b/DESIGN.md:431-490`, file sha256
|
||||
96510b83778e79a71b33d0d5f6fd0c806dfe74f6de315ff4a9f1fbe5620f6c69, approved by Duckworth (duckworth-0067).
|
||||
- Base: origin/refactor 8fede5fc6, with rows 199 (cb5fdb064) and 210 (f5753c0e8) landed. Branch
|
||||
`gizmoduck/row234`. Code cites are `path:line` at 8fede5fc6 unless they say otherwise.
|
||||
|
||||
## Names: r6 against section 7
|
||||
|
||||
r6's `grant.*` and `host.enroll.*` names are `body.action` values on `action.allowed` events, not event
|
||||
kinds. The `events` kind CHECK isn't rebuilt. Four r6 names move under `grant.`:
|
||||
|
||||
| r6 | This row |
|
||||
|---|---|
|
||||
| `host.enroll.step` | `grant.host.enroll.step` |
|
||||
| `host.enrolled` | `grant.host.enrolled` |
|
||||
| `host.checked` | `grant.host.checked` |
|
||||
| `host.removed` | `grant.host.removed` |
|
||||
|
||||
Only the consume record carries `decision`: `{operation: 'grant.host-enroll', decision}`. Every other
|
||||
guarded body names the decision `grantDecision`, so r6's `decision` field in step, enrolled and failed bodies
|
||||
is `grantDecision` here. The actor of every guarded event is `actor_role` NULL with `actor_run` 'grant',
|
||||
and `body.by` names the caller the broker checked.
|
||||
|
||||
## Split (r6 section 12 Q-S4)
|
||||
|
||||
Row a builds:
|
||||
|
||||
1. **Schema 3i to 3j.** `migrations/3i-3j.sql` adds `hosts` and `host_grants` (r6 S1, primary keys
|
||||
(business, host) and (business, agent, host)) and the guard trigger: an `action.allowed` whose
|
||||
`body.action` or `body.operation` GLOBs `'grant.*'` aborts unless the actor is (NULL, 'grant').
|
||||
`SCHEMA_VERSION` 3j, a migration test with a step that fails partway.
|
||||
2. **Grant event writer.** A private session-less broker writer for guarded events. `#event` doesn't change.
|
||||
`recordEvent` refuses `reserved-event` for the guarded namespace.
|
||||
3. **The `grant` raise frame.** `decision.raise` takes `grant` only with `credential.mint`, only from the chief
|
||||
(`grant-chief-only`), and in row a only with `method: 'host-enroll'`. Any other method refuses
|
||||
`invalid-request` until row b adds `host`. E1's checks, the `ssh-keyscan` set compare before the raise
|
||||
transaction, and the rendered block.
|
||||
4. **Enrollment executor.** `#consumeGrant`, the start on the human's cli yes (X1), E2 steps 1 to 4, the
|
||||
installer key at `<configDir>/secrets/hosts/<business>/<hostId>/installer`, the failure cleanup, and the
|
||||
start reconcile for enrollments: the `registry` step undo, and a resolved-yes unspent enrollment ending
|
||||
`grant.failed interrupted` (X3).
|
||||
5. **`hosts.check` and `hosts.remove`** (E4), with `mosaic hosts status|check|remove`.
|
||||
6. **`tools/host/`**: `mosaic-keys`, `mosaic-keys-write`, `mosaic-keys.sudoers`, `mosaic-keys.sshd.conf` (E3).
|
||||
7. **`host.run` on the store** (X4 first and third bullets): it reads the `hosts` and `host_grants` records
|
||||
and the Approve record check, and at start the runtime gives `Credentials` a reference per `host_grants`
|
||||
record. The loader refuses `hosts` at the top level and in a role (S1). Row 210's tests move to store
|
||||
fixtures.
|
||||
8. **Reader filter** (duckworth-0067 N1): every grant-evidence reader in this row (status, the start
|
||||
reconcile, `openGrantDecisions`) filters `kind = 'action.allowed' AND actor_role IS NULL AND actor_run =
|
||||
'grant'`, with a test that an `action.refused` look-alike and a session-written one are ignored.
|
||||
`hosts.remove`'s `hostLineLeft` reader is one of them (duckworth-0079 M4).
|
||||
|
||||
Row b keeps the `host` method's raise binding (R4), the grant executor (X2), the grant rows of X3, X5,
|
||||
`Credentials.register`/`claim`/`abortClaim`, revoke (section 6) and `sweep`. So row a has no way to make a
|
||||
`host_grants` record outside a test fixture, and from its landing to row b's no agent holds a host grant
|
||||
(r6 N9, the run sheet strips row 210's keys).
|
||||
|
||||
Tests in row a (r6 section 10): G1 and G5 for the `host-enroll` raise, G6 and G10 for the enrollment
|
||||
executor, G11's enrollment cases, G14, G15, G16, G17, G18, G20, G21 (its `host-line-left` case reads an
|
||||
event fixture until row b writes `hostLineLeft`), G25, the migration test, the trigger cases, the reader
|
||||
filter test, and row 210's T-tests on store fixtures. Row b: G2 to G4, G7 to G9, G11's grant cases, G12,
|
||||
G13, G19, G22 to G24, G26 to G28.
|
||||
|
||||
## Early check items (duckworth-0079, REVIEW-build-split.md 10672772e on `duckworth/reviews`)
|
||||
|
||||
Moves into row a:
|
||||
- M1. G4 for the `host-enroll` block (r6:240-242): a question can't forge the block, and the 4096-character
|
||||
limit holds with the block appended. G4 for the `host` block stays in row b.
|
||||
- M2. The tag function (r6:265-266), because E2 step 2 tags the installer line. G24's first sentence moves
|
||||
here: names of 64 characters with `.` and `_` give distinct tags of the fixed length. The `sweep` half
|
||||
stays in row b.
|
||||
- M3. A store-fixture test that `host.run` refuses `host-not-granted` for no record, an unresolved decision,
|
||||
a decision resolved by an arbiter, the tester or an agent or not via cli, a consume record with operation
|
||||
`grant.host-enroll` in place of `grant.host`, and an unspent decision (X4 first bullet, r6:593-596). A
|
||||
restart test that a `host_grants` fixture gives `Credentials` a reference (X4 third bullet).
|
||||
|
||||
G21's `host-line-left` case:
|
||||
- F1. The fixture goes through this row's private grant writer in the real shape: `action.allowed`, actor
|
||||
(NULL, 'grant'), `body.action` 'grant.failed', `body.host`, `body.hostLineLeft`. No raw SQL around the
|
||||
trigger.
|
||||
- F2. `hosts.remove` reads `body.hostLineLeft` from any guarded body, not only `grant.failed`.
|
||||
- F3. Decided: a guarded body that carries `hostLineLeft` also carries `host`. Row b adds `host` to
|
||||
`grant.revoke.closed`, so its body is `{tag, host, hostLineLeft}` (r6:716 has `{tag, hostLineLeft}`).
|
||||
The reader matches on `body.host`, and doesn't map tags back to hosts through `grant.step`. A direct
|
||||
field is one equality in SQL, and a tag lookup would add a second reader that the N1 filter also has to
|
||||
cover.
|
||||
- F4. Row b carry: "no later `sweep` cleared it" (r6:403) needs `grant.sweep`, which only row b writes. Row
|
||||
b adds the sweep clear to `hosts.remove` with a G21 case, and its G8 or G13 shows `hosts.remove`
|
||||
refusing after a real `hostLineLeft` write, not only after the fixture. Until then row a's check is "any
|
||||
guarded `hostLineLeft` for this host refuses".
|
||||
|
||||
## Mutants (r6:838-883, numbered here)
|
||||
|
||||
Row a, killed on this row's tree:
|
||||
|
||||
| # | Mutant |
|
||||
|---|---|
|
||||
| A1 | drop the chief check |
|
||||
| A2 | start the executor on a `credential.mint` with no `grant` |
|
||||
| A3 | start it on an arbiter resolve |
|
||||
| A3b | start it on a `cli` yes by someone other than the human (duckworth-0083 N8) |
|
||||
| A4 | start it on `no` |
|
||||
| A5 | skip the keyscan compare |
|
||||
| A6 | compare only one key |
|
||||
| A7 | write the private key into the event body |
|
||||
| A8 | drop the "not yet enrolled" check in E2 step 1 |
|
||||
| A9 | drop the `hosts` primary key |
|
||||
| A10 | skip the start check for a resolved, unspent enrollment decision |
|
||||
| A11 | `host.run` reads the business file instead of the grant record |
|
||||
| A12 | build the tag without the business |
|
||||
| A13 | drop `from=` in the writer |
|
||||
| A14 | let the writer touch a line outside the tag pattern |
|
||||
| A15 | drop `O_NOFOLLOW` on the user file |
|
||||
| A16 | drop one group from the writer's refused list |
|
||||
| A17 | drop the user pattern check before the path use |
|
||||
| A18 | treat a `privileged` entry as enough without `users` |
|
||||
| A19 | let the writer accept a sudo user not in `privileged` |
|
||||
| A20 | trigger: drop the `body.operation` arm, so the consume record passes from a session |
|
||||
| A21 | trigger: accept actor_role NULL with any actor_run |
|
||||
| A22 | a grant reader drops the `kind = 'action.allowed'` or actor filter. The runner splits it: A22a drops the kind, and G21's `action.refused` look-alikes kill it. A22b drops the actor, which the trigger can't stand in for, because 3i-3j adds the trigger without checking old rows. Its killer is the grant-migration test "A22b, sage-1703": a session-written `grant.host.removed` with `hostLineLeft` and a `grant.host-enroll` row from 3i, migrated to 3j, where `hosts.remove` still removes the host and `reconcileGrants` ends nothing |
|
||||
| A23 | `host.run` skips the Approve record check (any `host_grants` record is enough) |
|
||||
|
||||
Row b keeps the rest of r6's list: the `ready` check, the three hash compares, the definition check, the
|
||||
install and record order, `remove` on a verify failure, both start sweeps, the reconcile's remote `remove`,
|
||||
the `grant.step` before install, revoke leaving the record, `register` before `issue`, the abort check and
|
||||
the `await` before `issue`, revoke and an unreferenced claim, `sweep` and any `mosaic-` line, revoke's host
|
||||
step order, claim before consume, the resolved unspent grant decision, an async `commit`, `hosts.revoke`
|
||||
through `credential-verbs.mjs`, the revoke marker reconcile, the upsert, the undo past `grant.revoked`, the
|
||||
abort after step 5 and a missing `bus.tester` as dev.
|
||||
|
||||
## Broker functions this row touches (sage-1677)
|
||||
|
||||
For row 236 (Megavolt), which rebuilds on this landing. Old lines are `packages/bus/src/broker.mjs` at
|
||||
8fede5fc6, new lines are at the last commit that changed this file (they moved by 10 after `runEnrollment`
|
||||
went in). The planned list is kept in this file's history (827b89ea6).
|
||||
|
||||
Changed:
|
||||
- `constructor` (:279): one more option, `hostGrant`, the object `host-grant.mjs` builds.
|
||||
- `recordEvent` (old :1243-1251, new :1606): one more refusal. An `action.allowed` whose `body.action` or
|
||||
`body.operation` is a `grant.*` name refuses `reserved-event`.
|
||||
- `request()` (old :1362, new :1726): two routes ahead of the sync path, a `decision.raise` with `grant` to
|
||||
`#raiseGrant` and the `hosts.*` verbs to `#requestHosts`. Both are async.
|
||||
- `requestHost` (old :1712-1795, new :2079): the business-file lookups (old :1730-1736) become one
|
||||
`#hostAccess` call, in the same transaction as `#agent`. `commands` and `entry` come from it.
|
||||
- `#dispatch` (old :1796, new :2160) gets a fifth parameter, `enroll`, which only `#raiseGrant` passes.
|
||||
- `decision.raise` (new :2244): `grant` joins the allowed keys, and `grant` without `enroll` (or the
|
||||
reverse) refuses `invalid-request`. The `host.sudo` block reads `#hostAccess` in place of `b.hosts`
|
||||
and the role's `hosts`, and an undefined `target` still refuses `host-unknown`. `validArgv` and
|
||||
`validStdin` now run after the access check, so a malformed argv for an ungranted host refuses
|
||||
`host-not-granted` first. The enrollment block is appended to the question before `string(question)`,
|
||||
so the 4096 cap counts it (M1). The decision's raise body carries `grant: enroll`.
|
||||
- `decision.resolve` (new :2409): when `op` is `resolved`, `#approvedGrant` reads the decision, and a
|
||||
`host-enroll` grant starts `#runEnrollment` from `setImmediate`, after the transaction commits. The
|
||||
result adds `grant: 'started'`.
|
||||
|
||||
New, all private but two:
|
||||
- `#grantEvent` (:657), the writer of guarded events. It runs the secret check, then `writeGrantEvent`.
|
||||
- `#approvedGrant` (:667), `#consumeGrant` (:678). `#consumeGrant` sits after `#approvedGrant`, not next
|
||||
to `#consumeAuthority` as planned.
|
||||
- `#hostRecord` (:685), `#hostAccess` (:692), `#grantNotice` (:708), `#enrollFailed` (:736),
|
||||
`#runEnrollment` (:750), `#openGrantDecisions` (:823).
|
||||
- `runEnrollment` (:745), public, a trusted entry to `#runEnrollment` for G10. No verb reaches it.
|
||||
- `reconcileGrants` (:833), public, which `runtime.mjs` calls once at start.
|
||||
- `#raiseGrant` (:866), `#hostsCaller` (:886), `#requestHosts` (:900), `#hostLinesLeft` (:964).
|
||||
|
||||
Module level, new (:184-230): `GRANT_RUN`, `grantName`, `GRANT_EVIDENCE` (the N1 filter as SQL),
|
||||
`registrySha256`, `writeGrantEvent` (exported, the one writer of guarded events, which test fixtures use
|
||||
for F1), `HOSTS_VERBS`, `fingerprintOf`, `INSTALLER_COMMENT`.
|
||||
|
||||
Unchanged: `#event`, `#spent`, `#consumeAuthority`.
|
||||
|
||||
Other `src` files:
|
||||
- `bus/src/host-grant.mjs`, new: the keyscan, the installer key pair, and `hosts.check`'s `list`.
|
||||
- `bus/src/runtime.mjs`: builds `hostGrant`, calls `reconcileGrants` before the server starts, and adds
|
||||
the `host_grants` references before `new Credentials` (X4 third bullet).
|
||||
- `bus/src/host-run.mjs`: `runHost` takes `command`, sent as is in place of the sudo argv, for the
|
||||
installer's `list`.
|
||||
- `bus/src/server.mjs`: the `hosts.*` verbs and a raise with `grant` go down the async path.
|
||||
- `bus/src/store.mjs`: the 3i-3j step, `SCHEMA_VERSION` 3j. `bus/src/credentials.mjs` exports
|
||||
`privateDir`.
|
||||
- `business/src/business.mjs` and `business/src/hosts.mjs`: the loader refuses `hosts` (S1).
|
||||
`checkHosts` and `checkHostGrants` are gone.
|
||||
- `cli/src/hosts.mjs`, new, routed from `cli.mjs` and `scripts/mosaic`.
|
||||
|
||||
## Decisions taken in the build
|
||||
|
||||
- **`#hostAccess` binds the agent and the host.** X4 says the Approve record check passes when the
|
||||
decision is an approved `host` grant with a `grant.host` consume. The check also requires the grant's
|
||||
`agent` to equal the session's role and its `host` to equal the requested host. Without that, an
|
||||
approved grant for one agent and host would vouch for any `host_grants` row that names its decision.
|
||||
Cost: none for row b, whose executor writes the row from the same grant.
|
||||
- **Store fixtures are written the real way.** `bus/tests/host-fixtures.mjs` inserts the decision rows by
|
||||
SQL, but every guarded event goes through `writeGrantEvent`, so the trigger sees it (F1). An enrollment
|
||||
fixture writes `grant.host.enrolled`, or the start reconcile reads it as interrupted.
|
||||
- **The resolver test for host kinds is gone.** `business/tests/hosts.test.mjs` lost its D5 case, because
|
||||
the loader now refuses `host.<id>` credential refs. The `isHostKind` filter in `business/src/resolve.mjs`
|
||||
can't be reached now. I left it in, since row b may want it back, and it's one line.
|
||||
- **Store before Credentials.** The runtime already opened the store before `Credentials`. The
|
||||
`host_grants` references depend on that order, and the comment there says so.
|
||||
- **CLI.** `mosaic hosts` prints tab lines, and `--json` prints the bus rows. Usage errors exit 4 and bus
|
||||
refusals exit 2, as `mosaic credentials` does. There is no enroll command: enrollment is the chief's raise.
|
||||
- **Host tools tests live in the bus suite.** `bus/tests/host-tools.test.mjs` loads the writer by path
|
||||
through a Python harness under `python3 -B -I`. The harness passes its own key dir, config dir, owner
|
||||
uid and user, group and sudo lookups, and runs `mosaic-keys` from a copy with stub `sudo` and `logger`.
|
||||
The gate runs the bus suite, so G16 and G25 run there. The real `pwd`, `grp` and `sudo -l` paths are
|
||||
checked on the #433 VM, not here. The test also runs `visudo -c` on the sudoers file and `sshd -T` on
|
||||
the drop-in.
|
||||
- **Writer details the design left open.** The `from` file must hold exactly one value matching
|
||||
`[0-9A-Za-z.:/*?!_-]+(,[0-9A-Za-z.:/*?!_-]+)*`, so it can't close the quote. `users`, `privileged` and `from` must be
|
||||
regular, owned by the owner uid and not group or world writable, and are opened without following a
|
||||
symlink. Lines starting with `#` are comments. A refusal exits 1 with its code on stderr and in syslog.
|
||||
`add` of a tag that is already there exits 0 and writes nothing.
|
||||
- **`runEnrollment` is public for G10 (duckworth-0081 B1).** G10 needs a second start for a spent
|
||||
decision, and the resolve path can't give one: a second resolve refuses `decision-closed`
|
||||
(broker.mjs:2416) before the executor runs. So
|
||||
`#runEnrollment` returns its code (`enrolled`, `host-enrolled` or the refusal), and a public
|
||||
`runEnrollment` calls it, as `reconcileGrants` is public. The test checks a spent
|
||||
decision gives `decision-consumed`, and an open or agent-resolved one gives `not-authorized`, with
|
||||
nothing written. That second case also reaches the `via` and `by` checks in `#approvedGrant`, so A3 is
|
||||
no longer vacuous (N1).
|
||||
Who can call it (sage-1692): only code holding the `Broker` object, which is the bus process and the
|
||||
tests. The one `new Broker` in `src` is `runtime.mjs:126`, and no `src` file calls `runEnrollment`;
|
||||
the resolve path calls the private `#runEnrollment` from `setImmediate` (broker.mjs:2445). A seat
|
||||
holds no `Broker`. It reaches the bus only over `broker.sock`, where `server.mjs` calls a fixed set of
|
||||
methods by name (`request` at :134 and :140, `identity`, `disconnect`). `request()` routes on the verb
|
||||
through the `#dispatch` switch, and neither file looks a method up by name (no `this[` in either), so no
|
||||
verb string can reach `runEnrollment`.
|
||||
- **A `sudo -l` that times out refuses `user-sudo` (N2).** `_sudo` returns an empty answer on
|
||||
`TimeoutExpired`, which isn't the expected line, so `main` logs the refusal and exits 1.
|
||||
- **Keyscan comment (N3).** A scan that can't start refuses `host-failed`. One that runs and finds no key
|
||||
returns no lines, and the raise refuses `host-key-mismatch`. The comment in `host-grant.mjs` now says so.
|
||||
- **No tag on in-run `grant.failed` (N4).** r6:578 tags only the `interrupted` one, so the code follows it.
|
||||
- **Run sheet (N5).** The writer refuses `config-privileged` when the file is missing, so each host needs an
|
||||
empty, root-owned `/etc/mosaic-keys/privileged` (0644) before the first enrollment.
|
||||
|
||||
## Mutant results (Sage's slot, 2026-10-21)
|
||||
|
||||
All 25 killed: A1 to A23, with A3b added for duckworth-0083 N8 and A22 split into A22a and A22b. The
|
||||
runner is `mutants.py` outside the repo. Each mutant gets its own session, a 420 s limit with a group kill,
|
||||
and a byte restore checked against git status. None hung. Run 1 (04:24Z, on 95f966b1c) killed A1 to A22a
|
||||
and stopped at A22b, which survived. Run 2 (04:43Z, on 097892741) killed A22b and A23.
|
||||
|
||||
- Each kill comes from a named assertion in the mutant's own test files, except A9. Dropping the `hosts`
|
||||
primary key leaves `host_grants`' foreign key with no parent key, so the store refuses to open
|
||||
("foreign key mismatch") and 28 of 29 tests fail at setup. That's a structural kill, not the S1 test.
|
||||
The schema can't exist without the key (sage-1709).
|
||||
- A22b drops the actor filter from `GRANT_EVIDENCE`. The trigger can't stand in for it, because a 3i store
|
||||
can hold a session-written grant row that the 3i-3j step keeps. The grant-migration test "A22b,
|
||||
sage-1703" kills it (see the A22 row above).
|
||||
|
||||
Follow-up, not in row a: G18's control calls `runHost` directly, so nothing runs `hg.check` end to end
|
||||
with a good pin (duckworth-0083 N9). The #433 VM run covers it.
|
||||
|
||||
## Status at 097892741
|
||||
|
||||
Done: every Split item, 1 to 8, duckworth-0081 B1 to B3, duckworth-0083 N8, and all 25 mutants killed.
|
||||
|
||||
Single files in the slot, all exit 0, 178 pass and 0 fail by an awk sum over 23 logs: bus grant-enroll
|
||||
24, host-run 24, grant-migration 6, host-tools 6, account-migration 3, chief-of-staff 18,
|
||||
comment-migration 5, hard-waits 13, name-migration 1, project-runtime 3, spec-migration 1,
|
||||
thread-migration 3, usage-migration 1, project-files 8, spec-socket 6, spec-review 12, land 17, spec 4;
|
||||
harness spec-tools 2, raise-domain 2; business hosts 4; cli hosts 5; seat sandbox 10.
|
||||
|
||||
Left: Sage runs the whole bus, business and cli suites and every `scripts/test-*.sh` at the landing gate,
|
||||
after Duckworth approves.
|
||||
|
||||
## Round 3 (sage-1742): two tests that count the schema
|
||||
|
||||
The landing gate failed `packages/bus/tests/schema.test.mjs:522`. It counts the tables and triggers that
|
||||
`schema.sql` creates, and row a adds two tables and a trigger, so the counts go from 16 to 18 tables and
|
||||
from 70 to 71 triggers. My singles didn't run it because I took it for a test of other rows' tables.
|
||||
|
||||
I grepped `packages/*/tests/*` for `sqlite_master` and `schema.sql`, with `schema.test.mjs:517` as the
|
||||
positive control. That turned up one more test that would fail: T46 in
|
||||
`packages/cli/tests/stack-release.test.mjs:910`. It puts every schema table in exactly one of four lists, and
|
||||
hosts and host_grants were in none. I'd left the stack-release tests out of the r2 singles on the claim that
|
||||
they don't load the bus, but T46 reads `schema.sql`. Both tables go in its untimed list (Sage's ruling,
|
||||
sage-1745: the message_threads pattern). Neither has an `at` column. Row a writes hosts at five lines,
|
||||
and each runs in one store transaction with a `grant.*` events row, which the restore guard counts.
|
||||
`#grantEvent` (broker.mjs:657) writes that row through `writeGrantEvent` (:201, INSERT INTO events at
|
||||
:212).
|
||||
|
||||
- :737 DELETE, in `#enrollFailed`, with grant.failed at :738. Both callers wrap it in a transaction,
|
||||
:816 and :849.
|
||||
- :763 INSERT, in the step 1 transaction (:754), with grant.host.enroll.step at :761.
|
||||
- :783 UPDATE installer_fingerprint, in the step 4 transaction (:780), with grant.host.enrolled at :784.
|
||||
- :932 DELETE, in the hosts.remove transaction (:927), with grant.host.removed at :933.
|
||||
- :950 UPDATE state ready, in the hosts.check transaction (:944), with grant.host.checked at :951.
|
||||
|
||||
Row a writes no host_grants rows. It only reads them (broker.mjs:695, :930, runtime.mjs:103). Row b adds
|
||||
the writes, and each must share a transaction with a `grant.*` event the same way. That goes on row
|
||||
235's carry list. The other hits pin an older schema
|
||||
(account, comment, name, spec, thread and usage migrations, chief-of-staff 3c) or count only their own
|
||||
tables (state.test.mjs:106 reads every table but pins no list, threads.test.mjs:129).
|
||||
|
||||
hosts and host_grants are mutable by design, so the refused-labels loop above `schema.test.mjs:517` gets no
|
||||
entry for them. hosts.check updates a host to ready. hosts.remove and a failed enrollment delete the
|
||||
record. Row b updates and deletes grant records. Neither table has append-only triggers. The one new
|
||||
trigger, events_grant_actor, guards events. grant-migration.test.mjs tests it, and it killed mutants
|
||||
A20 and A21.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Gosalyn
|
||||
|
||||
Worker seat for Mosaic Stack, MVP track T2 (Case 2, the creative writing hub, Vikunja #416).
|
||||
|
||||
- T3 thread: 7604438a-0ded-41c8-bd3f-26f57fdb5cd2
|
||||
- Model: Opus 5.5 (Claude Code, T3)
|
||||
- Launched by Mos (Jason's main agent, T3 38bc4c81-6270-4021-b00c-6a15c43b9b0e), who briefs the seat
|
||||
- Repository lead and queue: Sage (T3 1ef1e4f8-3ead-4208-beca-38f9f1add079)
|
||||
- Branch: gosalyn/case2-design, pushed only by this seat, landed on refactor by Sage
|
||||
|
||||
Work lives in `work/`. The Case 2 design is `work/case2/DESIGN.md`.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Row 230: C2-13a unit render and host precheck
|
||||
|
||||
Gitea #1732. Vikunja #416 (track T2). Brief: docs/plans/2026-10-20_case2-rows.md, section "C2-13a",
|
||||
blob f6b4933f0f47 (01b40d357, sage-1562). Base: 00d4cc1f6 (row 190). Reviewer: Webby. Gate: the cli
|
||||
suite plus every scripts/test-*.sh.
|
||||
|
||||
## What ships
|
||||
|
||||
`scripts/case2-unit.sh` renders `[email protected]` and `mosaic-webui.service` from the two existing
|
||||
templates into the system unit directory, for the default instance only. It writes each unit through
|
||||
a temp file at mode 0644, runs `daemon-reload`, enables both units, starts the bus and then the
|
||||
Console. If the bus fails to start, the script refuses and leaves the Console unstarted. Both units
|
||||
get `WantedBy=multi-user.target`, and neither gets `After=` or `Wants=`. The script never links a unit
|
||||
file into place. A re-install refuses unless each installed unit passes the same checks, both units
|
||||
name one copy, and each equals the render apart from the copy id.
|
||||
|
||||
`--profile library` (the default) refuses `--pm`, `--claude`, `--tester-port` and `--sso`, and needs
|
||||
the preview proxy flags plus the proof pair. `--profile factory` refuses the six proxy flags and takes
|
||||
`--pm` and `--claude`. Both refuse `--checkout` and `--board`.
|
||||
|
||||
`scripts/case2-precheck.sh --business ID [--profile factory|library]` runs as `<user>`. It prints one
|
||||
`ok` or `REFUSED` line per check and exits 1 if any check refused. It prints variable names only,
|
||||
never values.
|
||||
|
||||
`packages/cli/README.md:713` now says nothing creates the session env file, only the Case 2 precheck
|
||||
reads it, and the precheck prints only variable names (webby-0034 N2, webby-0035).
|
||||
|
||||
## Deviations for review
|
||||
|
||||
Sage accepted all six for the build (sage-1570). Webby rules on each.
|
||||
|
||||
1. The precheck takes `--business ID`. The brief names no other way to find the business file.
|
||||
2. A run root `<dataRoot>/launches/<business>` that doesn't exist passes, with a note that the first
|
||||
launch creates it at 0700 (session.mjs:47 and newRun). A symlink refuses, and so does an existing
|
||||
entry that isn't a 0700 directory.
|
||||
3. The claude marker check passes when the reply holds the marker and input plus cache tokens are at
|
||||
most 2000. **The 2000 ceiling is unmeasured.** The tests use fakes at 40 and 9000 tokens. Measuring
|
||||
it on a real claude belongs to row 195's install run, not this row.
|
||||
4. A non-root test can't create a root-owned proof file. For the "proof not owned by the user" case,
|
||||
the fake getent reports another uid for the user, so the user's own file reads as someone else's.
|
||||
5. The unit script refuses `%h` left in any unit, rendered or installed. A system unit expands `%h`
|
||||
to root's home. The render replaces every `%h`, so the test edits an installed unit to reach it.
|
||||
6. With `--auth-header X-Authentik-Uid`, the allowed user must be 64 lower-case hex characters (lead
|
||||
decision 101). The test covers both the refusal and the pass.
|
||||
|
||||
## Tests (packages/cli/tests/case2-unit.test.mjs, 30 cases after round 2)
|
||||
|
||||
The tests use a scratch unit directory and a scratch home, plus fake systemctl, getent, sudo, id,
|
||||
node, bwrap and claude. They never touch /etc/systemd, ~/.mosaic-dev, ~/.config/mosaic-dev or a
|
||||
live unit. Every precheck case asserts its output doesn't hold the fixture key.
|
||||
|
||||
- Unit: the clean library render, PATH for a node outside the default list and for /usr/bin/node, the
|
||||
systemctl order, a failed bus start, no `systemctl link`, factory `--pm` and `--claude` rendered and
|
||||
absent, re-install to another copy, edits to an installed unit, template restrictions,
|
||||
EnvironmentFile placement, unknown tokens, PermissionsStartOnly and `+`/`-+` Exec prefixes,
|
||||
symlinked unit paths, bad `--node`, re-install drift, a split copy pair, bad copy ids, every library
|
||||
and factory flag refusal, and bad `--claude`.
|
||||
- Precheck: a good host, node older than 24, more than one group, sudo succeeding, XDG_DATA_HOME,
|
||||
HOME mismatch, a run root mode, a bad config, bwrap failing, the port floor at or below the Console
|
||||
port, a missing Console unit, bus.tester per profile, profile-library-business, the claude check
|
||||
(no route, replaces, appends, missing, the unit's MOSAIC_CLAUDE_BIN beating PATH), and the session
|
||||
env file (missing, 0644, symlink, empty, emptied by a later line, quoted).
|
||||
- Bind (mos-1100): inside `binds()` plus `sandboxArgv()`, a bind on port 980 gets EACCES, and a bind
|
||||
on a free port of 1024 or above succeeds. The test fails if the host's floor is at or below 980.
|
||||
|
||||
## Verification
|
||||
|
||||
Round 1 singles, run before the gate: case2-unit.test.mjs passes 28/28 with no skips, and host.test.mjs (it
|
||||
reads the cli README) passes 42/42. No test pins a hash of the README. No other test reads the two
|
||||
scripts.
|
||||
|
||||
## Round 2 (webby-0046, sage-1589, sage-1590, sage-1591)
|
||||
|
||||
Base: origin/refactor (rebased from 00d4cc1f6; the 15 commits between touch none of the five paths
|
||||
or the two unit templates). Branch gosalyn/c2-13a-r2.
|
||||
|
||||
B1. The precheck now has a case for each refusal Webby named: the user missing from passwd (:77),
|
||||
a business file that is unparsable, missing or fails the schema (:137), a run root that is a symlink
|
||||
to a 0700 directory (:151), a Console ExecStart with no --port (:179), an unreadable port floor
|
||||
(:180), a claude reply without the marker (:223) and a claude reply that isn't JSON (:224). The fake
|
||||
claude gained `nomarker` and `text` kinds. The no-claude-code case and the missing-claude case share
|
||||
a PATH without `/usr/bin/claude` (`pathWithoutClaude`), so the no-claude-code case now shows a host
|
||||
with no claude passes.
|
||||
|
||||
N1 (ruled in for this round). Before writing, `case2-unit.sh` asks `systemctl is-active` for both
|
||||
units. For each unit that was running, it prints that the unit is still on its old copy, never
|
||||
"started", and then that the bus and Console must restart together to reach the new copy. A unit
|
||||
that wasn't running gets "started ... on copy". A fresh install prints the round 1 line. The script
|
||||
still restarts nothing. The new test covers both units running and only the Console running.
|
||||
|
||||
N4. The README line at :713 is rewrapped.
|
||||
|
||||
Mutants, run in Sage's slot on a scratch worktree, one branch each: :77, :137, :151, :179, :180,
|
||||
:223, :224, a precheck that refuses with no claude on PATH (for the no-claude-code case), and an
|
||||
install that prints "started" for a running unit (sage-1591).
|
||||
|
||||
## Carry items for row 195
|
||||
|
||||
- N1's restart. Row 195 restarts the bus and Console together after a re-install over running
|
||||
units, as the install line now asks.
|
||||
- N2. A system drop-in check like `bus-service.sh:122-145`, so a drop-in under
|
||||
`[email protected]/` or `mosaic-webui.service.d/` can't change what the checked unit runs.
|
||||
- N3. The precheck finds claude on the shell PATH. The bus unit's `PATH=` is what a session uses
|
||||
when `MOSAIC_CLAUDE_BIN` is unset, so the check should search that PATH.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Herb
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for a second gate lane
|
||||
design row (#1695, mos-0977 item 6).
|
||||
|
||||
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 `herb/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,294 @@
|
||||
# Row 197 execution ledger
|
||||
|
||||
Plan: MANIFEST.md r2, approved by Webby ca6b32d66.
|
||||
Build grant: sage-1390. Suites, image builds and paired proof are not granted.
|
||||
Rebased five documentation commits onto origin/refactor aa1bd209e before code.
|
||||
Existing worktree herb/row197 and plain node_modules symlink retained.
|
||||
|
||||
- T1 preflight/steps: in progress.
|
||||
- T2 locks/status/recovery: pending.
|
||||
- T3 image/Docker: pending.
|
||||
- T4 fixtures/timing: pending.
|
||||
- T5 evidence/acceptance: historical timing done; implementation pending.
|
||||
|
||||
Preflight interfaces: T1 RunPaths supplies T2 ownership and T3 image checks.
|
||||
T2 run receipt supplies T4 lane/port state and T5 attribution. T3 image identity
|
||||
is checked before Docker-bearing steps. T4 timings supply T5 pair summaries.
|
||||
No interface conflict found. No product API or assertion changes are authorized.
|
||||
|
||||
Ruling: use this approved BUILD.md as the execution ledger, rather than adding
|
||||
skill workspace paths outside the manifest. This preserves the reviewed path
|
||||
boundary. Missing ledger detail would cost recovery time, not product behavior.
|
||||
|
||||
## Implementation checkpoint 2026-10-20T20:11:58Z
|
||||
|
||||
T1-T5 source is present within the approved manifest. Full acceptance is pending.
|
||||
Fresh single-file checks: preflight/supervisor 6/6; state/recovery 11/11; image and
|
||||
Docker guard 4/4; port leases/attribution/proof summary 3/3. No skips or failures.
|
||||
Existing files checked singly: board serve; WebUI serve, preview, proof-cli,
|
||||
tester-cli and service; harness pi and Claude. Stack-release R1 selected single
|
||||
passed 1/1. WebUI service also passed 31/31 with lane-a leases and a 30-byte root.
|
||||
All existing assert lines are unchanged. Shell syntax and diff checks pass.
|
||||
|
||||
Observed defects fixed during the single-file cycles: cancellation incorrectly
|
||||
returned zero when a TERM handler exited zero; recovery did not supersede an
|
||||
unclean terminal; status omitted the lock-before-receipt window. Evidence logs
|
||||
retain the failing and passing observations. Source still has no commit because
|
||||
applicable suites have not run. No image build, proof, push or live operation.
|
||||
|
||||
Ruling: use an instrumented single-lane author gate on a frozen dirty-source
|
||||
clone before committing source. The new wrapper requires a clean candidate and
|
||||
proven image receipt, while repository rules require green suites before source
|
||||
commits. The author gate retains every suite and assertion, caps concurrency at
|
||||
two, uses own port/state/temp/project roots, and guards Docker against builds.
|
||||
Cost if wrong: the author gate fails; clean-candidate wrapper baselines and the
|
||||
paired proof remain mandatory after source is verified and committed.
|
||||
|
||||
Ruling: publish the initial run directory atomically after the start receipt is
|
||||
complete. This avoids a second lane observing a partially written start. A
|
||||
crash before publication has started no test or mutable Docker operation.
|
||||
Cost if wrong: a lane refuses or quarantines; no protected state is bypassed.
|
||||
|
||||
## Implementation review corrections, 2026-10-20T20:33Z
|
||||
|
||||
Webby-0029 reviewed the first implementation packet at f942bd7d4.
|
||||
B1 identified missing manifest tests. B2 identified a test that leased both real
|
||||
bands through a private lock namespace. The old author-gate clear was withdrawn
|
||||
in herb-0019. SOURCE.json, BUILD.patch and gate.sh still describe the old frozen
|
||||
clone. They are not a clear for the edited working tree. No author gate ran.
|
||||
|
||||
B2 correction: the helper test uses only the active lane and MOSAIC_GATE_STATE.
|
||||
It compares the other band arithmetically and has no unasserted listener. The
|
||||
occupied-port probe first obtains its own real lease, binds that port, and
|
||||
asserts that its sentinel listener survives refusal. No test reserves another
|
||||
lane's band. Exhaustion narrows only the test process's band to one owned port.
|
||||
|
||||
B1 coverage added within the existing approved paths. No case is dropped:
|
||||
|
||||
| Manifest case | Added check |
|
||||
| --- | --- |
|
||||
| T1 canonical and builder checkout, existing output | gate-lane.test: direct validateRun refusals |
|
||||
| T1 92-byte socket and over-107-byte socket | exact design example with a distinct gt- test root; 108-byte refusal |
|
||||
| T1 both candidates carry tooling | validateTooling compares each launcher file, including missing and changed cases |
|
||||
| T1 duplicates and NODE_TEST_CONTEXT | repeated package/script fixture is retained; sanitized environment is exercised by both supervised children |
|
||||
| T2 one-winner race and inherited FDs | two actual shell wrappers race; a controlled supervisor verifies held FDs, and its child checks that no FD points to either lock file |
|
||||
| T2 same checkout and legacy claim refusal | activeConflicts uses synthetic process observations with a live lane and a sibling landing gate |
|
||||
| T2 surviving child and interrupted recovery | locked recovery kills a child proven by the private root, preserves a foreign child, is killed during fake Docker cleanup, leaves quarantine, and resumes under a new fixture grant |
|
||||
| T3 missing receipt and missing image | verifyImage refuses absent provenance and failing inspection |
|
||||
| T3 tag changed mid-gate and at gate end | runGate now uses the same executePinnedSteps boundary exercised by the tests; the next step never runs after a changed ID |
|
||||
| T3 mutable-tag rebuild | fake preparation returns a new image ID with identical input digest; the old receipt refuses it |
|
||||
| T3 cold and warm counters | preparation records one build and its time; warm execution records zero builds; repeated verification never builds |
|
||||
| T4 parallel test-file leases | two generated Node test files hold ports in the same lane/state; locks are checked during child execution and shutdown |
|
||||
| T4 exhausted band, occupied port and foreign listener | a one-port process-local band refuses both a held lease and an existing sentinel listener; the listener survives |
|
||||
| T4 owner death | a killed allocator releases only its own holder; a foreign lease remains held |
|
||||
| T4 lease through child shutdown | parent probes the lock while the fixture test runs and after its child has stopped |
|
||||
| T4 fixtureCacheEnv | replacement child environment writes only under separate tmp, jiti and cache directories |
|
||||
| T5 unrelated session append | a separate process waits for a running-step barrier, appends under a fixture HOME, and remains unrelated in the resulting metadata attribution |
|
||||
|
||||
N1 removes the unused closed binding. All leases reserve an unbound endpoint;
|
||||
existing closed options need no call-site change. N2 makes child TMPDIR a tmp
|
||||
subdirectory. N3 normalizes ss interface scopes before checking port 9. N4 uses
|
||||
gt- for unit run roots. N5 documents the canonical node_modules dependency.
|
||||
N6 is recorded: row 200 also edits stack-release.test.mjs; the second landing
|
||||
must rebase and rerun that file singly. Sage was told in herb-0019.
|
||||
|
||||
Before Sage's hold, expanded preflight tests passed 11/11, state tests 14/14,
|
||||
image tests 7/7, and the pi/Claude singles passed 8/8 each with separate tmp
|
||||
subdirectories. Later source/test edits still need verification. In particular,
|
||||
the new parallel test-file probe had a cleanup-order defect and is unverified.
|
||||
Its root cleanup ran before child shutdown. Cleanup now stops children before
|
||||
removing their root. Output is retained for a useful failure report. The initial
|
||||
FD probe also assumed Node did not reopen FD numbers; it now checks the actual
|
||||
lock targets. The early passing fixture file covered child allocators, before
|
||||
they were changed to actual generated test files. It is not acceptance of the
|
||||
latest file.
|
||||
|
||||
Sage-1448 stopped all node --test runs at 20:30Z for Rocko's browser slot.
|
||||
The remaining owned process tree was stopped at 20:30:32Z. Inspection found no
|
||||
remaining owned test processes afterward. Edits and static inspection continue.
|
||||
20 module syntax checks, shell syntax and git diff checks pass. Existing-file
|
||||
assertion lines remain identical to HEAD. No test is run during the hold.
|
||||
|
||||
Next: after Sage passes Rocko's clear, verify the corrected files singly,
|
||||
including a lane-env helper-file check. Then refresh the frozen clone and all
|
||||
three packet artifacts, send their hashes, and request a new author-gate clear.
|
||||
Suites, image builds and paired proofs still need their own grants. No source
|
||||
commit, push, suite, real Docker build or paired proof has run in this phase.
|
||||
|
||||
## Refrozen review packet, 2026-10-20T20:41Z
|
||||
|
||||
Sage-1454 lifted the single-file hold. Fresh runs passed without failures or
|
||||
skips: review-t1-resume.log 11/11, review-t2-resume.log 14/14,
|
||||
review-t3-resume.log 8/8, review-t4-resume.log 8/8. The helper file also passed
|
||||
8/8 with lane-a environment in review-t4-lane-a.log. Its retained state had only
|
||||
an a port-lock namespace, no b directory, and every lock was released. Generated
|
||||
fixture Node test entries explicitly use --test-concurrency=2. The child-test
|
||||
probe now finishes and cleans up before removing its root. All 41 new tests pass.
|
||||
|
||||
B1 and B2 fixes are ready for Webby's review. No manifest case was dropped.
|
||||
All 25 source paths were copied to a new frozen clone and compared by mode and
|
||||
SHA-256. This supersedes the earlier withdrawn source packet:
|
||||
|
||||
- SOURCE.json: 91ba149f2fc039a8bed0e6d1ad5da44b80fb8cbec84d0fa4e38616040138596b
|
||||
- gate.sh: 48e06e23e45d3484755f8cd79ad5b6dfde5ef4f960081e7c455a2540e707fed1
|
||||
- BUILD.patch: 0b3871bd9848ca223c7cda50266debc04643e5d986eb7f0fece096aaf25ff43e
|
||||
- Frozen checkout: /mnt/storage/scratch/mg197-a-d22dacd92735/wt
|
||||
- Output: /mnt/storage/scratch/mg197-a-d22dacd92735/out
|
||||
- Temporary root: /mnt/storage/scratch/ga-VPXhG7 (30 bytes)
|
||||
- Compose project: mg197-a-d22dacd92735
|
||||
|
||||
After an explicit author-gate grant, run this command with Sage's actual ID:
|
||||
|
||||
```sh
|
||||
env MOSAIC_GATE_GRANT=ACTUAL_SAGE_GRANT_ID bash /home/jwoltje/.t3/worktrees/mosaic-stack/herb-row197/agents/herb/work/queue-197/gate.sh > /mnt/storage/scratch/mg197-a-d22dacd92735/out/gate.log 2>&1
|
||||
```
|
||||
|
||||
No gate or suite has started. Queue order from sage-1454 is 192, 201, 204,
|
||||
200, Gander, then 197. No branch deletion is authorized or attempted. Any later
|
||||
rebase uses a new branch name, and pr-merge.sh -d stays off under #426(a).
|
||||
Whichever of rows 197 and 200 lands second must rerun stack-release singly.
|
||||
Clean-candidate wrapper baselines, image preparation and paired proofs remain
|
||||
separately gated acceptance work. There is still no source commit or push.
|
||||
|
||||
## Implementation approval and final notes, 2026-10-20T20:47Z
|
||||
|
||||
Webby-0031 approved r2 in REVIEW-impl-r2.md at 1f404c973. Their static review
|
||||
verified every source hash/mode and patch result, and all B1/B2 cases. No queue
|
||||
review request comment exists yet; Sage owns creating it if needed.
|
||||
|
||||
Folded all three optional notes into the same approved paths. The helper file's
|
||||
standalone fallback now shares /mnt/storage/scratch/mosaic-gate-lanes with real
|
||||
lane-a leases and leaves its stable lock files in place. Band assertions use the
|
||||
imported BANDS. Both T5 polling children fail after five seconds if their barrier
|
||||
is missed. README records the shared standalone namespace and deadlines.
|
||||
|
||||
Fresh single-file logs review-r3-fixtures.log and review-r3-lane-a.log both pass
|
||||
8/8 with zero skips. The lane-a run has no b lock directory, and every lock was
|
||||
released. Syntax and diff checks pass. No existing test assertions or timeouts
|
||||
changed. Remaining source matches the 11/14/8-file checks from r2.
|
||||
|
||||
Refreshed all 25 source rows in the existing frozen checkout. New digests replace
|
||||
the preceding r2 packet. gate.sh and its command are unchanged:
|
||||
|
||||
- SOURCE.json: 3584763642506e81a532c839bdd07b111d25daa87e35dbe441d8e8c01a2c2aab
|
||||
- gate.sh: 48e06e23e45d3484755f8cd79ad5b6dfde5ef4f960081e7c455a2540e707fed1
|
||||
- BUILD.patch: 55fe390da17ab926a0593d39e79561abe52756a1b1a2c756ec57f45d90f10fde
|
||||
|
||||
Author gate remains unstarted and queued for Sage's explicit grant. No source
|
||||
commit, suite, image build, paired proof, push or branch deletion ran.
|
||||
|
||||
## R3 approval receipt
|
||||
|
||||
Webby-0033 approved r3 in REVIEW-impl-r3.md at a5643f9bd. Static review verified
|
||||
the three packet hashes, all 25 source rows, and the patch result. Their review
|
||||
accepts N1-N3; no additional hang-guard test is requested. No queue review request
|
||||
comment exists yet, so Webby has not recorded a queue review.
|
||||
|
||||
Read-only verification after receipt confirms that the approved packet hashes
|
||||
and all 25 worktree/frozen source hashes and modes still match. Source and gate
|
||||
script stay frozen. The author gate has not started and still needs Sage's grant.
|
||||
|
||||
## Queue review round 1
|
||||
|
||||
Sage-1476 authorized moving row 197 to in-review before the author gate.
|
||||
Created CANDIDATE.sha256 from the same 25 reviewed SOURCE.json rows. Its digest
|
||||
is 6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3.
|
||||
Ran the queue move from the canonical checkout with op herb-197-review-r3.
|
||||
The move succeeded at revision 2395, opening round 1 on #1695. The request post
|
||||
failed before send because MOSAIC_GITEA_CREDENTIAL_FILE is not set; revision 2396
|
||||
records the failed outcome. Exit code 1 describes the failed post, not a failed
|
||||
row transition. Read-back confirms state in-review and the correct candidate.
|
||||
Sage will post as jarvis and give Webby the request comment number. No credential
|
||||
lookup, alternate post or retry was attempted here.
|
||||
|
||||
SOURCE.json, BUILD.patch, gate.sh and all 25 frozen source rows remain unchanged.
|
||||
Latest slot order is row 190 r2, 201, 204, 200, Gander, then Herb. No gate starts
|
||||
before Sage sends an explicit grant ID.
|
||||
|
||||
## Follow-on assignment
|
||||
|
||||
Sage-1503 assigns row 215, Case 2 C2-04, to Herb after row 197 (queue rev 2455,
|
||||
Gosalyn design r11). Read docs/plans/2026-10-20_case2-rows.md, C2-04 section,
|
||||
at 5b8def2f1 when starting that row. Its required base is origin/refactor
|
||||
305857c8f. This assignment does not replace row 197 or authorize its gate.
|
||||
Keep row 197's approved source frozen while awaiting Sage's grant. Start row
|
||||
215 only after row 197, using a new branch name under the existing fleet rule.
|
||||
|
||||
## Round 2 merge preparation on row 233
|
||||
|
||||
Sage-1757 replaces history replay with one candidate commit on 57f59e0c3.
|
||||
The attempted history replay on herb/row197-r2-233 had already finished when
|
||||
that instruction arrived. Its only conflict was the append-only BUILD-LOG tail.
|
||||
That branch remains for history; none of its replayed commits is in this round.
|
||||
The current branch is herb/row197-r3-233, directly from
|
||||
57f59e0c399ce29b1bb85ca6af686fdc10eb195d.
|
||||
|
||||
Stash correction: shared refs/stash was used incorrectly. The exact entry
|
||||
2767b88549dfd8c7e810908b5416876fa50cb571 is now kept on
|
||||
herb/row197-r3-wip. After verifying stash@{0} still named it, only that entry was
|
||||
dropped. The two other entries remain. No further stash creation, force push,
|
||||
branch deletion or refs/stash mutation is planned. A separate working-copy
|
||||
backup is /mnt/storage/scratch/herb197-merge-W4oiLg.
|
||||
|
||||
Sage-1760 requires preserving every landed hunk on every changed manifest path.
|
||||
Checked each of the 25 r1 paths with git diff aa1bd209e 57f59e0c3 -- PATH.
|
||||
Exactly three changed upstream. Their r2 diff against 57f59e0c3 is captured in
|
||||
r2-merge-only.patch:
|
||||
|
||||
| Merged path | Only row 197 changes against landed base | Landed content retained |
|
||||
| --- | --- | --- |
|
||||
| packages/cli/tests/stack-release.test.mjs | leasePort and laneAfter imports; freePort now calls the lease helper | All landed tests, including row 233's tests after R15 and BACKUP_STORE checks |
|
||||
| packages/webui/tests/service.test.mjs | leasePort and laneAfter imports; closedPort now calls the lease helper | SEND_REFUSAL permits --sso; default unit rendering removes the @CONFIG_ENV@ line |
|
||||
| scripts/test-discord.sh | fixture PI_CODING_AGENT_DIR setup; optional two-worker nested Node cap | Row 190's absent scratch MOSAIC_CONFIG selection and caller-config restoration |
|
||||
|
||||
The other 22 paths equal the r1 reviewed source bytes and modes. r2-MERGE.json
|
||||
lists every path, its r1/r2 digest, mode, upstream-change flag and equality result.
|
||||
Source copies came from the hash-verified r1 frozen checkout. Original r1 digest:
|
||||
6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3.
|
||||
No existing assertion or timeout line differs from the landed base. Static
|
||||
checks only: 20 module syntax checks, four shell syntax checks and git diff check
|
||||
pass. BUILD-LOG.md equals 57f59e0c3 byte for byte and is outside this commit.
|
||||
|
||||
Sage explicitly requested the local candidate SHA before granting tests. This
|
||||
commit is an untested preparation checkpoint, not gate evidence. No tests or
|
||||
push are authorized yet. r2-TEST-FILES.txt lists the requested grant scope:
|
||||
16 Node test files singly and scripts/test-discord.sh, test-foundation.sh and
|
||||
test-queue.sh. The four stack-release consumers are stack-release.test.mjs,
|
||||
stack-release-external.test.mjs, stack-upgrade.test.mjs and stack-instances.test.mjs.
|
||||
The shared stack-release-fixture.mjs is exercised by its importing test files,
|
||||
not run as a standalone test. This covers all 13 touched Node test files plus
|
||||
three additional stack-release consumers and all three touched shell suites.
|
||||
|
||||
Current slot order from sage-1760: Gizmoduck, Launchpad, Darkwing, then Herb,
|
||||
around 06:30Z. Wait for an explicit grant. After granted checks and authorized
|
||||
push, build the new candidate manifest from that pushed tip and open the
|
||||
merge-scoped round 2. Old SOURCE.json, CANDIDATE.sha256 and gate.sh remain r1
|
||||
historical artifacts and are not a clear to run the merged candidate.
|
||||
|
||||
## Round 2 rebuilt on row 234
|
||||
|
||||
Sage-1776 names landing c771a36f4bd0b6c359282425c668996a90072f93.
|
||||
Built a new branch, herb/row197-r4-234, directly from that landing. No history
|
||||
replay, stash, force push, branch deletion, test run or push is involved.
|
||||
The prior candidate a3b7cbde8 remains untested and is not the queued candidate.
|
||||
|
||||
Checked git diff --name-only 57f59e0c3 c771a36f4 against all 25 source paths.
|
||||
Only packages/cli/tests/stack-release.test.mjs overlaps; the other 24 do not.
|
||||
The full comparison is in r2-on234-OVERLAP.txt. The stack-release file starts
|
||||
from the landing and keeps both T46 untimed entries, host_grants and hosts.
|
||||
Its only changes against the landing are the leasePort and laneAfter imports
|
||||
and freePort's call to the lease helper. r2-on234-merge-only.patch records those
|
||||
hunks. Existing assertions and timeouts match the landing exactly.
|
||||
|
||||
Every other source path equals a3b7cbde8 byte for byte and retains its mode.
|
||||
This includes the previously merged service and test-discord files, preserving
|
||||
all landed --sso, CONFIG_ENV and scratch-config handling. The earlier evidence
|
||||
r2-MERGE.json still records those three pre-234 merge cases. The new
|
||||
r2-on234-MERGE.json records each current digest, prior digest, mode and equality
|
||||
result. BUILD-LOG.md equals the new landing byte for byte and is excluded.
|
||||
|
||||
Static check of the changed Node file and the candidate diff pass. No tests
|
||||
were run. The scope stays r2-TEST-FILES.txt: 16 Node files singly plus the three
|
||||
shell suites. No manifest or queue round is rebuilt until granted checks and
|
||||
authorized push. Old SOURCE.json, CANDIDATE.sha256 and gate.sh remain historical
|
||||
r1 evidence. The test slot follows Darkwing 240 and needs Sage's explicit grant.
|
||||
@@ -0,0 +1,241 @@
|
||||
# Two-lane proof command and schedule
|
||||
|
||||
This evidence stays outside r5's 32 manifest paths. No proof or image build has
|
||||
run. The driver and example config are committed on herb/row197-proof, rooted
|
||||
at 9aa6d52d9. Sage runs the committed copy from their own checkout.
|
||||
|
||||
Driver: `agents/herb/work/queue-197/proof/run-proof.mjs`.
|
||||
The commit message to Sage carries its exact SHA-256. Check it with
|
||||
`git show <proof-commit>:agents/herb/work/queue-197/proof/run-proof.mjs | sha256sum`.
|
||||
|
||||
After Sage fills config.json from config.example.json and grants the whole block:
|
||||
|
||||
```sh
|
||||
MOSAIC_PROOF_GRANT=ACTUAL_SAGE_GRANT_ID node /SAGE_PROOF_CHECKOUT/agents/herb/work/queue-197/proof/run-proof.mjs /SAGE_OWNED_CONFIG/config.json
|
||||
```
|
||||
|
||||
The example config pins the launcher and candidate A to 197 r5:
|
||||
9aa6d52d9431efd215aa31d748d7e273eb321474. The launcher is a separate clean
|
||||
checkout owned by Sage, so later row 215 work cannot change it. Fill candidateB with
|
||||
Sage's approved combined 212+197 commit, imageReceipt with the granted pinned
|
||||
image-provenance receipt, and runRoot with a fresh private scratch path. Both
|
||||
candidates must carry identical lane tooling and image inputs and use the same
|
||||
pinned image ID. The driver refuses missing parameters, equal candidate SHAs,
|
||||
dirty launcher, changed tooling, mismatched image inputs or image ID.
|
||||
|
||||
D5 order is exactly two solo gates, then three pairs:
|
||||
|
||||
| Wave | Candidate A | Candidate B |
|
||||
| --- | --- | --- |
|
||||
| 1 | solo, lane a | idle |
|
||||
| 2 | idle | solo, lane b |
|
||||
| 3 | paired, lane a | paired, lane b |
|
||||
| 4 | paired, lane b | paired, lane a |
|
||||
| 5 | paired, lane a | paired, lane b |
|
||||
|
||||
Every invocation gets its own clean clone, output, short temp root and random
|
||||
Compose project. The wrapper discovers all package and shell steps, with two
|
||||
Node test-file workers per lane. The summary requires three overlaps, the lane
|
||||
swap, successful clean receipts and zero warm image builds. Raw logs and per-step
|
||||
receipts remain available. No failed gate or series is retried automatically.
|
||||
|
||||
A live monitor stops both gates on the first failure line, records lane, step,
|
||||
source file when the runner emits it, test name and preserved log, then enriches
|
||||
that report after shutdown. If the runner gives no source path, the report says
|
||||
unresolved and points to the log rather than guessing. It stops earlier than the
|
||||
two-failure limit. TERM/INT/HUP stop owned supervisors; cleanup waits for them.
|
||||
|
||||
Container and network names come from Compose's random recorded project. The
|
||||
project and service configuration have no fixed global names. Container event
|
||||
streams filter the exact project label; network streams match the exact project_
|
||||
name prefix. Both record create/destroy IDs and names, including --rm containers.
|
||||
An exit trap inventories names/labels, removes only exact-project objects with
|
||||
the approved cleanup helper, verifies emptiness and stops its own event streams.
|
||||
The driver's Docker inspection never reads Config.Env or credential contents, and never builds, pulls,
|
||||
retags or prunes. Missing resource-name evidence, an ended event stream, unknown
|
||||
ownership, leftover objects or quarantined lanes fail the proof. A SIGKILL cannot
|
||||
run an exit trap; durable wrapper receipts then require granted recovery.
|
||||
|
||||
Run this as its own whole-slot phase. A still-running legacy landing.mjs gate or
|
||||
gate.sh is LEGACY BUSY, including a parent process. The driver does not suppress
|
||||
that refusal. The ordinary landing gate must exit before this proof phase starts.
|
||||
|
||||
Planning runtime from sage-1837 is 12-15 minutes per wave, so reserve 60-75 minutes
|
||||
for all five waves, plus any separately granted image preparation. This is an
|
||||
estimate, not a measurement. Measured proof runtime is unavailable until this
|
||||
first run. Historical row 189 full gate was 659 seconds; the r4 focused 19-file
|
||||
run was 761.33 seconds and is not a full solo baseline. The resulting summary
|
||||
records actual per-gate times, pair makespans, slowdowns and total wall time.
|
||||
|
||||
No image provenance receipt or combined integration SHA is available to Herb yet.
|
||||
The driver remains unexecuted. Sage will read the exact hashed driver and request
|
||||
Webby's static review before the landing/proof grant.
|
||||
|
||||
Static node --check passed after sage-1839 single cleared. This is syntax-only;
|
||||
the driver has not run a proof or Docker command. Its runtime behavior awaits
|
||||
Sage and Webby review and the separately granted proof block.
|
||||
|
||||
## Driver review fixes, sage-1857
|
||||
|
||||
Webby's first review was CHANGES, B1, at bba870ee9. The revised driver takes
|
||||
tempRoot only from immutable start records for the plan's checkouts. After
|
||||
every owned gate exits, Docker cleanup and the final status/process checks
|
||||
run. The driver requires the exact short ga-/gb- scratch spelling, a matching
|
||||
lane, an owned real directory and no live owner, child group or process with
|
||||
the run paths in its argv before removal. It also requires a clean terminal
|
||||
receipt. A successful gate's root is removed recursively. A failed gate's
|
||||
root stays for diagnosis; an unverified cleanup also stays and fails the
|
||||
proof. docker-summary.json records removed, kept and error entries. Any
|
||||
cleanup error makes the driver fail. No unrelated scratch directory is read.
|
||||
|
||||
Signal handlers are installed before configuration and preparation. Clone
|
||||
preparation is inside the try/finally, and yields between synchronous clones
|
||||
so a pending signal stops it before another invocation. A preparation signal
|
||||
writes failure evidence and reaches the cleanup path. Partial clones remain
|
||||
under runRoot as permitted by Sage; they are never confused with lane roots.
|
||||
SIGKILL cannot be trapped and still needs granted recovery.
|
||||
|
||||
At 2026-10-21T08:26:41Z, Herb removed four earlier roots recorded in their
|
||||
own packet: ga-VPXhG7, gb-XeTAUS, ga-35Zo9Y and ga-R5IOT3. Each was an owned
|
||||
real directory with no process argv/cwd reference before removal. Names
|
||||
ga-AMtfI3 and ga-Vd9Ef2 were visible in the scratch directory listing but had
|
||||
no ownership evidence in Herb's packet; neither was opened or changed.
|
||||
|
||||
## N4 read-only trace, pre-existing and out of scope
|
||||
|
||||
Sage ruled in sage-1859 that N4 does not block 197. Legacy suites already
|
||||
reach Compose's read-only host auth mount, and the lane adds no exposure.
|
||||
Sage owns a DEFERRED entry for fixture PI_AUTH_FILE coverage. No fix is made
|
||||
in 197, and this procedure does not claim that the full gate is secret-free.
|
||||
|
||||
At 9aa6d52d9:
|
||||
|
||||
- gate-lane.mjs:109-111 keeps HOME real but neither keeps nor sets
|
||||
PI_AUTH_FILE. compose.yaml:43 therefore selects its host auth-file default.
|
||||
- script-task is discovered by gate-lane.mjs:36. test-task.sh has no
|
||||
PI_AUTH_FILE assignment. Calls at :149-150 (mock), :444, :460, :480, :483
|
||||
and :520 (live) reach run-task. mosaic-task.mjs:321 copies the environment
|
||||
and :395-400 invokes compose run mosaic-agent without changing that path.
|
||||
- script-release calls release activate at test-release.sh:69,85,93.
|
||||
release.sh:41-54 health_check calls run-task with no PI_AUTH_FILE override,
|
||||
then reaches the same Compose call. These tests use scratch MOSAIC_CONFIG;
|
||||
that does not override the separate auth-file bind mount.
|
||||
- test-auth.sh:48-92 sets explicit fixtures, but calls auth.sh instead of
|
||||
Compose. Package runs/task-cli.test.mjs exercises list/show, not run.
|
||||
|
||||
This was a static source trace only. No auth file was opened, no credential
|
||||
value was read, and no Docker command, image build or proof was executed.
|
||||
|
||||
|
||||
## Proof4 watcher change (sage-2029, mos-1174)
|
||||
|
||||
Proof3's eight gates passed and cleaned up, but Docker29 network events do
|
||||
not carry Compose project labels, so the driver's combined label filter
|
||||
missed their creation. Only this proof packet changes; none of the37
|
||||
c45ddeb6b candidate paths changes.
|
||||
|
||||
Container events retain the label filter. Network events use a type filter
|
||||
and a local exact project_ name-prefix match, accepting default and other
|
||||
owned network names. The existing recorded-name project guard stays.
|
||||
The same exported watcher/parser powers unit tests, the proof and a daemon
|
||||
preflight before wave1. Importing the module does not execute a proof.
|
||||
Direct execution still installs signal handlers before preparation.
|
||||
|
||||
The preflight creates one named and labelled mg197 probe network and one
|
||||
create-only container from the pinned imageId, --pull never, network none,
|
||||
no mounts or start. Exact-project label-checked cleanup removes both,
|
||||
including on failure. All four create/destroy observations must arrive
|
||||
within5000ms. Docker commands share that deadline; cleanup has a separate
|
||||
3000ms cap. Watcher shutdown is bounded. Failure prevents all gate waves.
|
||||
Probe metadata and outcomes go into docker-summary.json and summary.json.
|
||||
No Config.Env, credential file contents or raw foreign events are saved.
|
||||
|
||||
Tests cover network events with name/type only, all project_ network names,
|
||||
foreign names/labels, recorded-name ownership, fragmented streams, early
|
||||
exit, missing create/destroy observations, deadline and cleanup failures.
|
||||
Sage-2034 accepted this scope. Grant sage-2035 supplied the runtime evidence below.
|
||||
|
||||
## Granted verification, sage-2035
|
||||
|
||||
On 2026-10-21, the whole events.test.mjs passed 8/0, with zero skipped or
|
||||
cancelled. node --check passed on run-proof.mjs and events.test.mjs. One
|
||||
exported probeDockerWatchers call used the supplied proof3 image ID and
|
||||
cleanupProject from the unchanged r8 launcher. No container was started;
|
||||
no pull, build, lane or wave ran. Both filtered post-probe Docker listings
|
||||
were empty, and the owned /proc cwd/project scan was empty. The slot was
|
||||
returned at 2026-10-21T15:16:52Z.
|
||||
|
||||
Driver SHA-256:
|
||||
`998f41948355ea86169c1711324a3c9e6cf4bdbe5157dcae9e78e414c1c16342`.
|
||||
Sage updates their proof-phase.sh digest pin to this value.
|
||||
|
||||
The complete standalone probe output was:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"project": "mg197-a-724f1800af2c",
|
||||
"imageId": "sha256:1b83521d092a3f005ea2192e9e6268099e6e0cdab85d41a5d107b4fb824131a6",
|
||||
"timeoutMs": 5000,
|
||||
"names": {
|
||||
"container": "mg197-a-724f1800af2c-watcher-probe",
|
||||
"network": "mg197-a-724f1800af2c_default"
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"project": "mg197-a-724f1800af2c",
|
||||
"kind": "network",
|
||||
"id": "930b1c80596fc93b4dc104ab1e278e5b1e1b2bc6373a5cb9cba7bba46f230ef8",
|
||||
"name": "mg197-a-724f1800af2c_default",
|
||||
"action": "create",
|
||||
"observedMs": 38.03536499999999
|
||||
},
|
||||
{
|
||||
"project": "mg197-a-724f1800af2c",
|
||||
"kind": "container",
|
||||
"id": "8fcdf9d06c437e162698095b2234402ce75fd9744669ced4017f823b5a99fc83",
|
||||
"name": "mg197-a-724f1800af2c-watcher-probe",
|
||||
"action": "create",
|
||||
"observedMs": 123.93732700000001
|
||||
},
|
||||
{
|
||||
"project": "mg197-a-724f1800af2c",
|
||||
"kind": "container",
|
||||
"id": "8fcdf9d06c437e162698095b2234402ce75fd9744669ced4017f823b5a99fc83",
|
||||
"name": "mg197-a-724f1800af2c-watcher-probe",
|
||||
"action": "destroy",
|
||||
"observedMs": 297.033782
|
||||
},
|
||||
{
|
||||
"project": "mg197-a-724f1800af2c",
|
||||
"kind": "network",
|
||||
"id": "930b1c80596fc93b4dc104ab1e278e5b1e1b2bc6373a5cb9cba7bba46f230ef8",
|
||||
"name": "mg197-a-724f1800af2c_default",
|
||||
"action": "destroy",
|
||||
"observedMs": 297.07703999999995
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"cleanup": {
|
||||
"clean": true,
|
||||
"removed": [
|
||||
{
|
||||
"kind": "container",
|
||||
"id": "8fcdf9d06c43"
|
||||
},
|
||||
{
|
||||
"kind": "network",
|
||||
"id": "930b1c80596f"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ok": true,
|
||||
"missing": [],
|
||||
"wallMs": 308.567205
|
||||
},
|
||||
"inventories": {
|
||||
"containers": "",
|
||||
"networks": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"repo": "/mnt/storage/src/mosaic-stack",
|
||||
"launcherRoot": "SAGE_OWNED_CLEAN_LAUNCHER_CHECKOUT",
|
||||
"launcherCandidate": "9aa6d52d9431efd215aa31d748d7e273eb321474",
|
||||
"candidateA": "9aa6d52d9431efd215aa31d748d7e273eb321474",
|
||||
"candidateB": "SAGE_APPROVED_212_PLUS_197_COMMIT",
|
||||
"candidateDifference": "A is approved 197 r5; B must be the approved combined 212+197 integration commit",
|
||||
"imageReceipt": "SAGE_PINNED_IMAGE_PROVENANCE_RECEIPT",
|
||||
"runRoot": "/mnt/storage/scratch/mg197-proof-REPLACE_WITH_UNIQUE_NONCE"
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import { dockerEvent, watchDockerProject, probeDockerWatchers } from './run-proof.mjs';
|
||||
const project='mg197-a-0123456789ab', imageId='sha256:'+'a'.repeat(64);
|
||||
const ids={container:'b'.repeat(64),network:'c'.repeat(64)};
|
||||
const event=(kind,action,name=kind==='network'?project+'_default':project+'-probe')=>({Type:kind,Action:action,Actor:{ID:ids[kind],Attributes:{name,type:'bridge',...(kind==='container'?{'com.docker.compose.project':project}:{})}}});
|
||||
function streams(){
|
||||
const children=new Map(),calls=[];
|
||||
const spawnImpl=(binary,args)=>{const kind=args.includes('type=network')?'network':'container';calls.push(args);const child=new EventEmitter();child.stdout=new PassThrough();child.stderr=new PassThrough();child.kill=()=>{queueMicrotask(()=>child.emit('close',0));return true;};children.set(kind,child);return child;};
|
||||
const emit=(kind,action,name)=>children.get(kind).stdout.write(JSON.stringify(event(kind,action,name))+'\n');
|
||||
return {children,calls,spawnImpl,emit};
|
||||
}
|
||||
test('network create/destroy with only name and type match every exact project_ network',()=>{
|
||||
for(const name of [project+'_default',project+'_other_network'])for(const action of ['create','destroy']){
|
||||
const value=dockerEvent(project,'network',event('network',action,name));
|
||||
assert.deepEqual(value,{project,kind:'network',id:ids.network,name,action});
|
||||
}
|
||||
for(const name of ['foreign_default',project+'x_default',project+'-other',project])assert.equal(dockerEvent(project,'network',event('network','create',name)),null);
|
||||
assert.equal(dockerEvent(project,'network',event('network','connect')),null);
|
||||
});
|
||||
test('container labels and recorded name ownership remain strict',()=>{
|
||||
assert.equal(dockerEvent(project,'container',event('container','create')).id,ids.container);
|
||||
const foreign=event('container','create');foreign.Actor.Attributes['com.docker.compose.project']='other';assert.equal(dockerEvent(project,'container',foreign),null);
|
||||
assert.throws(()=>dockerEvent(project,'container',event('container','create','foreign-container')),/name outside/);
|
||||
const invalid=event('network','create');invalid.Actor.ID='not-an-id';assert.throws(()=>dockerEvent(project,'network',invalid),/ownership metadata/);
|
||||
});
|
||||
test('two real watcher shapes decode fragmented lines and network stream has no label filter',async()=>{
|
||||
const s=streams(),seen=[],errors=[];const watcher=watchDockerProject(project,{docker:'/fixture/docker',spawnImpl:s.spawnImpl,onEvent:e=>seen.push(e),onError:e=>errors.push(e)});
|
||||
const text=JSON.stringify(event('network','create'))+'\n';s.children.get('network').stdout.write(text.slice(0,17));s.children.get('network').stdout.write(text.slice(17));s.emit('network','destroy');s.emit('container','create');s.emit('container','destroy');
|
||||
assert.equal(seen.length,4);assert.ok(s.calls.find(a=>a.includes('type=container')).includes('label=com.docker.compose.project='+project));assert.ok(!s.calls.find(a=>a.includes('type=network')).some(a=>a.startsWith('label=')));
|
||||
await watcher.stop();assert.deepEqual(errors,[]);
|
||||
});
|
||||
test('an event watcher ending early reports an error',async()=>{
|
||||
const s=streams(),errors=[];const watcher=watchDockerProject(project,{docker:'/fixture/docker',spawnImpl:s.spawnImpl,onEvent:()=>{},onError:e=>errors.push(e)});s.children.get('network').emit('close',1);assert.match(errors[0],/ended early/);await watcher.stop();
|
||||
});
|
||||
function probeFixture({missing=null,commandFails=false,cleanupFails=false}={}){
|
||||
const s=streams(),calls=[];let cleaned=false;
|
||||
const invoke=async args=>{calls.push(args);const kind=args[0];if(commandFails)throw Error('fixture daemon error');if(missing!==kind+'.create')s.emit(kind,'create');return ids[kind]+'\n';};
|
||||
const cleanupProject=run=>{assert.equal(run.project,project);cleaned=true;if(cleanupFails)throw Error('fixture cleanup refusal');for(const kind of ['container','network'])if(missing!==kind+'.destroy')s.emit(kind,'destroy');return {clean:true,removed:Object.entries(ids).map(([kind,id])=>({kind,id}))};};
|
||||
return {options:{docker:'/fixture/docker',imageId,project,spawnImpl:s.spawnImpl,invoke,cleanupProject,timeoutMs:80},calls,get cleaned(){return cleaned;}};
|
||||
}
|
||||
test('preflight uses the same watchers, creates only pinned no-pull objects, removes both and sees all four events',async()=>{
|
||||
const f=probeFixture(),result=await probeDockerWatchers(f.options);assert.equal(result.ok,true);assert.equal(f.cleaned,true);assert.deepEqual(result.missing,[]);assert.equal(result.events.length,4);
|
||||
const create=f.calls.find(a=>a[0]==='container');assert.equal(create[1],'create');assert.equal(create[create.indexOf('--pull')+1],'never');assert.ok(create.includes(imageId));assert.ok(create.includes('--network')&&create.includes('none'));assert.ok(f.calls.every(a=>!a.includes('start')&&!a.includes('build')));
|
||||
});
|
||||
test('missing either create or destroy fails within the bounded wait and still cleans up',async()=>{
|
||||
for(const missing of ['container.create','container.destroy','network.create','network.destroy']){
|
||||
const f=probeFixture({missing}),start=performance.now(),result=await probeDockerWatchers(f.options);assert.equal(result.ok,false);assert.ok(result.missing.includes(missing));assert.ok(performance.now()-start<1500);assert.equal(f.cleaned,true);
|
||||
}
|
||||
});
|
||||
test('probe command and cleanup failures refuse and retain their own result',async()=>{
|
||||
for(const options of [{commandFails:true},{cleanupFails:true}]){const f=probeFixture(options),result=await probeDockerWatchers(f.options);assert.equal(result.ok,false);assert.ok(result.errors.length);assert.equal(f.cleaned,true);}
|
||||
});
|
||||
|
||||
test('late observations cannot turn a missed deadline into success',async()=>{
|
||||
const f=probeFixture(),invoke=f.options.invoke;f.options.invoke=async args=>{await new Promise(r=>setTimeout(r,100));return invoke(args);};
|
||||
const result=await probeDockerWatchers(f.options);assert.equal(result.ok,false);assert.ok(result.missing.includes('network.create'));assert.equal(f.cleaned,true);
|
||||
});
|
||||
@@ -0,0 +1,264 @@
|
||||
// Evidence driver only. Runs no test without Sage's whole-slot grant.
|
||||
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, realpathSync, openSync, closeSync, symlinkSync, readdirSync, statSync, lstatSync, readSync, rmSync } from 'node:fs';
|
||||
import { resolve, join, dirname, relative, isAbsolute } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { execFileSync, execFile, spawn } from 'node:child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
const projectPattern=/^mg197-[ab]-[a-f0-9]{12}$/;
|
||||
export function dockerEvent(project,kind,event){
|
||||
if(!projectPattern.test(project)||!['container','network'].includes(kind))throw Error('invalid Docker ownership metadata');
|
||||
if(event.Type!==kind||!['create','destroy'].includes(event.Action))return null;
|
||||
const attrs=event.Actor?.Attributes??{},name=typeof attrs.name==='string'?attrs.name.replace(/^\//,''):null;
|
||||
if(kind==='network'){
|
||||
if(!name?.startsWith(project+'_'))return null;
|
||||
if(attrs['com.docker.compose.project']&&attrs['com.docker.compose.project']!==project)throw Error('foreign project event');
|
||||
}else if(attrs['com.docker.compose.project']!==project)return null;
|
||||
if(!name?.startsWith(project))throw Error('Docker name outside its recorded project');
|
||||
const id=event.Actor?.ID;if(typeof id!=='string'||!/^[a-f0-9]{12,64}$/.test(id))throw Error('invalid Docker ownership metadata');
|
||||
return {project,kind,id,name,action:event.Action};
|
||||
}
|
||||
export function watchDockerProject(project,{docker,onEvent,onError,spawnImpl=spawn}){
|
||||
if(!projectPattern.test(project))throw Error('invalid Docker ownership project');
|
||||
const streams=[];
|
||||
for(const kind of ['container','network']){
|
||||
// Network events on Docker29 have name/type but no Compose labels. Filter
|
||||
// their exact project_ name prefix locally, never by a missing label.
|
||||
const args=['events','--since',String(Math.floor(Date.now()/1000)-5),'--filter','type='+kind];
|
||||
if(kind==='container')args.push('--filter','label=com.docker.compose.project='+project);
|
||||
args.push('--filter','event=create','--filter','event=destroy','--format','{{json .}}');
|
||||
const child=spawnImpl(docker,args,{stdio:['ignore','pipe','pipe']}),stream={child,stopping:false,buffer:'',closed:false};streams.push(stream);
|
||||
child.stdout.on('data',bytes=>{stream.buffer+=bytes;let end;while((end=stream.buffer.indexOf('\n'))>=0){const line=stream.buffer.slice(0,end);stream.buffer=stream.buffer.slice(end+1);if(!line)continue;try{const event=dockerEvent(project,kind,JSON.parse(line));if(event)onEvent(event);}catch(error){onError(error.message);}}});
|
||||
child.stderr.resume();stream.done=new Promise(resolve=>{child.once('error',error=>{stream.closed=true;onError(error.message);resolve();});child.once('close',code=>{stream.closed=true;if(!stream.stopping)onError('Docker '+kind+' event stream ended early: '+code);resolve();});});
|
||||
}
|
||||
return {async stop(){
|
||||
for(const stream of streams){stream.stopping=true;if(!stream.closed)stream.child.kill('SIGTERM');}
|
||||
let timer;await Promise.race([Promise.all(streams.map(s=>s.done)),new Promise(resolve=>{timer=setTimeout(resolve,1000);})]);clearTimeout(timer);
|
||||
for(const stream of streams)if(!stream.closed){stream.child.kill('SIGKILL');onError('Docker event watcher did not stop promptly');}
|
||||
}};
|
||||
}
|
||||
export async function probeDockerWatchers({docker,imageId,cleanupProject,project='mg197-a-'+randomBytes(6).toString('hex'),timeoutMs=5000,spawnImpl=spawn,invoke}){
|
||||
if(!projectPattern.test(project)||!/^sha256:[a-f0-9]{64}$/.test(imageId??''))throw Error('invalid watcher probe ownership or image');
|
||||
const start=performance.now(),deadline=start+timeoutMs,events=[],errors=[],names={container:project+'-watcher-probe',network:project+'_default'};
|
||||
const result={project,imageId,timeoutMs,names,events,errors,cleanup:null,ok:false};
|
||||
const watcher=watchDockerProject(project,{docker,spawnImpl,onEvent:e=>events.push({...e,observedMs:performance.now()-start}),onError:e=>errors.push(e)});
|
||||
const remaining=()=>{const ms=Math.ceil(deadline-performance.now());if(ms<=0)throw Error('Docker watcher probe deadline');if(errors.length)throw Error('Docker watcher probe stream failed');return ms;};
|
||||
const call=invoke??((args,timeout)=>new Promise((resolve,reject)=>execFile(docker,args,{encoding:'utf8',timeout,killSignal:'SIGKILL',maxBuffer:65536},(error,stdout)=>error?reject(Error('Docker probe '+args.slice(0,2).join(' ')+' failed: '+error.code)):resolve(stdout))));
|
||||
try{
|
||||
for(const args of [
|
||||
['network','create','--label','com.docker.compose.project='+project,names.network],
|
||||
['container','create','--pull','never','--name',names.container,'--label','com.docker.compose.project='+project,'--network','none',imageId,'/bin/true'],
|
||||
]){const id=(await call(args,remaining())).trim();if(!/^[a-f0-9]{12,64}$/.test(id))throw Error('invalid Docker probe create ID');}
|
||||
}catch(error){errors.push(error.message);}
|
||||
finally{
|
||||
// Label-checked cleanup also covers an uncertain create result. Each CLI
|
||||
// call shares a separate bounded cleanup budget and never reads Config.Env.
|
||||
const cleanupDeadline=performance.now()+3000;
|
||||
try{result.cleanup=cleanupProject({project},args=>{const timeout=Math.ceil(cleanupDeadline-performance.now());if(timeout<=0)throw Error('probe cleanup deadline');return execFileSync(docker,args,{encoding:'utf8',stdio:['ignore','pipe','pipe'],timeout,killSignal:'SIGKILL',maxBuffer:65536});});if(!result.cleanup?.clean)throw Error('probe cleanup not confirmed');}
|
||||
catch(error){errors.push('probe cleanup: '+error.message);}
|
||||
}
|
||||
const missing=()=>['container.create','container.destroy','network.create','network.destroy'].filter(key=>!events.some(e=>e.kind+'.'+e.action===key&&e.observedMs<=timeoutMs));
|
||||
while(!errors.length&&missing().length&&performance.now()<deadline)await new Promise(resolve=>setTimeout(resolve,10));
|
||||
result.missing=missing();if(result.missing.length)errors.push('missing probe events: '+result.missing.join(', '));
|
||||
await watcher.stop();result.wallMs=performance.now()-start;result.ok=!errors.length&&result.cleanup?.clean===true;
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runProof(){
|
||||
const proofStart=performance.now();
|
||||
let interrupted=false,abortOnSignal=()=>{};
|
||||
const signal=()=>{interrupted=true;abortOnSignal();};
|
||||
for(const name of ['SIGINT','SIGTERM','SIGHUP'])process.on(name,signal);
|
||||
const fail=m=>{throw Error(m);};
|
||||
const within=(root,path)=>{const p=relative(root,path);return p===''||!p.startsWith('..')&&!isAbsolute(p);};
|
||||
const grant=process.env.MOSAIC_PROOF_GRANT;
|
||||
if(!/^sage-[0-9]+$/.test(grant??''))fail('explicit whole-slot MOSAIC_PROOF_GRANT required');
|
||||
const config=JSON.parse(readFileSync(process.argv[2],'utf8'));
|
||||
for(const key of ['candidateA','candidateB'])if(!/^[a-f0-9]{40}$/.test(config[key]??''))fail('full approved candidate SHA required: '+key);
|
||||
if(config.candidateA===config.candidateB)fail('two distinct approved candidates required');
|
||||
if(!config.candidateDifference?.trim())fail('record why the approved candidates differ');
|
||||
const launcher=realpathSync(config.launcherRoot),repo=realpathSync(config.repo),receipt=realpathSync(config.imageReceipt);
|
||||
const git=(root,...args)=>execFileSync('git',['-C',root,...args],{encoding:'utf8',stdio:['ignore','pipe','pipe']}).trim();
|
||||
if(git(launcher,'rev-parse','HEAD')!==config.launcherCandidate)fail('launcher pin differs');
|
||||
if(git(launcher,'status','--porcelain').split('\n').some(l=>l&&l!=='?? node_modules'))fail('launcher checkout dirty');
|
||||
const root=resolve(config.runRoot);
|
||||
if(existsSync(root)||!within('/mnt/storage/scratch',realpathSync(dirname(root))))fail('fresh scratch runRoot required');
|
||||
const {validateTooling}=await import(pathToFileURL(join(launcher,'scripts/testing/gate-lane.mjs')));
|
||||
const {verifyImage}=await import(pathToFileURL(join(launcher,'scripts/testing/lane-image.mjs')));
|
||||
const {runRecords,status,processInventory,sameProcess}=await import(pathToFileURL(join(launcher,'scripts/testing/lane-state.mjs')));
|
||||
const {summarizePairs}=await import(pathToFileURL(join(launcher,'scripts/testing/lane-evidence.mjs')));
|
||||
const {cleanupProject}=await import(pathToFileURL(join(launcher,'scripts/testing/lane-docker.mjs')));
|
||||
const docker=execFileSync('which',['docker'],{encoding:'utf8'}).trim();
|
||||
const image=JSON.parse(readFileSync(receipt,'utf8'));
|
||||
mkdirSync(root,{mode:0o700});const state=join(root,'state');mkdirSync(state,{mode:0o700});
|
||||
const put=(name,value)=>writeFileSync(join(root,name),JSON.stringify(value,null,2)+'\n',{mode:0o600,flag:'wx'});
|
||||
const waves=[
|
||||
[['solo-a','a',config.candidateA]],
|
||||
[['solo-b','b',config.candidateB]],
|
||||
[['pair1-a','a',config.candidateA],['pair1-b','b',config.candidateB]],
|
||||
[['pair2-b','b',config.candidateA],['pair2-a','a',config.candidateB]],
|
||||
[['pair3-a','a',config.candidateA],['pair3-b','b',config.candidateB]],
|
||||
];
|
||||
const plan=waves.map(w=>w.map(([label,lane,candidate])=>({label,lane,candidate,dir:join(root,label),checkout:join(root,label,'wt'),out:join(root,label,'out')})));
|
||||
put('plan.json',{grant,...config,plan,image,started:new Date().toISOString()});
|
||||
const active=new Set(),watchers=new Map(),objects=new Map();let firstFailure=null;
|
||||
const eventError=[];
|
||||
const knownRuns=new Map();let abortTimer;
|
||||
const ownedRuns=()=>runRecords(state).flatMap(r=>r.records.filter(e=>e.kind==='start'&&plan.flat().some(p=>p.checkout===e.checkout)));
|
||||
function logObject(project,kind,id,name,action){
|
||||
if(!/^mg197-[ab]-[a-f0-9]{12}$/.test(project)||!['container','network'].includes(kind)||!/^[a-f0-9]{12,64}$/.test(id))fail('invalid Docker ownership metadata');
|
||||
if(name&&!String(name).replace(/^\//,'').startsWith(project))fail('Docker name outside its recorded project');
|
||||
const key=kind+':'+id,old=objects.get(key);const value={project,kind,id,name:name??old?.name??null,created:old?.created||action==='create',destroyed:old?.destroyed||action==='destroy'};
|
||||
objects.set(key,value);appendFileSync(join(root,'docker-objects.jsonl'),JSON.stringify({...value,action,at:new Date().toISOString()})+'\n',{mode:0o600});
|
||||
}
|
||||
function ensureWatchers(final=false){
|
||||
if(final||[...active].some(j=>!knownRuns.has(j.run.checkout)))for(const run of ownedRuns())knownRuns.set(run.checkout,run);
|
||||
for(const run of knownRuns.values())if(!watchers.has(run.project)){
|
||||
const watcher=watchDockerProject(run.project,{docker,onEvent:event=>logObject(event.project,event.kind,event.id,event.name,event.action),onError:message=>eventError.push(message)});
|
||||
watchers.set(run.project,watcher);
|
||||
}
|
||||
}
|
||||
function inventory(project){
|
||||
const found=[];
|
||||
for(const kind of ['container','network']){
|
||||
const query=kind==='container'?['ps','-aq']:['network','ls','-q'];
|
||||
const ids=execFileSync(docker,[...query,'--filter','label=com.docker.compose.project='+project],{encoding:'utf8'}).trim().split(/\s+/).filter(Boolean);
|
||||
for(const id of ids){
|
||||
if(!/^[a-f0-9]{12,64}$/.test(id))fail('invalid inventory ID');
|
||||
// Read only name and ownership label, never Config.Env or credentials.
|
||||
const labels=kind==='container'?'Config.Labels':'Labels';
|
||||
const format='{"name":{{json .Name}},"project":{{json (index .'+labels+' "com.docker.compose.project")}}}';
|
||||
const item=JSON.parse(execFileSync(docker,[kind,'inspect',id,'--format',format],{encoding:'utf8'}));
|
||||
if(item.project!==project)fail('foreign Docker object');
|
||||
logObject(project,kind,id,item.name,'inventory');found.push({kind,id,name:item.name});
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
function failureDetails(job){
|
||||
const events=runRecords(state).find(r=>r.records.some(e=>e.kind==='start'&&e.checkout===job.run.checkout))?.records??[];
|
||||
const steps=events.filter(e=>e.kind==='step');
|
||||
const paths=firstFailure?.gate===job.run.label&&firstFailure.log?[firstFailure.log]:existsSync(job.run.out)?readdirSync(job.run.out).filter(n=>n.endsWith('.log')).map(n=>join(job.run.out,n)):[];
|
||||
for(const log of paths){
|
||||
const text=readFileSync(log,'utf8');const bad=text.match(/^(?:\s*✖ (?!failing tests:)|not ok \d+ - |FAIL\s+)(.+)$/m);if(!bad)continue;
|
||||
const detail=text.match(/✖ failing tests:[\s\S]*$/)?.[0]??text;
|
||||
const file=detail.match(/^test at (.+?\.test\.(?:mjs|js|cjs))(?::\d+)+/m)?.[1]??detail.match(/file:\/\/([^\s)]+\.test\.(?:mjs|js|cjs))/)?.[1];
|
||||
return {lane:job.run.lane,gate:job.run.label,step:log.split('/').at(-1).slice(0,-4),file:file??'unresolved; inspect preserved log',test:bad[1],log};
|
||||
}
|
||||
const failed=steps.find(e=>e.rc||e.requiredSkips?.length);
|
||||
return {lane:job.run.lane,gate:job.run.label,step:failed?.name??'preflight or cleanup',file:'see gate.log',test:failed?.requiredSkips?.join('; ')??'wrapper failed',log:join(job.run.dir,'gate.log')};
|
||||
}
|
||||
function abortActive(){
|
||||
for(const job of active)job.child.kill('SIGTERM');
|
||||
if(!abortTimer){abortTimer=setTimeout(()=>{for(const job of active)job.child.kill('SIGKILL');},30000);abortTimer.unref();}
|
||||
}
|
||||
abortOnSignal=abortActive;
|
||||
function newFailure(job){
|
||||
if(!existsSync(job.run.out))return null;job.logs??=new Map();
|
||||
for(const name of readdirSync(job.run.out).filter(n=>n.endsWith('.log'))){
|
||||
const log=join(job.run.out,name),st=statSync(log),cursor=job.logs.get(log)??{size:0,tail:''};if(st.size===cursor.size)continue;
|
||||
const fd=openSync(log,'r'),bytes=Buffer.alloc(st.size-cursor.size);try{readSync(fd,bytes,0,bytes.length,cursor.size);}finally{closeSync(fd);}
|
||||
const text=cursor.tail+bytes.toString('utf8'),match=text.match(/^(?:\s*✖ (?!failing tests:)|not ok \d+ - |FAIL\s+)(.+)$/m);
|
||||
job.logs.set(log,{size:st.size,tail:text.slice(-8192)});
|
||||
if(match)return {lane:job.run.lane,gate:job.run.label,step:name.slice(0,-4),file:'awaiting runner failure details',test:match[1],log};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function monitor(){
|
||||
try{
|
||||
ensureWatchers();
|
||||
for(const job of active){const detail=newFailure(job);if(detail&&!firstFailure){firstFailure=detail;process.stderr.write('FIRST_FAILURE '+JSON.stringify(detail)+'\n');abortActive();}}
|
||||
if(eventError.length&&!firstFailure){firstFailure={kind:'Docker evidence',errors:[...eventError]};abortActive();}
|
||||
}catch(error){eventError.push(error.message);abortActive();}
|
||||
}
|
||||
function launch(run){
|
||||
const fd=openSync(join(run.dir,'gate.log'),'wx',0o600);
|
||||
const child=spawn('bash',[join(launcher,'scripts/testing/gate-lane.sh'),'run','--lane',run.lane,'--candidate',run.candidate,'--checkout',run.checkout,'--out',run.out,'--state-root',state,'--image-receipt',receipt,'--grant',grant],{stdio:['ignore',fd,fd]});closeSync(fd);
|
||||
const job={child,run};active.add(job);
|
||||
job.done=new Promise(resolve=>{child.once('error',error=>{active.delete(job);resolve({rc:127,error:error.message});});child.once('exit',(code,signal)=>{active.delete(job);resolve({rc:code??128,signal});});});
|
||||
return job;
|
||||
}
|
||||
function gateReceipt(run){
|
||||
const item=runRecords(state).find(r=>r.records.some(e=>e.kind==='start'&&e.checkout===run.checkout));
|
||||
if(!item)fail('missing start receipt: '+run.label);
|
||||
const terminal=item.records.filter(e=>e.kind==='terminal').at(-1);
|
||||
if(!terminal||terminal.rc!==0||!terminal.clean||terminal.imageBuilds!==0)fail('gate failed or unclean: '+run.label);
|
||||
if(item.records.some(e=>e.requiredSkips?.length))fail('required prerequisite skipped: '+run.label);
|
||||
return {...terminal,label:run.label,lane:run.lane,candidate:run.candidate,receiptDir:item.dir};
|
||||
}
|
||||
async function stop(){
|
||||
const jobs=[...active];for(const j of jobs)j.child.kill('SIGTERM');
|
||||
const timer=setTimeout(()=>{for(const j of jobs)j.child.kill('SIGKILL');},30000);timer.unref();
|
||||
await Promise.all(jobs.map(j=>j.done));clearTimeout(timer);
|
||||
}
|
||||
const measured=[],start=proofStart;
|
||||
const monitorTimer=setInterval(monitor,100);
|
||||
let summary,watcherProbe=null;
|
||||
try{
|
||||
// Preparation is inside the cleanup path. Yield between synchronous clones
|
||||
// so a pending signal stops preparation before another invocation is made.
|
||||
for(const run of plan.flat()){
|
||||
await new Promise(resolve=>setImmediate(resolve));
|
||||
if(interrupted)fail('proof interrupted during preparation');
|
||||
mkdirSync(run.dir,{mode:0o700});
|
||||
execFileSync('git',['clone','--quiet','--no-hardlinks','--local',repo,run.checkout],{stdio:'pipe'});
|
||||
execFileSync('git',['-C',run.checkout,'checkout','--quiet','--detach',run.candidate],{stdio:'pipe'});
|
||||
symlinkSync('/mnt/storage/src/mosaic-stack/node_modules',join(run.checkout,'node_modules'));
|
||||
validateTooling(run.checkout);verifyImage(image,run.checkout);
|
||||
}
|
||||
const probeProject='mg197-a-'+randomBytes(6).toString('hex');
|
||||
put('watcher-probe-plan.json',{project:probeProject,container:probeProject+'-watcher-probe',network:probeProject+'_default',imageId:image.imageId});
|
||||
watcherProbe=await probeDockerWatchers({docker,imageId:image.imageId,cleanupProject,project:probeProject});
|
||||
if(!watcherProbe.ok)fail('Docker watcher preflight failed: '+JSON.stringify(watcherProbe));
|
||||
for(const wave of plan){
|
||||
if(interrupted)fail('proof interrupted');
|
||||
const observed=status(state);
|
||||
if(observed.legacy.length||observed.a.state!=='CLEAR'||observed.b.state!=='CLEAR')fail('proof requires both lanes clear and no legacy gate');
|
||||
const jobs=wave.map(launch);
|
||||
for(const j of jobs)j.done.then(result=>{if(result.rc){firstFailure??=failureDetails(j);abortActive();}}).catch(error=>{eventError.push(error.message);abortActive();});
|
||||
const results=await Promise.all(jobs.map(j=>j.done));
|
||||
if(firstFailure?.gate){const failedJob=jobs.find(j=>j.run.label===firstFailure.gate);if(failedJob)firstFailure=failureDetails(failedJob);}
|
||||
if(interrupted||firstFailure||eventError.length||results.some(r=>r.rc))fail('failed wave: '+wave.map(r=>r.label).join(',')+' '+JSON.stringify(firstFailure));
|
||||
measured.push(wave.map(gateReceipt));
|
||||
const end=status(state);
|
||||
if(end.legacy.length||end.a.state!=='CLEAR'||end.b.state!=='CLEAR')fail('unclean wave status');
|
||||
}
|
||||
const solos=measured.slice(0,2).flat(),pairs=measured.slice(2);summary={grant,watcherProbe,solos,pairs,summary:summarizePairs(solos,pairs),wallMs:performance.now()-start,finished:new Date().toISOString()};
|
||||
}catch(error){
|
||||
await stop();put('failed.json',{grant,message:error.message,firstFailure,measured,at:new Date().toISOString()});process.stderr.write(error.message+'\n');process.exitCode=1;
|
||||
}finally{
|
||||
clearInterval(monitorTimer);clearTimeout(abortTimer);await stop();const cleanupErrors=[];
|
||||
try{ensureWatchers(true);}catch(error){cleanupErrors.push({message:error.message});}
|
||||
for(const run of knownRuns.values())try{inventory(run.project);cleanupProject({project:run.project},docker);if(inventory(run.project).length)fail('Docker resources remain');}catch(error){cleanupErrors.push({project:run.project,message:error.message});}
|
||||
await Promise.all([...watchers.values()].map(w=>w.stop()));
|
||||
let finalStatus;try{finalStatus=status(state);if(finalStatus.legacy.length||finalStatus.a.state!=='CLEAR'||finalStatus.b.state!=='CLEAR')cleanupErrors.push({message:'lane status not clear',status:finalStatus});}catch(error){cleanupErrors.push({message:error.message});}
|
||||
// Start receipts establish which short roots belong to this proof. Never
|
||||
// discover roots by scanning scratch or follow a substituted root symlink.
|
||||
const tempRoots=[];let procs;
|
||||
try{procs=processInventory();}catch(error){cleanupErrors.push({message:'process inventory failed: '+error.message});}
|
||||
for(const run of knownRuns.values()){
|
||||
const record={gate:plan.flat().find(p=>p.checkout===run.checkout)?.label,lane:run.lane,tempRoot:run.tempRoot};
|
||||
try{
|
||||
if(!/^\/mnt\/storage\/scratch\/g[ab]-[A-Za-z0-9]{6}$/.test(run.tempRoot??'')||!run.tempRoot.startsWith('/mnt/storage/scratch/g'+run.lane+'-'))fail('invalid recorded temp root');
|
||||
const st=lstatSync(run.tempRoot);
|
||||
if(!st.isDirectory()||st.isSymbolicLink()||st.uid!==process.getuid()||realpathSync(run.tempRoot)!==run.tempRoot)fail('temp root is not an owned real directory');
|
||||
const receipt=runRecords(state).find(r=>r.records.some(e=>e.kind==='start'&&e.checkout===run.checkout));
|
||||
const groups=new Set(receipt?.records.filter(e=>e.kind==='child').map(e=>e.group));
|
||||
if(!procs||sameProcess(run.owner)||procs.some(p=>p.state!=='Z'&&p.pid!==process.pid&&(groups.has(p.group)||p.argv.some(a=>a.includes(run.tempRoot)||a.includes(run.checkout)))))fail('run process still present or inventory unavailable; temp root retained');
|
||||
const terminal=receipt?.records.filter(e=>e.kind==='terminal').at(-1);
|
||||
if(!terminal?.clean){record.action='kept';record.reason='run cleanup unverified';cleanupErrors.push({...record});tempRoots.push(record);continue;}
|
||||
if(terminal.rc!==0||firstFailure?.gate===record.gate){record.action='kept';record.reason='failed gate diagnosis';}
|
||||
else{rmSync(run.tempRoot,{recursive:true});record.action='removed';}
|
||||
}catch(error){record.action='error';record.message=error.message;cleanupErrors.push({...record});}
|
||||
tempRoots.push(record);
|
||||
}
|
||||
put('docker-summary.json',{watcherProbe,objects:[...objects.values()],eventError,cleanupErrors,finalStatus,tempRoots});
|
||||
if(cleanupErrors.length||eventError.length||[...objects.values()].some(o=>!o.name))process.exitCode=1;
|
||||
if(summary&&!process.exitCode){
|
||||
for(const run of knownRuns.values())for(const kind of ['container','network'])if(![...objects.values()].some(o=>o.project===run.project&&o.kind===kind&&o.created)){process.exitCode=1;process.stderr.write('missing Docker creation evidence: '+run.project+' '+kind+'\n');}
|
||||
if(!process.exitCode){put('summary.json',summary);process.stdout.write('PROOF_PASSED '+join(root,'summary.json')+'\n');}
|
||||
}
|
||||
for(const name of ['SIGINT','SIGTERM','SIGHUP'])process.off(name,signal);
|
||||
}
|
||||
|
||||
}
|
||||
if(process.argv[1]&&import.meta.url===pathToFileURL(process.argv[1]).href)await runProof();
|
||||
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"r1Base": "aa1bd209e",
|
||||
"r1Candidate": "6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3",
|
||||
"r2Base": "57f59e0c399ce29b1bb85ca6af686fdc10eb195d",
|
||||
"rows": [
|
||||
{
|
||||
"path": "packages/cli/tests/fixtures/gate-lane-child.mjs",
|
||||
"r1Sha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
|
||||
"r2Sha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "packages/cli/tests/gate-lane-fixtures.test.mjs",
|
||||
"r1Sha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
|
||||
"r2Sha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "packages/cli/tests/gate-lane-image.test.mjs",
|
||||
"r1Sha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
|
||||
"r2Sha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "packages/cli/tests/gate-lane-state.test.mjs",
|
||||
"r1Sha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
|
||||
"r2Sha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "packages/cli/tests/gate-lane.test.mjs",
|
||||
"r1Sha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
|
||||
"r2Sha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "packages/cli/tests/stack-release.test.mjs",
|
||||
"r1Sha256": "8ca733a600d5f5534d7e14d8533e1b16d710ebd43080ef8421238b5dd145fade",
|
||||
"r2Sha256": "b95c0c125b6a283961e24adc07d43150bcbd05d2f11005180b836043015c52a6",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": true,
|
||||
"r1BlobEqual": false
|
||||
},
|
||||
{
|
||||
"path": "packages/control-board/tests/serve.test.mjs",
|
||||
"r1Sha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
|
||||
"r2Sha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "packages/harness/tests/claude-session.test.mjs",
|
||||
"r1Sha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
|
||||
"r2Sha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "packages/harness/tests/pi-session.test.mjs",
|
||||
"r1Sha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
|
||||
"r2Sha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/preview.test.mjs",
|
||||
"r1Sha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
|
||||
"r2Sha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/proof-cli.test.mjs",
|
||||
"r1Sha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
|
||||
"r2Sha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/serve.test.mjs",
|
||||
"r1Sha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
|
||||
"r2Sha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/service.test.mjs",
|
||||
"r1Sha256": "c915239f0c223209baf84c96ec9f5ae2845fce7dd98ad46b073a96033aa7d6df",
|
||||
"r2Sha256": "845313239547ddbc478090b29f96385f0b8853e5087c572393be328e7075f578",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": true,
|
||||
"r1BlobEqual": false
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/tester-cli.test.mjs",
|
||||
"r1Sha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
|
||||
"r2Sha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-discord.sh",
|
||||
"r1Sha256": "c10af53b0717344026cb22462964f253c7401c5082ac6ef3722a89d9dfa5c661",
|
||||
"r2Sha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
|
||||
"mode": "0o755",
|
||||
"landedPathChanged": true,
|
||||
"r1BlobEqual": false
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-foundation.sh",
|
||||
"r1Sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
|
||||
"r2Sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
|
||||
"mode": "0o755",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-queue.sh",
|
||||
"r1Sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
|
||||
"r2Sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
|
||||
"mode": "0o755",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/README.md",
|
||||
"r1Sha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
|
||||
"r2Sha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/gate-lane.mjs",
|
||||
"r1Sha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
|
||||
"r2Sha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/gate-lane.sh",
|
||||
"r1Sha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
|
||||
"r2Sha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
|
||||
"mode": "0o755",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/lane-docker.mjs",
|
||||
"r1Sha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
|
||||
"r2Sha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/lane-evidence.mjs",
|
||||
"r1Sha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
|
||||
"r2Sha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/lane-fixtures.mjs",
|
||||
"r1Sha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
|
||||
"r2Sha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/lane-image.mjs",
|
||||
"r1Sha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
|
||||
"r2Sha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/lane-state.mjs",
|
||||
"r1Sha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
|
||||
"r2Sha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
|
||||
"mode": "0o644",
|
||||
"landedPathChanged": false,
|
||||
"r1BlobEqual": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
packages/cli/tests/gate-lane-fixtures.test.mjs
|
||||
packages/cli/tests/gate-lane-image.test.mjs
|
||||
packages/cli/tests/gate-lane-state.test.mjs
|
||||
packages/cli/tests/gate-lane.test.mjs
|
||||
packages/cli/tests/stack-instances.test.mjs
|
||||
packages/cli/tests/stack-release-external.test.mjs
|
||||
packages/cli/tests/stack-release.test.mjs
|
||||
packages/cli/tests/stack-upgrade.test.mjs
|
||||
packages/control-board/tests/serve.test.mjs
|
||||
packages/harness/tests/claude-session.test.mjs
|
||||
packages/harness/tests/pi-session.test.mjs
|
||||
packages/webui/tests/preview.test.mjs
|
||||
packages/webui/tests/proof-cli.test.mjs
|
||||
packages/webui/tests/serve.test.mjs
|
||||
packages/webui/tests/service.test.mjs
|
||||
packages/webui/tests/tester-cli.test.mjs
|
||||
scripts/test-discord.sh
|
||||
scripts/test-foundation.sh
|
||||
scripts/test-queue.sh
|
||||
@@ -0,0 +1,45 @@
|
||||
diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs
|
||||
index 8788158c3..a78f52062 100644
|
||||
--- a/packages/cli/tests/stack-release.test.mjs
|
||||
+++ b/packages/cli/tests/stack-release.test.mjs
|
||||
@@ -0,0 +1,2 @@
|
||||
+import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
|
||||
+import { after as laneAfter } from 'node:test';
|
||||
@@ -1777 +1779 @@ const TESTER='7499';
|
||||
-const freePort=async()=>{const s=createServer();s.listen(0,'127.0.0.1');await once(s,'listening');const p=String(s.address().port);await new Promise((r)=>s.close(r));return p};
|
||||
+const freePort=async()=>String(await leasePort({after:laneAfter},{closed:true}));
|
||||
diff --git a/packages/webui/tests/service.test.mjs b/packages/webui/tests/service.test.mjs
|
||||
index a6e66608a..adb460d07 100644
|
||||
--- a/packages/webui/tests/service.test.mjs
|
||||
+++ b/packages/webui/tests/service.test.mjs
|
||||
@@ -0,0 +1,2 @@
|
||||
+import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
|
||||
+import { after as laneAfter } from 'node:test';
|
||||
@@ -48,9 +50 @@ function filled(flags, root = REPO) {
|
||||
-async function closedPort() {
|
||||
- const server = createServer();
|
||||
- server.listen(0, '127.0.0.1');
|
||||
- await once(server, 'listening');
|
||||
- const { port } = server.address();
|
||||
- server.close();
|
||||
- await once(server, 'close');
|
||||
- return port;
|
||||
-}
|
||||
+async function closedPort() { return leasePort({after:laneAfter}, {closed:true}); }
|
||||
diff --git a/scripts/test-discord.sh b/scripts/test-discord.sh
|
||||
index 114994936..4daf0d143 100755
|
||||
--- a/scripts/test-discord.sh
|
||||
+++ b/scripts/test-discord.sh
|
||||
@@ -73,0 +74,2 @@ TOOLS_JSON="{\"roots\":[{\"name\":\"docs\",\"path\":\"$SANDBOX/toolroot\"}],\"ma
|
||||
+export PI_CODING_AGENT_DIR="$SANDBOX/pi-agent"
|
||||
+mkdir -p "$PI_CODING_AGENT_DIR"
|
||||
@@ -145 +147,8 @@ fi
|
||||
-node_tests() { env -u NODE_TEST_CONTEXT node --test "$@"; }
|
||||
+node_tests() {
|
||||
+ local lane_concurrency=()
|
||||
+ if [ -n "${MOSAIC_TEST_CONCURRENCY:-}" ]; then
|
||||
+ [ "$MOSAIC_TEST_CONCURRENCY" = 2 ] || return 2
|
||||
+ lane_concurrency=(--test-concurrency=2)
|
||||
+ fi
|
||||
+ env -u NODE_TEST_CONTEXT node --test "${lane_concurrency[@]}" "$@"
|
||||
+}
|
||||
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"base": "c771a36f4bd0b6c359282425c668996a90072f93",
|
||||
"priorCandidate": "a3b7cbde8ef51c206a83769a7c272e4f9a1de5bc",
|
||||
"originalCandidate": "6318f0d0bba9ee90ccaa3503cac8c2e04c6397f2ef6c3564f687fbc2092989e3",
|
||||
"overlap": [
|
||||
"packages/cli/tests/stack-release.test.mjs"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"path": "packages/cli/tests/fixtures/gate-lane-child.mjs",
|
||||
"r1Sha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
|
||||
"priorCandidateSha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
|
||||
"newSha256": "209682b968e929da4b46dc97c64fcd7aa68c490f3c6be44595f5d85f8d2e1483",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/cli/tests/gate-lane-fixtures.test.mjs",
|
||||
"r1Sha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
|
||||
"priorCandidateSha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
|
||||
"newSha256": "29afa181804a724c695678eb9459c156e00f1084647c704e814c5bee156c0c25",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/cli/tests/gate-lane-image.test.mjs",
|
||||
"r1Sha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
|
||||
"priorCandidateSha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
|
||||
"newSha256": "0253621a49c2e77ffcdbf32beee7e1f773ec2c52c8487dcc5aa254e61807af7c",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/cli/tests/gate-lane-state.test.mjs",
|
||||
"r1Sha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
|
||||
"priorCandidateSha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
|
||||
"newSha256": "948d62c76447d8269f03518562a261564c202ec9207461fb21d2b1c11c9bb9fc",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/cli/tests/gate-lane.test.mjs",
|
||||
"r1Sha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
|
||||
"priorCandidateSha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
|
||||
"newSha256": "240bf0c15025ad6f88b251917a3c8d72bd21c2e5df6771e1ceb83a1d90b24a51",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/cli/tests/stack-release.test.mjs",
|
||||
"r1Sha256": "8ca733a600d5f5534d7e14d8533e1b16d710ebd43080ef8421238b5dd145fade",
|
||||
"priorCandidateSha256": "b95c0c125b6a283961e24adc07d43150bcbd05d2f11005180b836043015c52a6",
|
||||
"newSha256": "071ea2803b8c7e0450bb4c86ac9b49ae7c180af1599dca52bfce6d0274ca205b",
|
||||
"unchangedFromPrior": false,
|
||||
"overlaps234": true,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/control-board/tests/serve.test.mjs",
|
||||
"r1Sha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
|
||||
"priorCandidateSha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
|
||||
"newSha256": "4838cb4609da28fc921370c5542a82402fbc9ceb469692a36f0eafc713bb56c8",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/harness/tests/claude-session.test.mjs",
|
||||
"r1Sha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
|
||||
"priorCandidateSha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
|
||||
"newSha256": "0a09b0255e9144a039428eb8e9f56181f94ccb2d187e474ea13392b485fdd008",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/harness/tests/pi-session.test.mjs",
|
||||
"r1Sha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
|
||||
"priorCandidateSha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
|
||||
"newSha256": "6db5c3b7063baf933061e91e8bdf66740717dbb3066be3b70ac779fb17022094",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/preview.test.mjs",
|
||||
"r1Sha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
|
||||
"priorCandidateSha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
|
||||
"newSha256": "ada49ec100d75b3e739263593164b2798c378f3df0f38d73d80cf011ef16e234",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/proof-cli.test.mjs",
|
||||
"r1Sha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
|
||||
"priorCandidateSha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
|
||||
"newSha256": "d3cec62f6eb8e2c1e0747f2a1bf1fc3f1a2122e45870d5a416d1830d3b6182cb",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/serve.test.mjs",
|
||||
"r1Sha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
|
||||
"priorCandidateSha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
|
||||
"newSha256": "36ecdfcb3d0d9a2411eec751421f770b5bb41ec2fbf9236c5794fb241fd3196b",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/service.test.mjs",
|
||||
"r1Sha256": "c915239f0c223209baf84c96ec9f5ae2845fce7dd98ad46b073a96033aa7d6df",
|
||||
"priorCandidateSha256": "845313239547ddbc478090b29f96385f0b8853e5087c572393be328e7075f578",
|
||||
"newSha256": "845313239547ddbc478090b29f96385f0b8853e5087c572393be328e7075f578",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/tester-cli.test.mjs",
|
||||
"r1Sha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
|
||||
"priorCandidateSha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
|
||||
"newSha256": "8bf4491f7982399b4162cfbe36d0b68888e99ceb2cbaefefc2361732dd95e0c8",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-discord.sh",
|
||||
"r1Sha256": "c10af53b0717344026cb22462964f253c7401c5082ac6ef3722a89d9dfa5c661",
|
||||
"priorCandidateSha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
|
||||
"newSha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o755"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-foundation.sh",
|
||||
"r1Sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
|
||||
"priorCandidateSha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
|
||||
"newSha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o755"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-queue.sh",
|
||||
"r1Sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
|
||||
"priorCandidateSha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
|
||||
"newSha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o755"
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/README.md",
|
||||
"r1Sha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
|
||||
"priorCandidateSha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
|
||||
"newSha256": "845db994e648f29f10efb5e6a4ddcf4be4e28798fc27f1eddf9d150f0e8b5319",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/gate-lane.mjs",
|
||||
"r1Sha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
|
||||
"priorCandidateSha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
|
||||
"newSha256": "2e5decf42a546b1441c199337c9a3162693051f2299878f617dd039d6231f226",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/gate-lane.sh",
|
||||
"r1Sha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
|
||||
"priorCandidateSha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
|
||||
"newSha256": "41804d964c55dd07b6224855aae9dc44037f4e80c515c10f98fe04d6d3fa0084",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o755"
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/lane-docker.mjs",
|
||||
"r1Sha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
|
||||
"priorCandidateSha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
|
||||
"newSha256": "fbe54165439923a852f64d69a6188ef29d237755289e9e21ddbfdab00e9ef933",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/lane-evidence.mjs",
|
||||
"r1Sha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
|
||||
"priorCandidateSha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
|
||||
"newSha256": "51d4dcff4ab01730b923e0a59c218f9a3d33e6ba729be1def03703bc457fb37e",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/lane-fixtures.mjs",
|
||||
"r1Sha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
|
||||
"priorCandidateSha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
|
||||
"newSha256": "8634070d68bca694c8a58911eda9576991ed0eca1eb055a2c1bbf10261305939",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/lane-image.mjs",
|
||||
"r1Sha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
|
||||
"priorCandidateSha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
|
||||
"newSha256": "2259045913b4e3042b7afd3ff6a0fb9c2cc9b4cf359f943e4dce9b8658b91982",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
},
|
||||
{
|
||||
"path": "scripts/testing/lane-state.mjs",
|
||||
"r1Sha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
|
||||
"priorCandidateSha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
|
||||
"newSha256": "c37b3d5ec3025948dd8150d18d4ccc4eeb3539739ca37e4d86edede5bcafd1a8",
|
||||
"unchangedFromPrior": true,
|
||||
"overlaps234": false,
|
||||
"mode": "0o644"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
Compared 57f59e0c3 to c771a36f4bd0b6c359282425c668996a90072f93
|
||||
agents/gizmoduck/work/queue-234/BUILD.md
|
||||
docs/SESSIONS.md
|
||||
packages/bus/README.md
|
||||
packages/bus/schema.sql
|
||||
packages/bus/src/broker.mjs
|
||||
packages/bus/src/credentials.mjs
|
||||
packages/bus/src/host-grant.mjs
|
||||
packages/bus/src/host-run.mjs
|
||||
packages/bus/src/migrations/3i-3j.sql
|
||||
packages/bus/src/runtime.mjs
|
||||
packages/bus/src/server.mjs
|
||||
packages/bus/src/store.mjs
|
||||
packages/bus/tests/account-migration.test.mjs
|
||||
packages/bus/tests/chief-of-staff.test.mjs
|
||||
packages/bus/tests/comment-migration.test.mjs
|
||||
packages/bus/tests/fixtures/schema-3i.sql
|
||||
packages/bus/tests/grant-enroll.test.mjs
|
||||
packages/bus/tests/grant-migration.test.mjs
|
||||
packages/bus/tests/hard-waits.test.mjs
|
||||
packages/bus/tests/host-fixtures.mjs
|
||||
packages/bus/tests/host-run.test.mjs
|
||||
packages/bus/tests/host-tools.test.mjs
|
||||
packages/bus/tests/name-migration.test.mjs
|
||||
packages/bus/tests/project-runtime.test.mjs
|
||||
packages/bus/tests/schema.test.mjs
|
||||
packages/bus/tests/spec-fixtures.mjs
|
||||
packages/bus/tests/spec-migration.test.mjs
|
||||
packages/bus/tests/thread-migration.test.mjs
|
||||
packages/bus/tests/usage-migration.test.mjs
|
||||
packages/business/src/business.mjs
|
||||
packages/business/src/hosts.mjs
|
||||
packages/business/tests/hosts.test.mjs
|
||||
packages/cli/README.md
|
||||
packages/cli/src/cli.mjs
|
||||
packages/cli/src/hosts.mjs
|
||||
packages/cli/tests/hosts.test.mjs
|
||||
packages/cli/tests/stack-release.test.mjs
|
||||
scripts/mosaic
|
||||
tools/host/mosaic-keys
|
||||
tools/host/mosaic-keys-write
|
||||
tools/host/mosaic-keys.sshd.conf
|
||||
tools/host/mosaic-keys.sudoers
|
||||
|
||||
Overlap with row 197 source manifest:
|
||||
packages/cli/tests/stack-release.test.mjs
|
||||
Other 24 source paths: no overlap.
|
||||
@@ -0,0 +1,10 @@
|
||||
diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs
|
||||
index daae6efb7..910785bc4 100644
|
||||
--- a/packages/cli/tests/stack-release.test.mjs
|
||||
+++ b/packages/cli/tests/stack-release.test.mjs
|
||||
@@ -0,0 +1,2 @@
|
||||
+import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs';
|
||||
+import { after as laneAfter } from 'node:test';
|
||||
@@ -1779 +1781 @@ const TESTER='7499';
|
||||
-const freePort=async()=>{const s=createServer();s.listen(0,'127.0.0.1');await once(s,'listening');const p=String(s.address().port);await new Promise((r)=>s.close(r));return p};
|
||||
+const freePort=async()=>String(await leasePort({after:laneAfter},{closed:true}));
|
||||
@@ -0,0 +1,158 @@
|
||||
# PRD 1.7 draft: the chief of staff's hire and fire authority, the coder's push to its fork, and agent
|
||||
# host access through the credential store
|
||||
|
||||
Draft by Honker for row 194 (#1692), under Mos's ruling mos-1017 (through Sage, sage-1365). Not approved.
|
||||
It changes nothing until Jason approves it as a `prd.approve` decision in the Console. Sage opens that
|
||||
decision and sends Mos its id. Base: `docs/prd/mosaic-stack.md` version 1.6 at 53bb59462.
|
||||
|
||||
## What it changes
|
||||
|
||||
Row 194 adds `role.hire` and `role.fire` to the vocabulary, and row 199 adds `git.push.remote`. Under
|
||||
REQ-WAIT-3 all three are hard waits, so today Jason resolves every hire, every fire and every push to a
|
||||
remote. This version routes hire and fire for the chief of staff within fixed bounds, so the chief hires
|
||||
and fires as needed and the user's per-agent forbid still holds. It routes `git.push.remote` for the coder
|
||||
only (Mos, mos-1028 item 3). It adds agent host access with sudo through the credential store for
|
||||
agents with a grant (Q183, Q184, Vikunja #424, due 2026-10-27). One `prd.approve` decision covers it all
|
||||
(Vikunja #420).
|
||||
|
||||
When this version lands, a reviewed commit:
|
||||
|
||||
1. adds `role.hire`, `role.fire` and `git.push.remote` to `ROUTED` in
|
||||
`packages/business/src/vocabulary.mjs` and in `packages/bus/src/broker.mjs`.
|
||||
2. adds `role.hire` and `role.fire` to `roles/chief-of-staff.json` withinRole, and `git.push.remote` to
|
||||
`roles/coder.json` withinRole. No other role file gets `git.push.remote`.
|
||||
3. switches the broker's check on `instance.hire` and `instance.fire` from a cited decision to the
|
||||
`reason` form, as `role.end` works for REQ-ROLE-2, and makes `publish`'s `decision` optional for a
|
||||
within-role class (row 199 D9 step 1).
|
||||
4. states, for a `git.push.remote` with no decision, where the source commit comes from: the latest work
|
||||
commit the bus recorded for the coder's task. The check still matches it against the work ref tip and
|
||||
the push sends it by oid (row 199, duckworth-0022 N9).
|
||||
5. adds the host credential grant check and the host access it allows (REQ-CRED-3, Vikunja #424).
|
||||
|
||||
Every other check stays as rows 194 and 199 build it. Filling `hireable` stays a separate reviewed role
|
||||
file commit in `roles/`.
|
||||
|
||||
## Text for the PRD
|
||||
|
||||
Status line, appended to the current paragraph:
|
||||
|
||||
> Version 1.7 routes the chief of staff's hire and fire actions and the coder's push to its own fork,
|
||||
> and adds agent host access through the credential store, approved by Jason in decision
|
||||
> <prd.approve decision id>.
|
||||
|
||||
Source, a new bullet above "Version 1.6 source":
|
||||
|
||||
> - Version 1.7 source, TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as
|
||||
> needed; unless user forbids per agent". And Jason through Mos (mos-1067), DECISIONS Q183, in part: "I will
|
||||
> still want to allow infra agent(s) on my .53 instance to access other servers, etc. However, not all
|
||||
> agents will need to have that access. I envision a credential store within Mosaic Stack that works
|
||||
> as some sort of variable for the agent to call. The system will enforce access to the credentials to
|
||||
> keep the agents sandboxed with proper security. I need to be able to trust my agents with assisting
|
||||
> me. This will require sudo access to several systems. If we can utilize the credential store in
|
||||
> Mosaic Stack and allow agents to perform their jobs with NOPASSWD sudo on various machines, I am a
|
||||
> happy man." DECISIONS Q184, on whether that is in place by 10-27: "Q5: by 10.27"
|
||||
|
||||
Under "Added in version 1.2", after REQ-COS-2:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-COS-3.** The chief of staff hires and fires role instances without a decision each time. Hiring
|
||||
> adds a role instance to the organization file and lists it for launch. Firing ends the instance's
|
||||
> runs and removes it from the launch list. It stays in the organization file. Only the user deletes
|
||||
> an instance. These actions are `role.hire` and `role.fire`. They leave the hard waits and route
|
||||
> under this requirement, within these bounds:
|
||||
> - The chief acts only in its own organization.
|
||||
> - The chief hires only role definitions that need no credentials, until a later version adds
|
||||
> credentialed roles.
|
||||
> - The chief hires only the role kinds its role file lists. Changing that list is a reviewed commit.
|
||||
> - The chief hires only a role whose every action is on a fixed list of plain work actions: sending
|
||||
> messages, writing records, asking for review, updating its assigned tasks, and the scope and
|
||||
> working-push actions of an assigned task. A role holding any other action, such as launch, end,
|
||||
> organization, naming, hire, review verdict or merge authority, or any action added later, is never
|
||||
> hired. So the chief never hires a chief, never picks its own reviewers or merge gate, and the author
|
||||
> is never the reviewer. This holds whatever the role file's list says. Adding to that list is a PRD
|
||||
> change.
|
||||
> - The chief may set a hired instance's model and narrow its limits, and nothing else about it.
|
||||
> - The user can forbid per agent. The user marks an instance so the chief can't fire it, end its runs
|
||||
> or change its model, and marks role kinds the chief can't hire. Only the user changes these marks, through the
|
||||
> WebUI or the CLI.
|
||||
> - The kill switch stops every hire and fire while it is on.
|
||||
> - Every hire and fire is logged with the chief's instance, its run and the reason.
|
||||
|
||||
Under "Landing", after REQ-LAND-1:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-LAND-2.** A coder pushes its own work to its own fork. The action is `git.push.remote`.
|
||||
> - What it pushes: one commit, by its oid, never a ref name. That commit is the latest work commit the
|
||||
> bus recorded for the coder's task, and the coder made that push. A decision that authorizes a push
|
||||
> names that commit, and it doesn't authorize any other.
|
||||
> - Where it lands: only the branch `refs/heads/<its Gitea user>/<branch>`. The host builds that
|
||||
> name, the agent never does. A push never forces.
|
||||
> - Which remote: the agent's own fork of the project's upstream. The business file names the upstream,
|
||||
> and the host never contacts it. For each push the host builds the fork's address over https from
|
||||
> the upstream's host, port and repository name and the agent's own Gitea user. The history check,
|
||||
> the push and the credential match all use that address. So the agent's own user owns the receiving
|
||||
> repository on every push, and the agent never names it.
|
||||
> - The host refuses an upstream on a host or port other than the agent's Gitea, and an upstream the
|
||||
> agent's own user owns. It refuses a fork that is missing, empty or doesn't share the project's
|
||||
> history. Gitea's push-to-create stays off, so a push never creates a repository.
|
||||
> - Who may: the action routes under REQ-DEC-2 and is granted within-role to the coder role only. No
|
||||
> other role file grants it.
|
||||
> - Before version 1.7 the action was a hard wait under REQ-WAIT-3, so each push needed a decision
|
||||
> the human resolved, and that decision named the commit.
|
||||
>
|
||||
> Every push is logged with the remote, the ref, the commit and, when one was given, the decision.
|
||||
|
||||
Under "Credentials", after REQ-CRED-2:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-CRED-3.** An agent with a grant can run commands with sudo on another host. The agent asks
|
||||
> for the host credential by name. The stack checks that a grant exists for that agent and that host
|
||||
> before the credential is used. The agent never sees the key or any other secret value (REQ-CRED-1,
|
||||
> REQ-VAR-2). An agent without a grant for that host is refused. Not every agent gets a grant. Who
|
||||
> grants a host credential, and how, is set by the #424 design, which Jason approves.
|
||||
|
||||
REQ-WAIT-3, appended:
|
||||
|
||||
> `role.hire` and `role.fire` were added as hard waits and route under REQ-COS-3 since version 1.7.
|
||||
> `git.push.remote` was added as a hard wait and routes under REQ-LAND-2 since version 1.7.
|
||||
|
||||
## Notes for the reviewer and for Mos
|
||||
|
||||
- REQ-COS-1 (1.2) already says the chief "hires and terminates role instances". REQ-COS-3 gives that
|
||||
sentence its actions and bounds. It doesn't widen REQ-COS-1.
|
||||
- The forbid now names what r3 called "replace": ending a kept instance's run with a reason and changing
|
||||
its model (webby-0010 N2). A user-resolved decision still ends a kept run (row 194 D2.4).
|
||||
- The fixed list is `HIRE_ALLOWED` in row 194 D2.3, an allowlist since r4 (webby-0010 B1). r3's text
|
||||
named the refused authorities instead, which let merge-gate and any later action through.
|
||||
- The kill switch is row 191's. If row 191 hasn't landed when this version is approved, the bound still
|
||||
holds through row 194's chain on its candidate.
|
||||
- REQ-LAND-2 follows row 199's design r6 (4d3007ec9 on megavolt/row199, approved by Duckworth in
|
||||
duckworth-0027: D9 to D11, D9a, D9c, Q-M1 and Q-M2, and the oid binding from mos-1034 item 5) and
|
||||
Megavolt's wording in megavolt-0005, megavolt-0008 and megavolt-0014, through Sage (sage-1377,
|
||||
sage-1380, sage-1387). The fork bullet answers duckworth B5 and B6 and webby-0011 N1. Commit-email
|
||||
checks, the refusal codes and the URL parse rules (D9c) are host checks with tests in row 199, not
|
||||
bounds, so they stay out of the text (megavolt-0008).
|
||||
- Once the routing lands and the decision is optional for the within-role class, the commit is the task's
|
||||
latest recorded push at check time. It is still checked against the work ref tip and pushed by oid, so
|
||||
the binding holds without a decision (megavolt-0008). Step 4 of "When this version lands" puts that in
|
||||
the routing commit (duckworth-0022 N9, sage-1387).
|
||||
- REQ-LAND-2 and REQ-COS-3 meet at the coder. Coder declares Gitea credentials, so REQ-COS-3's
|
||||
credentials bound keeps the chief from hiring one until a later version lifts it. `git.push.remote` isn't
|
||||
on REQ-COS-3's fixed list either, so a coder holding it stays unhireable until a PRD version adds it
|
||||
(row 194 D2.3).
|
||||
- Adding to the fixed action list is a PRD change (mos-1035 item 3, sage-1395). The role file's
|
||||
`hireable` list stays a reviewed commit in `roles/`.
|
||||
- The credentials bound names no row number, so the text doesn't go stale when row 199 lands. A later
|
||||
version lifts it.
|
||||
- REQ-CRED-3 states only what Jason said in Q183 and Q184 (sage-1422). It names the outcome and the
|
||||
check, not how the stack reaches the host. Proxying the ssh and a signing socket are both designs, and
|
||||
the #424 design picks one (duckworth-0038 B1).
|
||||
- Open point: Q183 doesn't say who grants host access or how, so the text leaves it open and defines no
|
||||
mechanism. The #424 design settles it, Jason approves that design, and Duckworth reviews it because it
|
||||
gives agents sudo on hosts (Q184, Mos's reading, duckworth-0038 B2). The build is row 210 (#1707,
|
||||
owner Gizmoduck), a new row on top of row 199 (megavolt-0019, sage-1430).
|
||||
- This draft follows the PRD's own style: plain words, no semicolons in new sentences, and the status
|
||||
paragraph never edited in place except to append.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Megavolt
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage under mos-0974 and mos-0977 item 5 for MVP T1 item 6, the
|
||||
credential store per agent with its own git identity (#1697).
|
||||
|
||||
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 `megavolt/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,216 @@
|
||||
# Row 199 (#1697): per-agent credential store, each agent's own git identity, publish to its fork
|
||||
|
||||
Builder: Megavolt. Brief: `docs/plans/2026-10-20_agent-credentials.md`. Design: `DESIGN.md` r6 at
|
||||
4d3007ec9, approved by Duckworth in duckworth-0027 (REVIEW-r6.md at 640011518). Authority model ruled
|
||||
by Mos in mos-1027. Branch: megavolt/row199. Reviewer: Duckworth.
|
||||
|
||||
Status: candidate on megavolt/row199-on-190, rebased on origin/refactor 00d4cc1f6 (sage-1555). The
|
||||
full gate and the section 11 mutants ran in Sage's slot; results under "Runs".
|
||||
|
||||
## Commits so far
|
||||
|
||||
- 286bf598c business: agent identity, the publish grant, the project upstream with the D9c parse, store
|
||||
credential references; git.push.remote as a hard wait.
|
||||
- a2c387dba bus: broker publish authority (checkPublish, recordPublish, publishRefused, pushCommit on
|
||||
the raise); the D9c parse moves to `business/src/vocabulary.mjs`; pm.md and chief-of-staff.md list
|
||||
git.push.remote among the hard waits.
|
||||
- 609dc9364 bus: the per-agent credential store in `credentials.mjs`.
|
||||
- 504336797 bus: runtime wiring, the store root and the four publish entries in the op table.
|
||||
- a18fc6cf8 tasks: request calls use `useFor` keyed from the session; the start probe keeps `use`. The
|
||||
D5 test in `credential-store.test.mjs` pins the `use(` call sites to `credentials.mjs` (useFor's own
|
||||
call) and `tasks/src/adapter.mjs` (the host's @sync and start probe).
|
||||
- aa740cc71 bus: the lifecycle verbs `credentials.issue`, `credentials.revoke` and `credentials.delete`
|
||||
in `credential-verbs.mjs`, the broker branch, the runtime wiring; `organizationWriter` gets a
|
||||
`detail` on `write` and a receipt-only `record`.
|
||||
- 0cdace415 cli: `mosaic credentials status|issue|rotate|revoke|delete`, the scripts/mosaic route, docs.
|
||||
|
||||
## Deviations from DESIGN.md r6
|
||||
|
||||
1. **Identity is optional on an agent.** D8 reads as identity on every agent. Today's business files
|
||||
carry none, so identity stays optional and `publish` requires it (N10's load refusal). An agent
|
||||
without identity commits as E7 (T6).
|
||||
2. **Revoke of a `{file}` or `{env}` reference.** Section 6 describes revoke for store references.
|
||||
For a reference outside the store, revoke forgets the token in memory and never unlinks the file;
|
||||
the lifecycle verb then turns the business file's reference into `store`, so a restart finds
|
||||
nothing. The test: `credential-store.test.mjs`, "revoke of a {file} reference".
|
||||
3. **Issue order.** Issue writes the token file (tmp, fsync, rename, dir fsync), then runs the
|
||||
caller's commit (the business file's reference becomes `store`), then sets memory. A failed commit
|
||||
leaves the file on disk and the token out of memory, so the agent stays `not-issued` until the
|
||||
issue is run again. The test: "a failed commit leaves the token on disk but not in memory".
|
||||
4. **The D9c parse lives in `business/src/vocabulary.mjs`, not `business.mjs`.** The broker reads
|
||||
`forkProblem` and `forkUrl`, and `project-runtime.test.mjs` pins the broker's import closure to
|
||||
files that import nothing. `business.mjs` wraps `parseGiteaUrl` with the loader's refusal
|
||||
messages. The pin test names the two new imports.
|
||||
5. **`withGitea` takes the publish ticket, not a cap (D9b).** The key is the session checkPublish
|
||||
checked with `#agent` (a run holding its claim; never a tester, reader or human), read through
|
||||
`broker.publishSession(ticket)`. The token is usable only between checkPublish and recordPublish
|
||||
or publishRefused. Its refusals are filed once, by publishRefused on the task's trail, and not
|
||||
also by `refusing()`. The test: `publish.test.mjs`, "runtime: withGitea keys from the open ticket
|
||||
only".
|
||||
6. **Role contracts.** `workflow-roles.test.mjs` requires the hard waits listed in `roles/pm.md` and
|
||||
`roles/chief-of-staff.md` to equal GATED_ONLY. Adding git.push.remote to the vocabulary made it
|
||||
fail, so both lists now name git.push.remote. Prose only; no hash pin reads either file. Duckworth
|
||||
should review this hunk because `roles/` is reviewed authority.
|
||||
|
||||
7. **Delete takes a reason.** Section 6 lists `delete AGENT` with no reason. The broker requires
|
||||
`reason` on delete as on revoke, so every receipt that ends a credential says why.
|
||||
8. **Rotate is `credentials.issue`.** There are three bus verbs, not four. An issue over an entry the
|
||||
bus holds is recorded as `rotate` in the receipt and the answer. The CLI's `rotate` reads the
|
||||
status first and refuses before reading stdin when nothing is issued.
|
||||
9. **Issue covers entries the bus loaded.** The business file must already name the kind for the
|
||||
agent, and the bus must have loaded that reference at start; otherwise `credential-unknown`. An
|
||||
agent added after the bus started needs the restart row 165 already asks for. Row 194's human
|
||||
`create` is where a new agent's `store` references come from.
|
||||
10. **No Console page yet.** Section 6 puts the verbs in the Console's agent page too. This branch has
|
||||
the bus verbs and the CLI. The Console page is a separate piece (packages/webui, its `BUS_VERBS`
|
||||
and `STATUS` pins); I raise the split with Sage before the manifest. Ruled in sage-1421: the
|
||||
Console page is out of row 199 and becomes a follow-up row with Webby as reviewer. The manifest
|
||||
holds bus, business, tasks, cli and seat.
|
||||
11. **The push runs in the bus child, and the git socket polls for it (D9b).** The git socket runs in
|
||||
the host process. The broker, the runtime and the token are in the bus child that
|
||||
`packages/cli/src/host.mjs` forks, and host ops cross that IPC one at a time with a 10 s answer
|
||||
limit. ls-remote and the push can run longer than that, so `withGitea(ticket, fn)` can't wrap a
|
||||
host-side git. The split: the socket runs D9 steps 3 and 4 (tip and identity under the lock,
|
||||
`working.publish`), then op `publishPush`, which starts `publishRemote` in the bus child under
|
||||
`withGitea` and answers `{state: 'running'}`. The socket polls op `publishResult` every 200 ms
|
||||
until it reads `pushed` or `refused`. Fork, user, source and destination come from the ticket
|
||||
(`broker.publishTarget`), never from the host's message. `recordPublish` also refuses `ref-moved`
|
||||
when the pushed ref isn't the ticket's destination. A second `publishPush` on a running ticket
|
||||
refuses `publish-busy`. The token reaches git on fd 3 only (`runGit` option `pass`), read once
|
||||
by a one-shot credential helper that answers `get` for https on the fork's exact host and port.
|
||||
On close, a running publish's git isn't awaited; git's own time limit ends it, and the ticket
|
||||
goes with the process. The 10 s IPC limit stays as it is (sage-1440). The poll has a bound
|
||||
(sage-1440): `publishDeadlineMs`, by default four times git's limit and grace (publishRemote runs
|
||||
at most four git commands) plus 30 s. Past it the socket refuses `publish-timeout`, filed once by
|
||||
publishRefused, and logs that the bus process's push may still reach the fork. A failure that is not a refusal is filed as `git-failed` and logged on
|
||||
the bus's stderr by error name and code only. Tests: `seat/tests/publish.test.mjs` (11),
|
||||
`bus/tests/publish.test.mjs` (the process ops), `cli/tests/publish-socket.test.mjs` (7, end to
|
||||
end through the socket, the in-process runtime, the store and a TLS fake Gitea; one is the
|
||||
publish-timeout outcome).
|
||||
12. **No harness tool for publish.** `packages/harness/src/git-tools.mjs` has push, fetch and land
|
||||
clients. A runner tool that calls the socket's `publish` op is in packages/harness, outside the
|
||||
manifest scope sage-1421 set. Today a publish needs a socket client; the tool is a follow-up.
|
||||
13. **The bind rule's `git.push.remote` clause has no test of its own.** D11 adds it and the
|
||||
launcher has it. A role can publish only a push its own role made (`work-not-yours`), and
|
||||
`recordPush` needs `git.push.working`, which already binds the socket. No role reaches the
|
||||
clause alone, so dropping it changes no outcome today. It matters once a role can publish work
|
||||
it didn't push. Ruled in sage-1440: keep it; its mutant goes in the section 11 list as
|
||||
equivalent, with this reasoning, and Duckworth judges it.
|
||||
14. **A push between checkPublish and the git push refuses; it never lands a different commit.**
|
||||
T3 says such a push "doesn't change the commit that lands". Under the hard wait it refuses:
|
||||
`working.publish` checks the work ref's tip against the ticket's source under the lock
|
||||
(`ref-moved`, `seat/tests/publish.test.mjs`), and `recordPublish` checks `pushCommit` again
|
||||
(`decision-mismatch`, `bus/tests/publish.test.mjs` "r4 B4"). The fork gets only the ticket's
|
||||
oid either way. N9's routing-commit test still applies. Ruled in sage-1440: T3's wording in
|
||||
DESIGN.md now reads "refuses with `ref-moved` or `decision-mismatch`", and an end-to-end test
|
||||
asserts each code mid-flight in `cli/tests/publish-socket.test.mjs`: a push after checkPublish
|
||||
refuses `ref-moved` with no request to the fork; a push recorded while the bus process pushes
|
||||
refuses `decision-mismatch`, filed once, and the fork holds only the ticket's oid.
|
||||
|
||||
15. **Fix: rotate on a due gitea key (gizmoduck-0050, sage-1494).** The CLI's `ISSUED` set
|
||||
(`packages/cli/src/credentials.mjs`) listed valid, expiring and expired, but the bus's `status()`
|
||||
gives a gitea key past its rotate-by date `rotation-due`. So `credentials rotate` refused the one
|
||||
case it exists for. `ISSUED` now names every state status() gives a held token. The test:
|
||||
`cli/tests/credentials.test.mjs`, "rotate goes ahead on each held state". Its mutant (the old set)
|
||||
fails that test and goes in the section 11 list.
|
||||
|
||||
16. **The bus process refuses a boot config carrying `publishTls` (M26, sage-1563).** process.mjs
|
||||
used to drop `publishTls` from the boot config before `startBroker`, and no test could see the
|
||||
drop. The host's boot config (`packages/cli/src/config.mjs`) never carries it; it is the tests'
|
||||
scratch CA for an in-process runtime only. A boot that carries it now refuses
|
||||
`invalid-host-request` and the process exits 2, rather than trusting it or dropping it quietly.
|
||||
The test: `bus/tests/publish.test.mjs`, "the bus process answers the publish ops from their own
|
||||
lines". Every test file that reads process.mjs ran singly, green (see "Runs").
|
||||
17. **M17 is equivalent: `#authorityFor`'s pushCommit comparison is also made by `#publishFacts`.**
|
||||
A git.push.remote raise with no recorded push refuses `no-push` (`broker.mjs` raise,
|
||||
`pushCommit = ... ?? fail('no-push')`), so every publish decision carries a pushCommit.
|
||||
`#publishFacts` takes `source = result.pushCommit` and refuses `decision-mismatch` when it isn't
|
||||
`#latestPush(task_ref).commit`, the same two values `#authorityFor` compares. The check stays as
|
||||
defense in depth, for a decision whose context lacks pushCommit, which no code path writes today.
|
||||
Duckworth judges it, as with M27.
|
||||
|
||||
## N9, for the routing commit (sage-1388)
|
||||
|
||||
Today git.push.remote is a hard wait, so every publish carries a decision, and the decision's
|
||||
`pushCommit` fixes the commit. If a later commit routes git.push.remote within role, a publish has
|
||||
no decision and no `pushCommit`. `#publishFacts` then takes the commit from `#latestPush`, and the
|
||||
git socket's tip check under the lock is the only thing that holds it. That commit needs a test
|
||||
that a push between checkPublish and the git push doesn't change what lands.
|
||||
|
||||
## R1, the backup set
|
||||
|
||||
Checked at 504336797. Row 185's `backup_store` (`scripts/stack-release.sh:372-392`) copies only
|
||||
`<dataRoot>/bus/bus.sqlite`, plus its sums, modes, listing and stamps, into
|
||||
`<dataRoot>/backups/stack/<activation>/`. Nothing under `<configDir>` is copied, so
|
||||
`<configDir>/secrets/agents` isn't in the backup set. `rehearse` copies the live business file
|
||||
through `R_BUSINESS_JS` (`:1268-1288`). Its `swap` replaces every credential reference, a `store`
|
||||
one included, with a placeholder file in the rehearsal's own dir. The guard then refuses
|
||||
`credential-ref-live` unless every reference is such a placeholder (`:1329-1337`), so a rehearsal
|
||||
never reads a store token. A backup tool outside this repository that copies `<configDir>` would
|
||||
copy tokens. That is R1's open half, and it is Jason's to check on each VM.
|
||||
|
||||
## T1 limit
|
||||
|
||||
A wrong owner on a store dir or file can't be set up without root, so T1 covers modes, symlinks and
|
||||
dirs. The owner check (`st.uid !== process.getuid()`) is in `privateDir` and `#read`, and is untested.
|
||||
|
||||
## Runs
|
||||
|
||||
Single files, each green on its commit: bus broker, decision-close, decision-route, decisions,
|
||||
hard-waits, land, project-runtime, launch-notice, review, publish, credential-store, credentials,
|
||||
credential-status, limits, tester, plugins-verb, routes-verb, limits-runtime, instances-verb, web,
|
||||
usage-runtime, limit-checks, daily-quota-runtime, chief-of-staff, tasks, tester-writes; every
|
||||
business test file. Since: credential-verbs (2/2), credential-store (11/11, with D5), each tasks file,
|
||||
tester, credential-status, plugins-verb, routes-verb, instances-verb; business launch-settings,
|
||||
plugin-settings, instance-settings, token-limit-settings; cli credentials (3/3), cli, app,
|
||||
tester-reads, tester, other-tree, transport, instances, limits, verbs. Hash pins: none on a changed
|
||||
file (`thread-migration.test.mjs:46` pins DDL only).
|
||||
|
||||
Publish, single files, each green before its commit: seat
|
||||
checkout (77), working (26), publish (11), landing (8); bus process (8), land (17), publish (12),
|
||||
credential-verbs (2); cli publish-socket (4), git-socket (24), launcher (29); harness tools (22).
|
||||
Hash pins: none on a changed file.
|
||||
|
||||
sage-1440 changes (the poll bound, the mid-flight tests), single files, green before the commit: cli
|
||||
publish-socket (7), git-socket (24), launcher (29). Hash pins: none on a changed file.
|
||||
|
||||
The rotation-due fix, single files: cli credentials (4), green; the old-set mutant fails it 3/1.
|
||||
Hash pins: none on a changed file.
|
||||
|
||||
Slip, 2026-10-20 about 19:51Z: I ran `node --test packages/tasks/tests/*.test.mjs` in one command,
|
||||
which is a package suite, without asking Sage for the slot. It passed, 97 pass and 0 fail, in under a
|
||||
minute. Reported to Sage in megavolt-0017.
|
||||
|
||||
Rebase (sage-1555): the 21 commits from megavolt/row199 sit on origin/refactor 00d4cc1f6 as the new
|
||||
branch megavolt/row199-on-190 (no force-push). `git range-diff` shows all 21 patches as `=`; the
|
||||
merge of 12c85ada1 flattened with an empty remerge-diff.
|
||||
|
||||
Full gate, Sage's slot, on c6868ba41 (base 00d4cc1f6), logs started 2026-10-20T22:55:44Z: bus 476
|
||||
pass 0 fail; business 191/0; tasks 97/0; cli 522/0 (472 s); seat 163/0; scripts/test-queue.sh
|
||||
156/0; test-auth, test-conductor, test-config, test-discord, test-extension-package,
|
||||
test-foundation, test-land-work, test-release and test-task each exit 0. The test-task compose run
|
||||
left the network megavolt-row199_default with 0 containers; I removed it.
|
||||
|
||||
Section 11 mutants, batch 1, on c6868ba41, 2026-10-20T23:09:03Z to 23:09:56Z, restores verified and
|
||||
packages/ clean: 22 of 28 killed; M27 survived, expected equivalent (deviation 13). Five expected
|
||||
kills survived, each a test gap: M05 (the token in the push URL), M06 (revoke unlinks before it drops
|
||||
memory), M12 (http.followRedirects=false dropped), M17 (#authorityFor's pushCommit check dropped)
|
||||
and M26 (publishTls reaches startBroker). Launchpad's stack-release single file ran alongside from
|
||||
23:09:28Z, so batch 2 reruns all 28 (sage-1563).
|
||||
|
||||
Fixes, each checked by its mutant alone: 5fa7b2553 seat publish, a test reads git's argv through a
|
||||
PATH shim and finds no token and no userinfo (M05 killed); the 302 test's target shares the CA git
|
||||
trusts, so only http.followRedirects=false keeps git off it (M12 killed). 116eeec48 bus,
|
||||
credential-store: revoke's unlink fails (dir 0500) and the token is already out of memory (M06
|
||||
killed); process.mjs refuses publishTls, deviation 16 (M26 killed, its anchor is now the refusal
|
||||
line). M17 is equivalent, deviation 17. Single files after the fixes, each green: seat publish (12),
|
||||
bus credential-store (12), bus publish (12), bus process (8), and every other file that reads
|
||||
process.mjs: bus credential-status (4), limit-checks (16), land (17), tasks (9), end-launch (7); cli
|
||||
trackers-boot (1), thread-launch (11), publish-socket (7). Hash pins: none on a changed file.
|
||||
|
||||
Section 11 mutants, batch 2, all 28, on 7c3cdd940 in Sage's slot (sage-1565), 2026-10-20T23:17:59Z to
|
||||
23:18:48Z, restores verified and packages/ clean: 26 killed; M17 and M27 survived, both expected
|
||||
equivalent (deviations 17 and 13). Each of the five batch 1 survivors but M17 now fails its test. Then
|
||||
pkg-bus 477 pass 0 fail, pkg-seat 164/0, pkg-cli 522/0 (461 s), ending 23:26:40Z. No other seat's run
|
||||
was active: the busy check said CLEAR before the batch started. Logs:
|
||||
/mnt/storage/scratch/megavolt-r199/batch2-20261020T231650Z/.
|
||||
@@ -0,0 +1,416 @@
|
||||
# Row 199 design: a credential store per agent, own git identity, pushes to a remote
|
||||
|
||||
Megavolt, 2026-10-20. Gitea #1697, MVP T1 item 6. Brief: `docs/plans/2026-10-20_agent-credentials.md`.
|
||||
Reviewer Duckworth. Mos rules on the authority model (section 9) before any code. Base: refactor at 992650232.
|
||||
No credential value was read to write this. Evidence is code and docs only.
|
||||
|
||||
r2 answers duckworth-0013 (review at 6ab9cd927 on duckworth/reviews): B1 in D3a, T1 and T5, B2 in D9 step 5
|
||||
and T3, N1 in section 6 and Q-M3, N2 in section 6, N3 in D9 step 3, N4 in D9a and T3, N5 in E5 and D9b.
|
||||
|
||||
r3 records Mos's rulings (mos-1027) in section 9. The Q-M2 hard wait changes D9 step 1, D9b and T3. Q-M1
|
||||
drops O2 (D10, A3, R2).
|
||||
|
||||
r4 answers duckworth-0017 (REVIEW-r3.md at 4065ed59f): B3 in T3 and D9 step 5, B4 in D9 steps 1, 3 and 5, N6
|
||||
and N7 in D9a, N8 in D9 step 1.
|
||||
|
||||
r5 takes webby-0011 N1 (row 194 review, through honker-0009): the host checks at each push that the
|
||||
remote's owner is the agent's own Gitea user. r4 named one `remote` per project, which can't be a fork per
|
||||
agent (O1) once two agents publish the same project. So `projects.<p>.remote` now names the upstream
|
||||
repository, and the host builds each agent's fork URL from it (D9 step 2, D10, T3). It also fixes a conflict
|
||||
found while building: D8 made `identity` required for every agent whose definition declares `gitea`, while D9
|
||||
step 2 says today's files load unchanged. Coder declares `gitea`, so r5 makes `identity` optional and
|
||||
`publish` require it (D8).
|
||||
|
||||
r6 answers duckworth-0026 (REVIEW-r5.md at 3cb6888e1), with Mos's acceptance (mos-1043). B5: the derived URL
|
||||
has one name, `fork`, and D9a's `ls-remote`, step 5's push, the helper's host match, `checkPublish`'s result
|
||||
and step 6's event all use it (D9, D9a, D9b, T3). B6: the URL parse is written down (D9c) and tested (T3).
|
||||
N10 in D8 and T3. N11 in D9a and A3. N9 is carried to BUILD.md for the routing commit.
|
||||
|
||||
## 0. Terms
|
||||
|
||||
- **Stack instance**: one Mosaic Stack deployment (row 190). It has one OS user, one config dir
|
||||
`~/.config/mosaic-NAME` (here `<configDir>`), one data root and one bus. Case 1 and Case 2 are two stack
|
||||
instances on two VMs (DECISIONS Q180).
|
||||
- **Agent**: a role instance in a business file (`roles.<agent>`). Today's code calls it "instance". This
|
||||
design says agent.
|
||||
- **Kind**: a credential type: `gitea`, `vikunja`, `model`.
|
||||
- **Host**: the bus process and its git socket, running as the stack instance's OS user, outside any sandbox.
|
||||
|
||||
## 1. What exists (measured at 992650232)
|
||||
|
||||
| Code | Fact | Where |
|
||||
|---|---|---|
|
||||
| E1 | The business file references credentials per agent: `roles.<agent>.credentials.<kind> = {file or env, rotateBy or expires}`. The kinds an agent may hold are the ones its role definition lists | `business/src/business.mjs:89-111`, `credentials.mjs:18-38`, `role.mjs:105-118` |
|
||||
| E2 | The bus reads each token once at start (O_NOFOLLOW, own uid, 0600, at most 16 KiB, outside the repos and data root) into a private map keyed `<business>/<agent>:<kind>` | `bus/src/credentials.mjs:18-89`, `bus/src/runtime.mjs:78-89` |
|
||||
| E3 | Only Vikunja uses a token: `credentials.use("<business>/<agent>", "vikunja", ...)`. Gitea tokens are loaded and never used. `assertClean` refuses any event that contains a token | `tasks/src/adapter.mjs:70`, `runtime.mjs:134`, `broker.mjs:560` |
|
||||
| E4 | Agents run in bwrap as the bus's own uid. `~/.config`, `~/.mosaic` and `~/.ssh` are not bound. The git socket is bound only for `git.push.working`, `review.verdict` or a landing | `seat/src/sandbox.mjs:21-30, 104-124`, `cli/src/launcher.mjs:401` |
|
||||
| E5 | A session proves who it is with a 64-hex cap the broker mints and maps to a frozen session record. The git socket resolves the cap to (business, role) before anything else | `broker.mjs:280, 379-383`, `packages/cli/src/git-socket.mjs:398-407` |
|
||||
| E6 | The model credential is one env file per host (`EnvironmentFile=-%h/.config/mosaic-dev/secrets/claude-session.env`). `ENV_ALLOW` passes it into every sandbox, so every agent holds the same model token | `cli/systemd/mosaic-bus.service.in:24`, `seat/src/session.mjs:83-94` |
|
||||
| E7 | Commits in a session's clone use `<agent>@<business>.mosaic.invalid`. Landed squashes use "Mosaic bus host" | `sandbox.mjs:151-157`, `working.mjs:280-281` |
|
||||
| E8 | No code pushes to a remote. The git socket pushes bundles into a local bare working repo with no remote and no credential | `working.mjs:1-14, 198-238` |
|
||||
| E9 | Adding or changing an agent refuses a definition that declares credentials (`organization-credentials-out-of-scope`). Binding them is a hand edit. Row 194's design keeps that refusal for create and hire "until the credentials row" | `business/src/instance-settings.mjs:20-29`, row 194 DESIGN.md D1 |
|
||||
| E10 | The development repository's helper pushes as one identity from one JSON file | `scripts/git-credential-mosaic.mjs` |
|
||||
|
||||
So per-agent references and a host-only reader already exist for two kinds. What's missing: a fixed place for
|
||||
the files, an agent identity, any remote push, lifecycle verbs, and a model credential that agents don't
|
||||
share.
|
||||
|
||||
## 2. The store (brief item 1)
|
||||
|
||||
D1. **Layout.** Each stack instance keeps its agents' secrets under its own config dir:
|
||||
|
||||
```
|
||||
<configDir>/secrets/agents/ 0700, owner = the stack instance's OS user
|
||||
<configDir>/secrets/agents/<business>/<agent>/ 0700
|
||||
<configDir>/secrets/agents/<business>/<agent>/<kind> 0600, the token only, one line
|
||||
```
|
||||
|
||||
This is row 190's secrets root (`<configDir>/secrets/...`, as Q175's slot uses). The token file keeps E2's
|
||||
format, so the bus reader doesn't change. Nothing in a token file is a name, an email or a URL. Those are not
|
||||
secret and go in the business file (D6), where review sees them.
|
||||
|
||||
D2. **The business file reference.** A new reference form, `{"store": true, "rotateBy" | "expires": DATE}`,
|
||||
means "the file at the layout path for this business, agent and kind". The path is derived, never written,
|
||||
so an agent's entry can't name another agent's file. The old `{file}` and `{env}` forms stay valid for
|
||||
migration (section 7). A `{file}` reference that points inside `<configDir>/secrets/agents/` but not at its
|
||||
own agent's path refuses `credential-not-yours` when the business file loads.
|
||||
|
||||
D3. **Read path.** Unchanged in shape: the bus reads every entry once at start (E2) and on a store change
|
||||
(section 6), and holds the values in memory. No sandbox binds `<configDir>` (E4 already holds). No token goes
|
||||
into a sandbox for `gitea` or `vikunja`. The agent asks the bus to act, and the bus uses the token.
|
||||
|
||||
D3a. **A `store` reference with no file.** ENOENT on a `store` path loads as an absent entry. `status` shows
|
||||
`not-issued`, and `useFor` refuses `credential-unavailable`. The bus still starts. This is the state after a
|
||||
create without `issue` (D13) and after `revoke` (section 6). Every other defect still refuses start: wrong
|
||||
owner or mode on the file or a dir, a symlink, a non-regular file, bad format, a missing or wrong-mode
|
||||
`secrets/agents/` dir. `{file}` and `{env}` keep E2's behavior, where a missing file refuses start.
|
||||
|
||||
## 3. The "RLS equivalent" (brief item 1)
|
||||
|
||||
Three layers. Each one is enforced by code or the OS, not by prose.
|
||||
|
||||
D4. **OS, between stack instances.** Files are 0600 and dirs 0700, owned by the stack instance's own OS user.
|
||||
Case 1's user can't read Case 2's store (Q180: each instance under its own user without sudo). The bus refuses
|
||||
to start when any file or dir under `secrets/agents/` has the wrong owner or mode (`credential-file`, as E2
|
||||
does for files today, extended to the dirs).
|
||||
|
||||
D5. **Bus, between agents of one stack instance.** The principal for every credential use comes only from the
|
||||
verified cap (E5). The agent-facing API is `credentials.useFor(session, kind, op)`: `session` is the broker's
|
||||
frozen record, and the key is built from `session.business` and `session.role`. It has no agent argument, so
|
||||
there is nothing in a request to point at another agent. The existing `use(name, kind, op)` stays for the
|
||||
host's own `@sync` entry and is not reachable from a request. The tasks adapter's per-role calls (`ctx.as`,
|
||||
`verbs.mjs:126`) move to `useFor`. Any request field that names an agent other than the caller (`as`,
|
||||
`agent`, `instance`, `role` on a credential path) is refused `credential-not-yours` and recorded through
|
||||
`broker.refuse()` as `action.refused`, with the caller's cap and the task ref. A build test greps every
|
||||
`credentials.use(` call site and fails on a new one outside the allowlist.
|
||||
|
||||
D6. **What stays open.** A process of the same OS user outside the sandbox can read every agent's file. That
|
||||
is the same limit as the human proof (P19 333). Q180's per-user VMs move the boundary to "one stack instance
|
||||
per OS user". Agents of one instance are kept apart by the sandbox and D5, not by the OS.
|
||||
|
||||
## 4. Kinds (brief item 2)
|
||||
|
||||
| Kind | 10-27 | Use |
|
||||
|---|---|---|
|
||||
| `gitea` | yes | Git push to a granted remote (section 5). Gitea API verbs (issues, pull requests) wait. The factory's review flow runs in the working repo today, and no 10-27 case needs an agent to open a pull request |
|
||||
| `vikunja` | yes, exists | Moves to `store` references and `useFor`. No behavior change |
|
||||
| `model` | per stack instance only | See D7 |
|
||||
|
||||
D7. **Model provider.** A model token has to sit inside the sandbox today, because the harness calls the
|
||||
provider itself (E6). Per-agent model tokens need a proxy that holds the real token and gives each run its own
|
||||
short-lived one, which is the CLIProxyAPI line in TARGET.md. That waits for its own row. For 10-27 the model
|
||||
credential is per stack instance: row 190's `<configDir>/secrets/claude-session.env`, owned by that
|
||||
instance's OS user. It is never a copy of jwoltje's file. Jason signs in on each VM user himself (A6). Whether
|
||||
Case 1 and Case 2 run on Jason's existing subscription or need a new account is Jason's call (Q-J1). This row
|
||||
doesn't pick. A pi model credential also waits for P19 #383.
|
||||
|
||||
## 5. Git identity and push (brief item 3)
|
||||
|
||||
D8. **Identity.** A new business-file key, `roles.<agent>.identity = {name, email, gitea: {url, user}}`. It is
|
||||
optional, because today's business files with a `gitea` agent (coder) must load unchanged. `publish` (D9
|
||||
step 2) requires it, and requires that the definition declare `gitea`. A business file with a `publish`
|
||||
grant on an agent that lacks either refuses at load, and the error names `roles.<agent>.publish` and says
|
||||
"needs identity and a gitea definition" (N10). Load is the only place this is checked: a business file that
|
||||
fails it never reaches the bus. The sandbox sets `GIT_AUTHOR_*` and `GIT_COMMITTER_*` from
|
||||
it in place of E7's invalid domain. The email is the one Jason gives the agent's Gitea user, so Gitea links the
|
||||
commits to that user. Agents without `gitea` keep E7.
|
||||
|
||||
D9. **Push to a remote: a new git socket op, `publish`.** No new socket, no bundle, and no token in the sandbox.
|
||||
|
||||
```
|
||||
publish {"cap", "op": "publish", "task_ref", "project", "branch", "decision"}
|
||||
-> {"ok": true, "result": {"fork", "ref", "commit", "previous"}} or a refusal
|
||||
```
|
||||
|
||||
The host checks, in order, and each refusal goes through `broker.refuse()`:
|
||||
|
||||
1. Identity from the cap (E5), then the broker's `#checkAuthority(s, "git.push.remote", {decision, target:
|
||||
task_ref})`, as `checkLand` does for a merge (`broker.mjs:907-913`). `git.push.remote` is a new action in
|
||||
ACTIONS and is not in ROUTED (REQ-WAIT-3, `packages/business/src/vocabulary.mjs:52-58`,
|
||||
`broker.mjs:42-69`), so it is a hard wait and classifies `gated` for every role. Every publish therefore
|
||||
needs a decision the human resolved for this task ref. A decision is spent once (`decision-consumed`).
|
||||
|
||||
**The decision binds one commit.** `#authorityFor` binds action, role, thread and target only
|
||||
(`broker.mjs:769-798`), so the commit needs its own carrier. The raise records it the same way it records
|
||||
`holderRun` for `role.revoke` (`broker.mjs:1733`). When the action is `git.push.remote`, the
|
||||
`decision.raise` event body gets `pushCommit`. That is the commit of `#latestPush(business, target)` at
|
||||
raise time, read by the broker and never taken from the agent. A raise with no recorded push for the task
|
||||
refuses `no-push`. The decision's question shown to the human ends with that commit. `#authorityFor` then
|
||||
refuses `decision-mismatch` when `context.pushCommit` differs from the commit of `#latestPush` now, which
|
||||
is the same check `holderRun` gets at `broker.mjs:796`. So the sequence approve at A, push B, publish B
|
||||
refuses.
|
||||
No role definition grants the action yet. When Jason's PRD version routes it (Sage, Vikunja #420), a
|
||||
reviewed commit adds it to both ROUTED lists and to coder's `withinRole`. From then on a coder publishes
|
||||
without a decision, and `decision` is optional for a within-role class only.
|
||||
2. The project is in the agent's grant, `roles.<agent>.publish: [project]`, and the business file names the
|
||||
project's upstream repository, `projects.<project>.remote: "https://HOST/OWNER/REPO"`
|
||||
(`remote-not-granted`). Both URLs pass D9c's parse at load. The upstream's host and port equal
|
||||
`identity.gitea.url`'s (`remote-host-mismatch`). The host never contacts that URL. It builds the agent's
|
||||
fork, `fork = https://HOST[:PORT]/<gitea.user>/REPO`, from the upstream's origin and repository name and
|
||||
the agent's own `gitea.user`. `fork` is the only URL the rest of `publish` uses: D9a's `ls-remote`, step
|
||||
5's push, the helper's host match, `checkPublish`'s result and step 6's event. So the owner of the
|
||||
repository that receives a push is the agent's own Gitea user on every push, by construction, and the
|
||||
agent can't name it (webby-0011 N1). D9c is what makes "by construction" hold. Gitea's refusal of a
|
||||
push to a repository the user doesn't own or collaborate on is the second layer. An upstream whose owner
|
||||
equals the agent's `gitea.user`, compared without case, refuses `remote-not-fork`, at load and at push. `remote` is a new key
|
||||
beside `root` and `repo` (`business.mjs:186`), and `identity` and `publish` are new keys on a role entry
|
||||
(`business.mjs:89`). All three are optional, so today's business files load unchanged.
|
||||
3. The source is one commit, `C = context.pushCommit`. It is checked three ways, so all three are equal:
|
||||
the decision's commit, the commit of `#latestPush`, and the work ref's tip in the working repo,
|
||||
`work/vikunja-<project>-<task>` (`working.mjs:110-114`). The tip is read under the repository's lock, and
|
||||
if they differ the push refuses `ref-moved`. The work ref is per task, not per agent. Two checks make it the
|
||||
caller's: `#latestPush` was recorded by the caller's role (`work-not-yours`), and step 4. The destination is always `refs/heads/<gitea.user>/<branch>`, built by the host.
|
||||
`validBranch(branch)` holds (`invalid-request`). The agent never names the destination ref.
|
||||
4. Every commit in `main..<work ref>` has author and committer email equal to `identity.email`
|
||||
(`identity-mismatch`).
|
||||
5. The push runs through `credentials.useFor(session, "gitea", ...)`: `git push <fork> <C>:<dst>`, by oid,
|
||||
with no `+` and no `--force`. It never pushes by ref name, so a push to the working repo after
|
||||
`checkPublish` can't change what goes out (`non-fast-forward` from git stays a refusal). Git runs with E8's SAFE flags
|
||||
(`working.mjs:92-98`), which set `protocol.allow=never`. `publish` adds exactly two flags,
|
||||
`protocol.https.allow=always` and `http.followRedirects=false`, plus `GIT_TERMINAL_PROMPT=0` and no global
|
||||
or system config. The token reaches git through a one-shot credential helper that reads it from an
|
||||
inherited pipe, never through argv, env, the URL or a file. Seat `gitEnv` (`checkout.mjs:79-89`) takes only
|
||||
PATH from the host and sets the rest itself, so no proxy or `GIT_SSL_*` variable reaches git. `publish` takes a `tls` parameter that
|
||||
defaults to `[]`, the same way `push` takes `fsck = FSCK` (`working.mjs:100`). Tests pass
|
||||
`["-c", "http.sslCAInfo=<scratch CA>"]`. The host never sets it, and nothing in config or env reaches it. The helper answers `get` only when `protocol`
|
||||
is `https` and `host`, port included, equals `fork`'s. It writes nothing for any other `get` and for
|
||||
`store` and `erase`. `assertClean` checks git's output, so a fork that echoes the token gets
|
||||
`credential-leak`.
|
||||
6. The bus records `action.allowed` for `git.push.remote` with fork, ref and commit.
|
||||
|
||||
D9a. **The agent's fork must share root's history.** A push sends every object the fork lacks. The working
|
||||
repo's `main` is the project root's history (`working.mjs`, seed), so a fork that doesn't share it would
|
||||
receive all of root, landing squashes included. The check runs on `fork`, never on the upstream: a related
|
||||
upstream says nothing about what the agent's `<user>/REPO` holds.
|
||||
`publish` runs `seed` first (`working.mjs:164-176`), under the repository's lock as `push` does, so `main`
|
||||
catches up with root before the check below. That means a fork also receives any landed root commits that
|
||||
upstream doesn't have yet, because they sit under the agent's work. That is expected: the agent's branch is
|
||||
built on them.
|
||||
Before the push, the host runs `git ls-remote --symref <fork> HEAD` (same flags and helper). It refuses
|
||||
`remote-unrelated` when the fork is empty or when that tip isn't a commit the working repo already has and an
|
||||
ancestor of `main`. When `ls-remote` exits non-zero, for a fork that doesn't exist, a refused token or a
|
||||
network failure, `publish` refuses `fork-unavailable` and the push doesn't run (N11). Git's stderr isn't
|
||||
parsed to tell those apart and isn't returned, since it can carry the URL. Only `ls-remote` crosses the
|
||||
network for this, so nothing is fetched from the fork. If the fork has moved past root, the refusal stands
|
||||
until root catches up. That is a human pull into root, not an agent's.
|
||||
|
||||
D9c. **The URL parse (B6).** `projects.<p>.remote` and `identity.gitea.url` are parsed with `new URL()` at load,
|
||||
and the business file refuses to load unless all of these hold:
|
||||
|
||||
- The scheme is `https:`. There is no username, password, query or fragment.
|
||||
- The string equals its own normalized form: for the upstream, `url.origin + url.pathname`; for
|
||||
`gitea.url`, `url.origin + "/"` or `url.origin`. So a dot segment (`/<user>/../OWNER/REPO`), a
|
||||
percent-encoded dot, an upper-case host, an explicit `:443`, a backslash, userinfo, a query and a fragment
|
||||
each refuse, because `new URL()` would change them (checked with node: each differs from its normalized
|
||||
form). A trailing slash survives normalization and is refused by the two-segment rule below. The checks below run on that path, so the owner that is checked is
|
||||
the owner that is used.
|
||||
- The upstream's path is exactly `/OWNER/REPO`: two non-empty segments, no more and no fewer. One trailing `.git` on
|
||||
REPO is allowed and stripped. `fork` is built from the stripped REPO.
|
||||
- `OWNER`, `REPO` and `gitea.user` each match Gitea's name rule, `^[A-Za-z0-9_.-]{1,100}$` and not `.` or
|
||||
`..`. REPO after stripping must still match.
|
||||
- `gitea.url`'s path is `/`. A Gitea served under a sub-path isn't supported by this row and refuses at load.
|
||||
- Host and port are compared after `new URL()` normalized them, and an omitted port counts as 443 on both
|
||||
sides.
|
||||
- Gitea compares user and repository names without case (`lower_name`), so `remote-not-fork` compares the
|
||||
upstream's OWNER with `gitea.user` without case. An upstream owned by `Coder` and a `gitea.user` of `coder`
|
||||
would otherwise build a "fork" that is the upstream itself.
|
||||
|
||||
D9b. **How `publish` reaches the broker and the store.** The git socket runs in the bus process
|
||||
(`cli.mjs:504-506`) and reaches the broker only through the host's op table (`bus/src/runtime.mjs:222-246`).
|
||||
`publish` adds three entries there, each wrapped in `refusing()`:
|
||||
|
||||
- `checkPublish(cap, args)`: steps 1 to 3, including the decision check. It returns `{ticket, fork, user,
|
||||
email, source}` and no token. The ticket holds the session and the decision until `recordPublish`, the
|
||||
same way `checkLand` holds a landing.
|
||||
- `withGitea(cap, fn)`: `credentials.useFor(broker.session(cap), "gitea", fn)`. `fn` gets the token and runs
|
||||
step 5. The token never leaves that call.
|
||||
- `recordPublish(ticket, result)`: step 6, and it spends the decision.
|
||||
|
||||
The op table gets no entry that returns a token.
|
||||
|
||||
D10. **Which refs.** Our side: only `refs/heads/<gitea.user>/*`, set by the host (D9 step 3). The default
|
||||
branch and other agents' prefixes can't be named. Gitea side, two options (Q-M1):
|
||||
|
||||
- **O1. A fork per agent. Ruled by Mos (mos-1027).** The agent's Gitea user has read on the upstream repo and owns a fork
|
||||
named like it. The host derives the fork's URL from the upstream and the agent's user (D9 step 2).
|
||||
Its token can write only repos the user owns or collaborates on, so Gitea itself stops any push to upstream
|
||||
or to another agent's fork. No branch rules are needed per agent, so none can drift. The cost: work reaches
|
||||
upstream as a pull request from the fork, opened by a human or a later API verb.
|
||||
- **O2. One shared repo. Not chosen.** The agent is a Write collaborator. Upstream's default and release branches get
|
||||
branch protection with no agent on the push list, plus a rule `<user>/*` per agent. Gitea doesn't stop agent
|
||||
A from creating an unprotected name outside `A/*` unless a catch-all rule outranks the per-agent ones. That
|
||||
depends on Gitea's rule priority, which I haven't verified on our version (probe P1). Every hire adds a rule.
|
||||
|
||||
Token scopes for either: `write:repository` and `read:user`, nothing else, per vocabulary.mjs rules (no
|
||||
`all`, no admin).
|
||||
|
||||
D11. **The host git socket.** `publish` is added to the socket. `push`, `fetch` and `land` don't change. The
|
||||
socket's bind rule (E4) adds `git.push.remote`. The working repo stays credential-free and remote-free. The
|
||||
`fork` URL is passed per call and never written into its config. A landing still squashes into the project
|
||||
root as "Mosaic bus host". Landings don't push anywhere.
|
||||
|
||||
## 6. Lifecycle (brief item 4)
|
||||
|
||||
All verbs are on the bus, in the CLI (`mosaic credentials ...`) and in the Console's agent page. Every verb is
|
||||
human-only. Under Q173 the tester may run `status` and `revoke` on the dev stack instance. `issue` and
|
||||
`rotate` stay with Jason, because the tester is an agent and the token would pass through its session
|
||||
(duckworth N1). Each one appends to the organization
|
||||
writer's `changes.jsonl` (actor, agent, kind, verb, date; never a value). No schema migration, no new event
|
||||
kind. A refusal uses `action.refused`.
|
||||
|
||||
| Verb | What it does |
|
||||
|---|---|
|
||||
| `issue AGENT KIND --rotate-by DATE` (gitea) or `--expires DATE` (vikunja) | Reads the token on stdin (never argv), checks E2's format, writes `<kind>.tmp` 0600, fsyncs, renames over `<kind>`, and sets the business file reference to `{"store": true, date}`. The bus reloads that one entry, with no restart |
|
||||
| `rotate` | Same as `issue` over an existing entry. The old value is gone from disk and memory when the call returns. Jason then deletes the old token in Gitea (A5) |
|
||||
| `revoke AGENT [KIND] --reason` | Drops the entry from the bus's memory first, then unlinks the file. A use in flight finishes. Every later use refuses `credential-unavailable` |
|
||||
| `delete AGENT` | `revoke` for every kind, then removes the agent's dir. Refused while the agent has a running claim |
|
||||
| `status [AGENT]` | Kind, date and state per entry (E3's `status()`), no values |
|
||||
|
||||
D12. **"Stops working at once."** On our side this holds without a timing window. A fired agent never holds a
|
||||
`gitea` or `vikunja` token (D3), so revoking means the bus no longer has it. On the Gitea and Vikunja side the
|
||||
token still exists until Jason deletes it (A5). The status line for a revoked entry says so until Jason
|
||||
confirms. The model token is the exception: it sits in the running sandbox (D7), so a fired agent's runs must
|
||||
end. Row 194's fire ends them.
|
||||
|
||||
D13. **What row 194 calls.**
|
||||
|
||||
- `credentials.issue` is never called by the chief. Row 194's hire stays uncredentialed (its D1).
|
||||
- A human `create` of a credentialed definition: row 194 lifts E9's refusal for the human path only. The agent
|
||||
is created with `store` references and no files. `launch` refuses `credential-unavailable` until Jason runs
|
||||
`issue`.
|
||||
- `fire` and `delete`: row 194 calls `credentials.revoke(business, agent, {reason, actor})` before it changes
|
||||
the business file. A failed revoke stops the fire (`credential-revoke-failed`), so an agent can't be fired
|
||||
with its credentials still live on our side.
|
||||
- `regenerate`: no credential change.
|
||||
|
||||
## 7. Who does what (brief item 5)
|
||||
|
||||
Agents never mint accounts or tokens. Jason's steps, per agent that needs `gitea`:
|
||||
|
||||
- A1. Create the Gitea user: Site Administration, User Accounts, Create User Account. The name is the agent's
|
||||
`gitea.user`. The email is the agent's `identity.email`.
|
||||
- A2. Create its token: sign in as that user, Settings, Applications, Generate New Token, scopes
|
||||
`write:repository` and `read:user`, nothing else.
|
||||
- A3. Create the fork of each granted repo as that user (O1). Gitea's push-to-create
|
||||
(`ENABLE_PUSH_CREATE_USER`, `ENABLE_PUSH_CREATE_ORG`) stays off, so a push to a missing `<user>/REPO`
|
||||
fails rather than creating a plain repository (N11). `publish` refuses `fork-unavailable` before that.
|
||||
- A4. On the stack instance's VM, as its OS user: `mosaic credentials issue AGENT gitea --rotate-by DATE`, with
|
||||
the token pasted into stdin.
|
||||
- A5. On fire or rotate: delete the token in Gitea (Settings, Applications), or disable the user.
|
||||
- A6. Model: sign in to the provider on each VM user to write `<configDir>/secrets/claude-session.env`.
|
||||
|
||||
Vikunja bot accounts and tokens are already Jason's (tracker bots). Only A4 changes for them.
|
||||
|
||||
## 8. Migration (brief item 6)
|
||||
|
||||
- M1. Existing `{file}` and `{env}` references keep working. A business moves one agent at a time with
|
||||
`issue`, which rewrites that agent's reference to `store`.
|
||||
- M2. Nothing here touches the dev repository's helper, its `.git/config`, `~/.mosaic` or `~/.mosaic-dev`.
|
||||
Dev seats (jarvis, seat tokens) keep pushing as today. The mos-0981 rule ends only when Mos says so.
|
||||
- M3. The live dev stack instance needs no change at re-pin: no reference uses `store` until someone issues
|
||||
one.
|
||||
|
||||
## 9. Questions
|
||||
|
||||
Mos ruled on the authority model in mos-1027 (2026-10-20T19:27Z):
|
||||
|
||||
- Q-M1. O1, one fork per agent.
|
||||
- Q-M2. `git.push.remote` and its bounds in D9 to D11 are approved. It lands as a hard wait: out of ROUTED,
|
||||
and no role grants it within-role (D9 step 1). Sage puts its routing in the PRD version draft going to
|
||||
Jason with `role.hire` and `role.fire` (Vikunja #420). After his approval, a reviewed commit routes it and
|
||||
grants it to coder.
|
||||
- Q-M3. Issue, rotate and delete are human-only, and the chief gets none of them. Under Q173 the dev
|
||||
instance's tester gets `status` and `revoke` only.
|
||||
- Q-M4. The model credential stays per stack instance for 10-27. Per-agent model tokens wait for a proxy
|
||||
row.
|
||||
|
||||
Open for Jason. Mos takes these to him one at a time:
|
||||
|
||||
- Q-J1. The model account for Case 1 and Case 2: existing subscription or a new account.
|
||||
- Q-J2. Gitea users and tokens for the agents that need them (A1 to A3).
|
||||
|
||||
## 10. What this closes (brief item 7)
|
||||
|
||||
- G11: closes the store (D1 to D6) and lifecycle (section 6) for `gitea` and `vikunja`. Still open: per-agent
|
||||
model credentials (D7), and the same-uid limit inside one stack instance (D6).
|
||||
- G13: closes "own credentials for git" (D8 to D10). Still open: Gitea API verbs (section 4).
|
||||
- C2.5 (git remote): Case 2's books repo can use `publish` once its project names a remote.
|
||||
|
||||
## 11. Tests and mutants
|
||||
|
||||
All tests use scratch roots, fake tokens and a fake Gitea: `git http-backend` behind a node http server on
|
||||
127.0.0.1 that checks basic auth against a fake token and records each ref update. No request leaves the host.
|
||||
|
||||
- T1. Layout: wrong mode or owner on a dir or file refuses start. A `{file}` reference into another agent's
|
||||
path refuses `credential-not-yours`. A bus restart after a create without `issue` starts, and the agent's
|
||||
status is `not-issued`. A missing `{file}` still refuses start.
|
||||
- T2. `useFor` keys from the cap only. A request naming another agent is refused and leaves one
|
||||
`action.refused`.
|
||||
- T3. The fake Gitea serves https on 127.0.0.1 with a cert from a scratch CA made by the test. It reaches git
|
||||
only through the `tls` parameter (D9 step 5). Each refusal test that should reach the network asserts that
|
||||
the first port received the request, so no test passes because git refused http first. `publish`: the
|
||||
happy path lands `refs/heads/<user>/<branch>` on the agent's fork in the fake, which shares root's history, at
|
||||
exactly the decision's commit. Refusals for each step in D9, one test each, including `work-not-yours`. Until routing, a publish with
|
||||
no decision refuses, one with a decision for another task or commit refuses, and a second publish on a
|
||||
spent decision refuses `decision-consumed`. Approve while the latest push is A, push B, publish: refuses
|
||||
`decision-mismatch`, and the fork receives nothing. A push to the working repo between `checkPublish` and
|
||||
the git push refuses with `ref-moved` or `decision-mismatch` (sage-1440, BUILD.md deviation 14). A raise with no recorded push refuses `no-push`. A test pins that `git.push.remote` is in neither ROUTED list
|
||||
and that no `roles/*.json` grants it. A force-push attempt is
|
||||
refused by git's non-fast-forward. A fake fork that answers 302 to a second 127.0.0.1 port: the push
|
||||
fails, and the second port receives no request with an Authorization header. A remote on the right host
|
||||
and a different port is refused at step 2. Two agents granted one project each land on their own fork:
|
||||
agent A's push reaches `/<A's user>/REPO` only and B's reaches `/<B's user>/REPO` only, and the fake's
|
||||
upstream repository receives nothing. An upstream owned by the agent's own user refuses `remote-not-fork`,
|
||||
and so does one whose owner differs from `gitea.user` only in case. With an upstream that shares root's
|
||||
history, a first publish to an empty fork and to an unrelated fork each refuse `remote-unrelated`, and
|
||||
neither fork nor upstream receives objects. A fork that doesn't exist refuses `fork-unavailable`, and the
|
||||
fake records no push. `ls-remote` and the push each reach the fork's path only (the fake records every
|
||||
path). Load refusals (D9c), one test each: a dot segment, an extra path segment, a missing segment,
|
||||
a trailing slash, userinfo, a query, a fragment, an explicit `:443`, a name outside Gitea's rule, and a `gitea.url` with a
|
||||
sub-path. A `.git` suffix loads and builds the fork without it. A `publish` grant on an agent without
|
||||
`identity`, and on one whose definition lacks `gitea`, each refuse at load with the N10 message.
|
||||
- T4. Token handoff: the token never appears in argv, env, the fork URL, the working repo config,
|
||||
`changes.jsonl`, events or logs (scan every file under the scratch roots).
|
||||
- T5. Lifecycle: issue, rotate, revoke and delete, each live with no restart. After revoke, `publish` and the
|
||||
Vikunja path refuse `credential-unavailable`, and so they do after a bus restart that follows the revoke.
|
||||
The tester can run `status` and `revoke`, and `issue` and `rotate` refuse it.
|
||||
- T6. Default: an agent without `identity` commits as E7. Existing `{file}` references load unchanged.
|
||||
|
||||
Mutants to kill: drop the cap-derived key in `useFor`, let the request name the destination ref, add `+` to
|
||||
the refspec, skip the identity check, put the token in the URL, revoke file before memory, skip the dir mode
|
||||
check, allow a `{file}` reference into another agent's dir, make ENOENT on a `store` path fatal again, make
|
||||
ENOENT on a `{file}` path silent, a helper that answers any host, drop `http.followRedirects=false`, drop the
|
||||
`work-not-yours` check, drop the `remote-unrelated` check, add `git.push.remote` to ROUTED, skip the spent
|
||||
check on the decision, drop the `pushCommit` comparison in `#authorityFor`, push by ref name in place of the
|
||||
oid, push to the upstream URL in place of the derived fork, run `ls-remote` against the upstream in place of
|
||||
the fork, take the fork's owner from anywhere but the agent's `gitea.user`, compare the owner with case, run the
|
||||
D9c checks on the raw path in place of the normalized one, read a failed `ls-remote` as empty, skip the tip
|
||||
check under the lock, and pass a CA flag in production.
|
||||
|
||||
## 12. Risks
|
||||
|
||||
- R1. Backups: if a backup tool copies `<configDir>`, it copies tokens. The build checks row 185's backup set
|
||||
and says what it includes.
|
||||
- R2. Withdrawn. O1 was ruled, so probe P1 isn't needed.
|
||||
- R3. Outbound HTTPS from the bus host to Gitea is new network reach for the host process. On the VMs this is
|
||||
Jason's network to confirm.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Row 237: one decision publishes once (#1743)
|
||||
|
||||
Megavolt, 2026-10-21 (from `date -u`). Brief: `docs/plans/2026-10-20_agent-credentials.md`
|
||||
§ "row 199 follow-up N1", blob dca67d310e1b. Source: Duckworth's row 199 round 1 note N1
|
||||
(duckworth-0068). Base: `cb5fdb064` (row 199's landing). Reviewer Duckworth, gate owner Sage.
|
||||
|
||||
## The defect
|
||||
|
||||
`checkPublish` opens a ticket after checking the decision is unspent. The decision is spent only at
|
||||
`recordPublish`, which runs after the bus process has pushed. Two `checkPublish` calls on one decision
|
||||
therefore both get tickets, and both push. With different branches, the commit lands on two fork refs:
|
||||
`refs/heads/<user>/feat` and `refs/heads/<user>/other`. The second `recordPublish` then refuses
|
||||
`decision-consumed`, but by then its push has already reached the fork.
|
||||
|
||||
## The fix, and why this option
|
||||
|
||||
`checkPublish` refuses `publish-busy` when an open ticket in `#publishes` holds the same business and
|
||||
decision (`packages/bus/src/broker.mjs:1038-1040`). It runs after the authority and fact checks, so a
|
||||
spent decision still refuses `decision-consumed` first.
|
||||
|
||||
The brief's other option, rechecking `#spent` in `publishTarget`, doesn't close the race. Both tickets
|
||||
reach `publishTarget` before either push ends, and nothing is spent until `recordPublish`. So both
|
||||
rechecks pass and both pushes run. The only state that exists while the first push runs is the open
|
||||
ticket, so the check reads that.
|
||||
|
||||
A refusal (`publishRefused`) drops the ticket and leaves the decision unspent, so a retry on the same
|
||||
decision gets a new ticket. That is row 199's behaviour, unchanged. A within-role publish has no
|
||||
decision and isn't affected.
|
||||
|
||||
## Tests
|
||||
|
||||
- `packages/bus/tests/publish.test.mjs:186`. A second ticket refuses `publish-busy`, on the same branch
|
||||
and on another. After `publishRefused` the retry gets a ticket and a third refuses busy. After
|
||||
`recordPublish` there is one `action.allowed` for git.push.remote and the next call refuses
|
||||
`decision-consumed`. Another agent's decision on the same task isn't held.
|
||||
- `packages/cli/tests/publish-socket.test.mjs:269`. End to end through the git socket, the bus runtime
|
||||
and the TLS fake Gitea. While the first publish holds its ticket, a second publish on the same
|
||||
decision to branch `other` gets `publish-busy`. The fake Gitea gets exactly one `git-receive-pack`,
|
||||
the fork has only `refs/heads/coder/feat`, and there is one `action.allowed`.
|
||||
|
||||
Runs on this branch, single files:
|
||||
- publish.test.mjs: 13/13.
|
||||
- publish-socket.test.mjs: 8/8.
|
||||
|
||||
## Mutant
|
||||
|
||||
M1 removes the `publish-busy` line. Both tests fail:
|
||||
- The bus test fails at the first busy assert.
|
||||
- The cli test fails because the second publish answers `ok: true` with `ref: refs/heads/coder/other`.
|
||||
That is the defect: two refs on the fork.
|
||||
|
||||
Logs are in `/mnt/storage/scratch/megavolt-r237/` (m1-bus.log, m1b-cli.log). `cmp` confirmed
|
||||
broker.mjs matched the saved copy after the restore. The first cli mutant run (m1-cli.log) was against
|
||||
a hook that recursed on every checkPublish. I fixed the hook to fire only for branch `feat` and reran
|
||||
it (m1b-cli.log).
|
||||
|
||||
## Residuals, not fixed here
|
||||
|
||||
1. **A ticket that never ends holds its decision until the bus restarts.** `#publishes` is in memory.
|
||||
That happens when the host dies between checkPublish and recordPublish or publishRefused. It fails
|
||||
closed (`publish-busy`), and a restart clears it.
|
||||
2. **publish-timeout (sage-1440).** The socket files `publishRefused` and drops the ticket, but the bus
|
||||
process's push may still be running. A retry on the same decision can then open a ticket while that
|
||||
push is live. This is the same residual the socket already logs ("the bus process's push may still
|
||||
reach the fork"). Closing it means the runtime's `remotes` must keep the decision busy until its push
|
||||
settles, which reaches into runtime.mjs. That is beyond the brief's two options.
|
||||
3. **checkLand has the same shape.** `#landings` takes a ticket per call and the decision is spent at
|
||||
`recordLand`. Whether two landings on one decision can both reach main isn't checked here. That's a
|
||||
follow-up for Sage to row or dismiss.
|
||||
|
||||
## Gate
|
||||
|
||||
Per the brief: Duckworth approves the manifest, the bus suite and every `scripts/test-*.sh` are green
|
||||
on Sage's gate, and M1 is killed. This row also changes a cli test file, so the cli suite (or that file
|
||||
alone) should run on the gate too.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Row 241: one decision lands once (#1747, P19 441)
|
||||
|
||||
Megavolt, 2026-10-21 (from `date -u`). Brief: `docs/plans/2026-10-20_agent-credentials.md`
|
||||
§ "Brief: row 237 follow-up, one decision lands once, with tests at the broker", c25785d3d. Source:
|
||||
row 237 residual 3, megavolt-0068, megavolt-0069 and sage-1651. Base: origin/refactor f5753c0e8, row 210's landing.
|
||||
Reviewer Duckworth, gate owner Sage. Tests only; no source changes.
|
||||
|
||||
## Why landing has no row 237 race
|
||||
|
||||
Row 237 fixed publish: two tickets on one decision both pushed, because the decision is spent only
|
||||
after the push, and a push to a remote fork can't be taken back. Landing has the same ticket shape
|
||||
(`checkLand` opens a ticket, `recordLand` spends the decision), but three guards keep one decision
|
||||
from landing twice:
|
||||
|
||||
1. **land-busy, the outer guard.** `doLand` in `packages/cli/src/git-socket.mjs:351` refuses
|
||||
`land-busy` while another landing runs. The slot is taken before the first await, and freed only
|
||||
when the landing ends. There is one git socket per launcher, and one host per dataRoot (host.lock).
|
||||
So through the socket two landings never overlap, and a later one stops at `checkLand` with
|
||||
`decision-consumed`. Even past that, applying the same reviewed patch to the new main fails
|
||||
`base-conflict` in `prepareLand`.
|
||||
2. **The CAS on main.** `finishLand` (`packages/seat/src/working.mjs:391`) moves main with
|
||||
`update-ref main <commit> <old>`, so a landing prepared on a tip that has since moved refuses
|
||||
`base-moved` and writes nothing.
|
||||
3. **The recordLand recheck.** `finishLand` calls `record()`, which is `recordLand`, under the same
|
||||
repository lock. `recordLand` runs `#landFacts` again (`packages/bus/src/broker.mjs:963`), which
|
||||
refuses `decision-consumed` once the decision is spent. When `record()` throws, `finishLand` moves
|
||||
main back.
|
||||
|
||||
Guards 2 and 3 can't be reached end to end, because guard 1 stops a second landing first. So they are
|
||||
pinned below the socket, and no test bypasses land-busy.
|
||||
|
||||
### The unrecorded-commit window
|
||||
|
||||
Between the `update-ref` and the end of `record()`, main names the squash, which isn't recorded yet.
|
||||
That window is under the repository's lock, so no other push, review or landing in this host sees
|
||||
it. If `record()` refuses, main moves back. If moving back fails, `finishLand` refuses
|
||||
`rollback-failed` and main keeps naming an unrecorded commit (tested at `working.test.mjs:555`, the
|
||||
main.lock case). That is existing behaviour, unchanged here.
|
||||
|
||||
## The test
|
||||
|
||||
`packages/bus/tests/land.test.mjs:312`. Two tickets pass `checkLand` on one decision before either
|
||||
records. `recordLand` on the first lands. `recordLand` on the second refuses `decision-consumed`
|
||||
(:319), and leaves one merge `action.allowed`, one RESULT and no `action.refused`. `landRefused`, as
|
||||
the socket would call it, then files the refusal and ends the ticket.
|
||||
|
||||
**The assert that pins the recordLand recheck is :319.** The `#spent` line is shared with
|
||||
`checkLand`, so dropping it also fails :144 (a `checkLand` after the decision is spent). Only :319
|
||||
shows the recheck at record time.
|
||||
|
||||
Single file on this branch: land.test.mjs 18/18.
|
||||
Whole suites on this branch before in-review: bus 500/500, seat 164/164, cli 565/565 (02:52 to 03:01Z).
|
||||
|
||||
## Mutants
|
||||
|
||||
| Mutant | Change | Killed by |
|
||||
|---|---|---|
|
||||
| M1 | drop the `#spent` line in `#landFacts` (broker.mjs:963) | land.test.mjs: :312 at :319, and :94 at :144 (16/2) |
|
||||
| M2 | drop `land.old` from the `update-ref` in `finishLand` (working.mjs:391) | working.test.mjs:555 at :567, the `base-moved` assert (25/1) |
|
||||
| M3 | drop the `land-busy` throw (git-socket.mjs:351) | git-socket.test.mjs:812 at :825, the `land-busy` assert (23/1) |
|
||||
|
||||
Each run was one test file on base f5753c0e8 (row 210's landing, which moved the `#spent` line from
|
||||
:945 to :963), 2026-10-21 02:51Z. Each assert failed with "Missing expected exception" or "rejection".
|
||||
`cmp` confirmed each source matched its saved copy after the restore. M2 is killed by the existing
|
||||
working.test:555, so per the brief working.test.mjs isn't changed. Logs: `/mnt/storage/scratch/megavolt-r241/`
|
||||
(m1.log to m3.log, with each diff).
|
||||
|
||||
## Gate
|
||||
|
||||
Per the brief: Duckworth approves the manifest; the bus, seat and cli suites and every
|
||||
`scripts/test-*.sh` are green on Sage's gate; the three mutants are killed.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Row 242 build: a retry after publish-timeout pushes once
|
||||
|
||||
Row 242, #1748, P19 440. Brief: docs/plans/2026-10-20_agent-credentials.md, "Brief: row 237
|
||||
follow-up N1, a retry after publish-timeout pushes once" (9d165be8c). Base: origin/refactor
|
||||
8fede5fc6 (row 210 landed f5753c0e8).
|
||||
|
||||
## The defect
|
||||
|
||||
The git socket polls publishResult until publishDeadlineMs, then refuses publish-timeout
|
||||
(packages/cli/src/git-socket.mjs:433) while the bus process's push (runtime publishPush) can
|
||||
still be running. The socket then calls publishRefused, which went straight to
|
||||
broker.publishRefused: it filed the refusal and deleted the ticket. checkPublish no longer saw an
|
||||
open ticket on the decision, so a retry got a new ticket and pushed a second ref while the first
|
||||
push was still on its way to the fork.
|
||||
|
||||
## Fix chosen: hold the decision until the push ends, then record a push that reached the fork
|
||||
|
||||
packages/bus/src/runtime.mjs, `publishRefused` and `settle`:
|
||||
|
||||
- If the bus has no push for the ticket, publishRefused goes to the broker as before.
|
||||
- If the push is still running, the refusal code is held on the entry and the ticket stays open.
|
||||
checkPublish refuses another publish on the decision with publish-busy (row 237's check),
|
||||
the same as when the socket is still polling.
|
||||
- When the push ends, `settle` runs:
|
||||
- A push that reached the fork is recorded with broker.recordPublish, which spends the
|
||||
decision. The bus logs `pushed after the git socket refused <code>; recorded`.
|
||||
- A push that failed is filed as publishRefused with the socket's code.
|
||||
- If recordPublish refuses with a BusError, that code is filed instead.
|
||||
- If the push has already ended when the refusal arrives, it's settled at once. The sage-1440 test
|
||||
relies on this: its push fails fast, so publish-timeout is filed once, immediately, as before.
|
||||
|
||||
Why hold rather than kill. Killing the git client doesn't undo a push the fork has already
|
||||
received: receive-pack can update the ref after the client dies. After a kill the bus would have
|
||||
to ls-remote the fork to learn what landed, and the result could still race the server. Holding
|
||||
waits for the push's own outcome. Each git run in publishRemote already has its own limit
|
||||
(publishRemote defaults to checkout.mjs GIT_LIMITS), so a hold is bounded. Holding also reuses the existing publish-busy
|
||||
state, and the refusal text is unchanged.
|
||||
|
||||
Why record and not only hold. If the hold just filed publish-timeout once the push ended, the
|
||||
ticket would close and the decision would be free again with a ref on the fork. A retry would then
|
||||
push a second ref. Recording the late push spends the decision, so the retry gets
|
||||
decision-consumed.
|
||||
|
||||
The publish-timeout refusal text is unchanged (brief condition).
|
||||
|
||||
## Test
|
||||
|
||||
packages/cli/tests/publish-socket.test.mjs:302, "row 242: a retry after publish-timeout refuses
|
||||
publish-busy while the bus process's push runs; the late push is recorded and the decision pushes
|
||||
once".
|
||||
|
||||
- publishDeadlineMs 150 and a store token. The fork's pre-receive hook writes `held`, then loops
|
||||
until the test writes `release`.
|
||||
- The first publish gives publish-timeout. The test waits for `held`.
|
||||
- A retry to branch `other` gives publish-busy (:322). Refused events: ["publish-busy"]. No
|
||||
git.push.remote allowed event.
|
||||
- The test writes `release` and waits until there is exactly one allowed event, with ref
|
||||
refs/heads/coder/feat and the work commit.
|
||||
- The next retry gives decision-consumed (:330).
|
||||
- Exactly one git-receive-pack request reached the fake Gitea, and the fork has only
|
||||
refs/heads/coder/feat. Refused events: ["publish-busy", "decision-consumed"]. No TOKEN appears in
|
||||
events or logs.
|
||||
|
||||
Fail fast (sage-1681): the body is wrapped in try/finally, and the finally writes `release`. Under
|
||||
the first mutant run, the assert at the retry failed and the hook was never released. setup's
|
||||
teardown waits for the fake Gitea, which waited for the held push, so the file hung until I stopped
|
||||
it. Now a failed assert ends the hook and the run. Each wait is bounded at 10 s with an assert.fail.
|
||||
|
||||
## Mutants
|
||||
|
||||
Script: /mnt/storage/scratch/megavolt-r242/mutants.sh. Each mutant: save, one-line sed, run
|
||||
publish-socket.test.mjs whole, restore, cmp.
|
||||
|
||||
| Mutant | Change | Expected kill |
|
||||
|---|---|---|
|
||||
| M1 | export `publishRefused: (ticket, args) => broker.publishRefused(ticket, args)` again (removes the fix) | :302 at :322: the retry gets a new ticket and its own push, so publish-timeout, not publish-busy (seen in the stopped 03:06Z run) |
|
||||
| M2 | `settle` files publishRefused without trying recordPublish (hold only) | :302 at the wait for one allowed event, then :330 |
|
||||
|
||||
Results (Sage's slot, sage-1686, 2026-10-21T03:35:00Z to 03:35:15Z): both killed, both restored
|
||||
(cmp ok).
|
||||
|
||||
- M1: publish-socket 8/1, the row 242 test fails at :322 in 0.5 s.
|
||||
- M2: publish-socket 8/1, the row 242 test fails at :327, the wait for one allowed event, through
|
||||
the assert.fail inside `until` (:311) after its 10 s bound, in 10.3 s.
|
||||
|
||||
## Suites
|
||||
|
||||
On Sage's slot (sage-1686), on base 8fede5fc6 plus this row:
|
||||
|
||||
- bus `node --test tests/*.test.mjs`: 499/499, 03:35:20Z to 03:35:34Z. This is one fewer than
|
||||
Sage's 241 gate (500) because the base predates row 241's land.test case.
|
||||
- cli `node --test tests/*.test.mjs`: 566/566, 03:35:34Z to 03:43:47Z. That's 565 plus the
|
||||
row 242 test.
|
||||
|
||||
Singles before the slot: publish-socket.test.mjs 9/9, bus publish.test.mjs 13/13. The seat suite
|
||||
and scripts/test-*.sh are left for Sage's landing gate.
|
||||
|
||||
## Residuals
|
||||
|
||||
- The agent was told publish-timeout, but the push is recorded later and the run isn't
|
||||
notified. The allowed event and the bus log line are the only trace. A later publish on the
|
||||
decision gets decision-consumed, which tells the agent the decision was used. Whether the run
|
||||
should get a RESULT for a late record is a follow-up question for Sage.
|
||||
- A late push whose recordPublish refuses (Duckworth N2) is filed with that code: the ref stays on
|
||||
the fork and the decision is not spent, the same residual as an in-band push whose record
|
||||
refuses.
|
||||
- The hold lives in the bus process's memory, like the broker's open tickets. A bus restart during
|
||||
a hold drops both, the same as today for any publish in flight.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Quackerjack
|
||||
|
||||
Builder seat, Claude Haiku 5.5 at high effort, started by Sage under mos-1023 item 1 and Q177 for federation
|
||||
rows R5 (`mosaic access list, grant, revoke`) and R2 (the `federation` config key), per Gizmoduck's design
|
||||
section 7 (#414).
|
||||
|
||||
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
|
||||
`quackerjack/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or
|
||||
another seat's branch.
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
n=0
|
||||
for p in /proc/[0-9]*; do
|
||||
c=$(tr '\0' ' ' < $p/cmdline 2>/dev/null) || continue
|
||||
case "$c" in *shell-snapshots*|*chrome-devtools-mcp*|*sage-busy-now*) continue;; esac
|
||||
if echo "$c" | grep -qE 'node --test|gate\.sh|mutant|scripts/test-[a-z-]+\.sh|docker compose'; then echo "BUSY ${p#/proc/}: ${c:0:160}"; n=$((n+1)); fi
|
||||
done
|
||||
[ $n -eq 0 ] && echo CLEAR
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -o pipefail
|
||||
# Landing gate template (2026-10-14): sed the G= and OUT= lines and this comment.
|
||||
# Sequential suites, each teed to its own log; stops at the first failure.
|
||||
G=/mnt/storage/scratch/SET-ME-wt
|
||||
OUT=/mnt/storage/scratch/SET-ME-out
|
||||
cd "$G" || exit 9
|
||||
mkdir -p "$OUT" || exit 9 # a missing OUT failed the row 142 gate at step 1 (2026-10-15)
|
||||
# test-task.sh live cases run `docker compose` from $G, so without a name the project is "wt"
|
||||
# and leaves wt_default behind (row 142 gate, 2026-10-15). Name it per gate dir and remove it on exit.
|
||||
export COMPOSE_PROJECT_NAME="$(basename "$(dirname "$G")")"
|
||||
trap 'docker network rm "${COMPOSE_PROJECT_NAME}_default" >/dev/null 2>&1 && echo "removed network ${COMPOSE_PROJECT_NAME}_default"' EXIT
|
||||
# A gate tree without the node_modules link fails cli tests with ENOENT (2026-10-13).
|
||||
[ -d "$G/node_modules" ] || { echo "STOP no node_modules in $G"; exit 8; }
|
||||
echo "base $(git rev-parse --short HEAD) dirty $(git status --short | grep -vc node_modules) DOCKER_HOST=${DOCKER_HOST:-unset}"
|
||||
run() { local name="$1" dir="$2" cmd="$3"
|
||||
(cd "$G/$dir" && bash -c "$cmd") > "$OUT/$name.log" 2>&1; local rc=$?
|
||||
echo "$name rc=$rc $(grep -E '^ℹ (pass|fail) [0-9]+' "$OUT/$name.log" | tr '\n' ' ') | $(tail -n1 "$OUT/$name.log")"
|
||||
[ $rc -eq 0 ] || { echo "STOP $name"; exit $rc; }; }
|
||||
# No pkg-queue step: scripts/test-queue.sh runs packages/queue/tests/ itself (same 156 tests; CI debate item 2a, Vikunja #274).
|
||||
for d in packages/*/; do p=$(basename "$d"); [ -d "$d/tests" ] || continue; [ "$p" = queue ] && continue
|
||||
run "pkg-$p" "packages/$p" 'node --test tests/*.test.mjs'; done
|
||||
for s in scripts/test-*.sh; do n=$(basename "$s" .sh); run "script-${n#test-}" . "bash $s"; done
|
||||
echo GATE_PASSED
|
||||
@@ -0,0 +1,36 @@
|
||||
# Row 189 gate timing (for row 197, Herb)
|
||||
|
||||
Sage, 2026-10-20T19:25:04Z. Gate on 0c177e9ef (base 0217b71c8), one lane. The slot was mine. Seats may have run single test files at the same time, which the slot rules allow, and I did not record that.
|
||||
Start 2026-10-20T19:09:37Z, end 2026-10-20T19:20:36Z. Total wall time: 659 s.
|
||||
|
||||
Wall time per suite is the gap between one suite's log mtime and the one before it (the first suite starts at start.txt), so it includes node start-up. node's duration_ms is listed where the suite prints it. Scripts print no duration.
|
||||
|
||||
| Suite | Wall s | duration_ms s | Ended (UTC) |
|
||||
|---|---|---|---|
|
||||
| pkg-bus | 3 | 3.8 | 19:09:40 |
|
||||
| pkg-business | 3 | 2.2 | 19:09:43 |
|
||||
| pkg-cli | 388 | 388.5 | 19:16:11 |
|
||||
| pkg-control-board | 1 | 0.8 | 19:16:12 |
|
||||
| pkg-conversation | 35 | 35.4 | 19:16:47 |
|
||||
| pkg-discord | 3 | 2.6 | 19:16:50 |
|
||||
| pkg-harness | 12 | 11.6 | 19:17:02 |
|
||||
| pkg-ledger | 13 | 13.5 | 19:17:15 |
|
||||
| pkg-mosaic | 3 | 2.3 | 19:17:18 |
|
||||
| pkg-plugins | 0 | 0.3 | 19:17:18 |
|
||||
| pkg-project | 1 | 1.2 | 19:17:19 |
|
||||
| pkg-runs | 1 | 0.8 | 19:17:20 |
|
||||
| pkg-seat | 6 | 6.2 | 19:17:26 |
|
||||
| pkg-tasks | 1 | 1.2 | 19:17:27 |
|
||||
| pkg-webui | 58 | 57.9 | 19:18:25 |
|
||||
| script-auth | 1 | - | 19:18:26 |
|
||||
| script-conductor | 5 | - | 19:18:31 |
|
||||
| script-config | 2 | - | 19:18:33 |
|
||||
| script-discord | 8 | - | 19:18:41 |
|
||||
| script-extension-package | 1 | - | 19:18:42 |
|
||||
| script-foundation | 40 | - | 19:19:22 |
|
||||
| script-land-work | 11 | - | 19:19:33 |
|
||||
| script-queue | 21 | - | 19:19:54 |
|
||||
| script-release | 11 | - | 19:20:05 |
|
||||
| script-task | 31 | - | 19:20:36 |
|
||||
|
||||
Source: /mnt/storage/scratch/sage-g189/out/*.log mtimes and gate.log. The cli suite is about 59% of the run, and webui plus conversation add about 14%.
|
||||
@@ -0,0 +1,8 @@
|
||||
# PRD 1.8 r7: no in-Stack raise (mos-1154)
|
||||
|
||||
Sage, 2026-10-21T01:20:06Z. Jason approved PRD 1.8 in T3 (thread 38bc4c81, before 2026-10-21T01:16:55Z), verbatim:
|
||||
"PRD 1.8: I am preemptively approving it. Do not surface in the Mosaic Stack board. Just proceed."
|
||||
Mos records it as DECISIONS Q192 (mos-1154). The [mos test] raise request drafted here was dropped
|
||||
unsent, and the template is deleted. Jason closed 3a6c8fb9 himself at 01:05:45Z (no, "superseded by r7").
|
||||
Scope (Mos's reading): r7 at 47c2cb33c plus 82df17347, 342 lines, sha256
|
||||
de079281ff2760911039276379ec4b190287b2ce2e4e8a79c9459939727440cd, Duckworth duckworth-0073 (pass, no findings).
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"seq": 22,
|
||||
"id": "3a6c8fb9-a5ab-42f8-9755-902a88375ca2",
|
||||
"at": "2026-10-20T22:41:14.444Z",
|
||||
"business": "mosaic-stack",
|
||||
"project": null,
|
||||
"raised_by_role": "pm",
|
||||
"raised_by_run": "rc9e993da24cf",
|
||||
"class": "gated",
|
||||
"action": "prd.approve",
|
||||
"route_to": "human",
|
||||
"question": "Approve PRD 1.8 exactly as drafted?\n\nFile: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Duckworth reviewed it (duckworth-0061, confirmed duckworth-0062) and Mos checked it (mos-1140). The tip of branch honker/row194-b2 holds a later r6 (aba3cbdeb), which is not used here. This approval covers only the r5 commit and sha256 above. The PM copied these values from the request and did not compute or check the sha256.\n\nNotes the approval carries:\n1. Mos's reading: \"the main agent or chief of staff\" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.\n2. Mos's reading: revoke by \"you, Mos or the tester\" means the user, the assistant (\"Mos\", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.\n3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.\n4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.\n5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.\n\nSource: a tester request relayed by the chief (message 6990efb4, tester message d54a190e). It is a test request, not Jason's instruction. It takes the same route as PRD 1.7 (decision d52febb5): non-blocking, no task, uncited, so it goes to the human's Inbox. I recommend yes because two reviewers have passed r5 and the open points are written down above, not hidden.",
|
||||
"options": [
|
||||
{
|
||||
"key": "yes",
|
||||
"text": "Approve PRD 1.8 exactly as drafted: the r5 file at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 with the sha256 below, and only that."
|
||||
},
|
||||
{
|
||||
"key": "no",
|
||||
"text": "Don't approve. Say what changes, and a new revision comes back for review."
|
||||
}
|
||||
],
|
||||
"recommendation": "yes",
|
||||
"task_ref": null,
|
||||
"requirement_ref": null,
|
||||
"blocking": false,
|
||||
"supersedes": null,
|
||||
"spec_path": null,
|
||||
"spec_commit": null,
|
||||
"authorization": {
|
||||
"action": "prd.approve",
|
||||
"target": null,
|
||||
"approvalChoice": "yes"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"seq": 22,
|
||||
"id": "3a6c8fb9-a5ab-42f8-9755-902a88375ca2",
|
||||
"at": "2026-10-20T22:41:14.444Z",
|
||||
"business": "mosaic-stack",
|
||||
"project": null,
|
||||
"raised_by_role": "pm",
|
||||
"raised_by_run": "rc9e993da24cf",
|
||||
"class": "gated",
|
||||
"action": "prd.approve",
|
||||
"route_to": "human",
|
||||
"question": "Approve PRD 1.8 exactly as drafted?\n\nFile: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Duckworth reviewed it (duckworth-0061, confirmed duckworth-0062) and Mos checked it (mos-1140). The tip of branch honker/row194-b2 holds a later r6 (aba3cbdeb), which is not used here. This approval covers only the r5 commit and sha256 above. The PM copied these values from the request and did not compute or check the sha256.\n\nNotes the approval carries:\n1. Mos's reading: \"the main agent or chief of staff\" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.\n2. Mos's reading: revoke by \"you, Mos or the tester\" means the user, the assistant (\"Mos\", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.\n3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.\n4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.\n5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.\n\nSource: a tester request relayed by the chief (message 6990efb4, tester message d54a190e). It is a test request, not Jason's instruction. It takes the same route as PRD 1.7 (decision d52febb5): non-blocking, no task, uncited, so it goes to the human's Inbox. I recommend yes because two reviewers have passed r5 and the open points are written down above, not hidden.",
|
||||
"options": [
|
||||
{
|
||||
"key": "yes",
|
||||
"text": "Approve PRD 1.8 exactly as drafted: the r5 file at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 with the sha256 below, and only that."
|
||||
},
|
||||
{
|
||||
"key": "no",
|
||||
"text": "Don't approve. Say what changes, and a new revision comes back for review."
|
||||
}
|
||||
],
|
||||
"recommendation": "yes",
|
||||
"task_ref": null,
|
||||
"requirement_ref": null,
|
||||
"blocking": false,
|
||||
"supersedes": null,
|
||||
"spec_path": null,
|
||||
"spec_commit": null,
|
||||
"authorization": {
|
||||
"action": "prd.approve",
|
||||
"target": null,
|
||||
"approvalChoice": "yes"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
[mos test] Please have the PM raise a prd.approve decision for PRD 1.8, the same route as PRD 1.7 (decision d52febb5): blocking false, no task_ref, recommendation yes, gated to the human. The PM must not compute or change the sha256; it copies the values below.
|
||||
|
||||
Question: "Approve PRD 1.8 exactly as drafted?"
|
||||
File: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Reviewed by Duckworth (duckworth-0061, confirmed duckworth-0062) and checked by Mos (mos-1140). The branch tip of honker/row194-b2 holds a later r6 (aba3cbdeb) that is not used; the approval covers only the r5 commit and sha256 above.
|
||||
|
||||
The decision notes must carry these five items:
|
||||
1. Mos's reading: "the main agent or chief of staff" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.
|
||||
2. Mos's reading: revoke by "you, Mos or the tester" means the user, the assistant ("Mos", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.
|
||||
3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.
|
||||
4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.
|
||||
5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.
|
||||
|
||||
Reply with the decision id once raised. Do not resolve it.
|
||||
@@ -0,0 +1,8 @@
|
||||
sent d54a190e to chief
|
||||
chief (RESULT, re d54a190e) cc46a7b4:
|
||||
I passed your request to the pm without changing any of it, and launched the pm so it can act on it. No decision id yet, because the pm hasn't raised the decision.
|
||||
|
||||
- **Message to pm:** 6990efb4-8321-4d0e-9b8a-771f65625b83. It says the request comes from a tester and authorizes nothing, and it carries your PRD 1.8 values and the five notes as you wrote them.
|
||||
- **pm launch:** run rc9e993da24cf.
|
||||
|
||||
When the pm answers with the decision id, I'll send it to you.
|
||||
@@ -0,0 +1,5 @@
|
||||
trail 3a6c8fb9-a5ab-42f8-9755-902a88375ca2: 4 row(s)
|
||||
2026-10-20T22:40:47.949Z events#1550 session.launched pm
|
||||
2026-10-20T22:40:47.969Z role_claims#102 claim pm by rc9e993da24cf
|
||||
2026-10-20T22:41:14.444Z decisions#22 gated prd.approve → human: Approve PRD 1.8 exactly as drafted? File: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit …
|
||||
2026-10-20T22:41:14.445Z events#1553 action.allowed pm decision.raise decision 3a6c8fb9
|
||||
@@ -0,0 +1,86 @@
|
||||
# Row 189: `stack-release.sh activate --no-send`
|
||||
|
||||
Gitea #1687. Vikunja P19 #413 (Q175 interim, #412). Brief: docs/plans/2026-10-20_activate-no-send.md.
|
||||
Rulings: mos-0972 (the Console runs without `--send` until Q175 phase 2), mos-0973 (go; Sage builds,
|
||||
Dewey reviews; gate is the cli suite plus every scripts/test-*.sh). Base: def2e7932 (refactor, the
|
||||
brief commit on 9ae8c73ac).
|
||||
|
||||
## What it does
|
||||
|
||||
`activate ID BUSINESS --no-send` drops `--send` from `RUN_CONSOLE` in a new `send_for_target`, which
|
||||
`console_for_target` calls after `proof_for_target`. `install_stack` renders the unit from
|
||||
`RUN_CONSOLE` and `write_pointer` writes the pointer from it, so both lose the flag and `flags_drift`
|
||||
stays quiet afterwards. Only `activate` takes the flag; the parser sends every other op to usage
|
||||
(exit 4).
|
||||
|
||||
`check_target` lets a `--no-send` activate name the pointer's copy only when the pointer's Console
|
||||
flags hold `--send`. That run is a live-copy re-pin, the same path row 187 uses for the proof pair:
|
||||
the plan prints the live-copy note, the run stops both units, backs up the stopped store and starts
|
||||
them again. A plain same-copy activate, or a same-copy `--no-send` once `--send` is gone, still
|
||||
refuses `already-active`.
|
||||
|
||||
`--plan` prints one `send:` line after the `proof:` line, for activate and rollback:
|
||||
|
||||
- `send: dropped; --no-send was given`
|
||||
- `send: off; --no-send was given and the pointer holds no --send`
|
||||
- `send: carried` (the pointer's `--send` goes on unchanged)
|
||||
- `send: off`
|
||||
|
||||
The steps log gets `drop --send; --no-send was given`, or `--no-send was given and the pointer holds no
|
||||
--send, so there is nothing to drop`.
|
||||
|
||||
Rollback and pm-on carry the pointer's flags as they are, so once `--no-send` has run, nothing adds
|
||||
`--send` back. There is no re-enable flag (mos-0973). Q175 phase 2 changes the Console flags in its
|
||||
own row.
|
||||
|
||||
docs/TOOLS.md: `[--no-send]` in the usage block and one paragraph after the proof paragraph.
|
||||
|
||||
## Tests (packages/cli/tests/stack-release.test.mjs, T55)
|
||||
|
||||
1. Seeded preview pointer with `--send --tester-port 7333`: a same-copy `activate B acme --no-send`
|
||||
passes. The pointer and the unit ExecStart lack `--send` and keep the other flags in order, status
|
||||
says the unit flags match, the steps log has the drop line, the log line has from = to = B and the
|
||||
backup sum equals the stopped store's, and the units stop and start in order.
|
||||
2. Plain same-copy activate, with `--tester-port` and with `--no-proof`, refuses `already-active`. So does
|
||||
a same-copy `--no-send` once `--send` is gone, and on a pointer that never held it.
|
||||
3. The four `send:` lines, the live-copy note on the same-copy plan only, and the Console line right
|
||||
after `send:` without `--send`. The run on a pointer with no `--send` logs the nothing-to-drop line
|
||||
and no drop line.
|
||||
4. After `--no-send` on C1, a rollback to B leaves `--send` out of the pointer and the unit, the steps
|
||||
log never names `--send`, and status matches. pm-on after `activate --pm-after-harness --no-send`
|
||||
keeps it off.
|
||||
5. `--no-send` on rollback (with and without `--plan`), pm-on, status, adopt and rehearse is a usage
|
||||
error with the usage text, nothing written, started or logged. The usage text shows `[--no-send]`.
|
||||
6. A pointer holding both the proof pair and `--send`: the plans for `--no-proof`, `--no-send` and both
|
||||
name what each drops and carries. `--no-send` keeps the pair, `--no-send` with a new pair replaces it,
|
||||
`--no-proof` keeps `--send`, and both together leave neither. Status matches after each.
|
||||
|
||||
`planTail` gains `'send: off'` after `'proof: off'`, because every plan now prints the line. T36 takes
|
||||
`out.slice(-n)` around `planTail`, so its four slice counts grow by one.
|
||||
|
||||
## Mutants
|
||||
|
||||
mutants.sh in this packet, run on the candidate. Each mutates scripts/stack-release.sh and runs T55
|
||||
plus T1 and T2.
|
||||
|
||||
- M1: the same-copy exception ignores `--no-send`.
|
||||
- M2: the same-copy exception allows `--no-send` without `--send` in the pointer.
|
||||
- M3: with `--no-send`, the pointer is written from `P_CONSOLE`, so it keeps `--send` while the unit
|
||||
drops it.
|
||||
- M4: rollback re-adds `--send`.
|
||||
- M5: the parser accepts `--no-send` for rollback.
|
||||
|
||||
Results: mutants-run.log, all five killed (M1 5 failures, M2 2, M3 7, M4 2, M5 2).
|
||||
|
||||
## Deviations from the brief
|
||||
|
||||
- The brief's line numbers were taken before row 163 landed. The candidate corrects them to the
|
||||
9ae8c73ac lines (check_target :249-256, flags_drift :646-657, console_for_target :664-693, adopt
|
||||
:828).
|
||||
- Test 5 also covers rehearse and `rollback --plan`, which the brief did not list.
|
||||
|
||||
## Outbound calls
|
||||
|
||||
None. The change edits an array before the existing unit render and pointer write. Every test uses
|
||||
the stand-in systemctl, units, stores and data root the file already builds; no test touches
|
||||
`~/.mosaic-dev`, the live units or a slot file.
|
||||
@@ -0,0 +1,5 @@
|
||||
M1 KILLED fail=5
|
||||
M2 KILLED fail=2
|
||||
M3 KILLED fail=7
|
||||
M4 KILLED fail=2
|
||||
M5 KILLED fail=2
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user