Compare commits
102
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e826c6dac8 | ||
|
|
78bd0d404b | ||
|
|
873992a9e2 | ||
|
|
39c821e7c0 | ||
|
|
34e1ab2bf5 | ||
|
|
057eebc46d | ||
|
|
55760b938b | ||
|
|
3cf210d00e | ||
|
|
1741bf720c | ||
|
|
d774c2626a | ||
|
|
de018df873 | ||
|
|
3dee8c4a73 | ||
|
|
0242780bac | ||
|
|
ad2be8ae45 | ||
|
|
a06e8f000a | ||
|
|
e2e7c60e56 | ||
|
|
14ad3baadc | ||
|
|
7703865349 | ||
|
|
8ca09fedf7 | ||
|
|
ef7ce90895 | ||
|
|
5c67029398 | ||
|
|
f0f9ff89c4 | ||
|
|
d3fa3a0ada | ||
|
|
fb8cabc649 | ||
|
|
c80230196d | ||
|
|
860e2012f5 | ||
|
|
9f3d724dff | ||
|
|
cb5700b617 | ||
|
|
2e91ddef65 | ||
|
|
88f7ebe0b6 | ||
|
|
e44598b48d | ||
|
|
7c00c83450 | ||
|
|
35130c26bb | ||
|
|
2f488b4ea7 | ||
|
|
660d25e656 | ||
|
|
15c09fd7b5 | ||
|
|
ebb3d83bf6 | ||
|
|
a11a9926cd | ||
|
|
5d8f0f4b54 | ||
|
|
ef2098c967 | ||
|
|
bb7c028852 | ||
|
|
de135fc029 | ||
|
|
ef63393ee4 | ||
|
|
bbca2935ba | ||
|
|
191048fbec | ||
|
|
4a3dc25763 | ||
|
|
bef9a62f4c | ||
|
|
a0a00630ea | ||
|
|
efd6c1f028 | ||
|
|
02bb0afb3a | ||
|
|
387f04dfda | ||
|
|
7a542c0ea2 | ||
|
|
858f773625 | ||
|
|
e10a6f1166 | ||
|
|
bba870ee9c | ||
|
|
2fb6815963 | ||
|
|
57da6fda51 | ||
|
|
361bd753fa | ||
|
|
69d5b39600 | ||
|
|
4e931c4f1f | ||
|
|
d2344ff8bf | ||
|
|
3ab3c010b1 | ||
|
|
deda9f48e8 | ||
|
|
433a95292e | ||
|
|
7a9a90a7a2 | ||
|
|
2f00563249 | ||
|
|
2a216fd169 | ||
|
|
928405649c | ||
|
|
2dfc1b9ad6 | ||
|
|
5bd27e804b | ||
|
|
449c3458ff | ||
|
|
82f50d1273 | ||
|
|
2beb49daeb | ||
|
|
d37be6ff29 | ||
|
|
a5643f9bd7 | ||
|
|
9705a6aaca | ||
|
|
1f404c973b | ||
|
|
b2793f2aa1 | ||
|
|
f942bd7d43 | ||
|
|
425900b036 | ||
|
|
85a72842c3 | ||
|
|
19bb3ae364 | ||
|
|
216302d65f | ||
|
|
0d0df1ecee | ||
|
|
dba33aaf60 | ||
|
|
0597cc1988 | ||
|
|
c7a672d622 | ||
|
|
04e78354a6 | ||
|
|
fcac66f7bf | ||
|
|
ca6b32d668 | ||
|
|
5a02ee3954 | ||
|
|
e1e64ff011 | ||
|
|
6d2e502728 | ||
|
|
c9d65664af | ||
|
|
18f5daaaef | ||
|
|
822bf1aa04 | ||
|
|
162ce46dbc | ||
|
|
dada066753 | ||
|
|
9afb947c73 | ||
|
|
8d53b93c78 | ||
|
|
365a763122 | ||
|
|
f9d218ab1e |
@@ -16,17 +16,6 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
|
||||
| Rocko | General development, investigation, testing, and review | T3 session (Codex, gpt-6.1-sol, thread d99326cc, lead decision 122; Claude thread b84bb264 retired 2026-10-12); launcher Claude Code, Sonnet model | `agents/rocko/launch.sh` |
|
||||
| Filbert | General development, investigation, testing, and review | T3 session (Claude, Opus 5.5, thread 048f3c63, non-sandbox reviews only, lead decisions 130 and 134; Codex threads f712d8af and 50342f6a retired 2026-10-13, Claude thread 9cb9731e retired 2026-10-12); launcher Pi, `openai-codex/gpt-6-astra:low`, not to be used under R26 until it changes (lead decision 69) | `agents/filbert/launch.sh` |
|
||||
| Researcher | Source-grounded technical research and evidence | T3 session (Claude, Sonnet 5.5, thread 9bac0794, rows 97 and 98, lead decision 124); Haiku rows (71, 80, 84, 88, later 96) run as subagents Sage dispatches with model haiku, effort high and a worktree (lead decision 129); Haiku thread c98aaf4a retired 2026-10-13 unused; launcher Pi, configured Mosaic model | `agents/researcher/launch.sh` |
|
||||
| Launchpad | Builder, MVP T1 row 192 (Q175 SSO phase 1) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| 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` |
|
||||
|
||||
Each script supports `--check` and `--fresh`. Normal launches resume the agent's
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Bushroot
|
||||
|
||||
Builder seat, Claude Haiku 5.5, started by Sage under mos-1008 item 2 (a low-level model under Q177) for the row
|
||||
that retires the control board writer on 7331 (#1696).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `bushroot/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,872 +0,0 @@
|
||||
# Row 195 design: the Case 1 install on 10.1.1.53 (r6)
|
||||
|
||||
Darkwing, 2026-10-20. Brief: `docs/plans/2026-10-20_hub-own-uid.md`, blob 45be7f09afc1 (f24f2cd40,
|
||||
sage-1439). Gitea #1693. Reviewer Duckworth, gate owner Sage. Chained on row 190's candidate (round 3,
|
||||
df65a3d4c on `darkwing/row190-r3`, rebased on row 192; the config canon) and on row 230's candidate
|
||||
(C2-13a, #1732, Gosalyn: `scripts/case2-unit.sh` and `case2-precheck.sh`), Q177, sage-1499. Row 230's
|
||||
brief is `docs/plans/2026-10-20_case2-rows.md` "C2-13a" at 5b8def2f1, from Gosalyn's design r11
|
||||
(eac92d956 on `gosalyn/case2-design`, approved webby-0038). It has no candidate yet, so I read that
|
||||
brief as the interface and expect a rebase. Section 2 lists what Case 1 needs from it that the brief
|
||||
doesn't give.
|
||||
|
||||
Changes from r5 (Duckworth's REVIEW-r4.md at 62200362b on `duckworth/reviews`, duckworth-0054). This
|
||||
round changes only B1's and B2's lines, their section 7 cases and the notes:
|
||||
|
||||
- B1. The result line `confirm` writes said op `confirm`, because `log_line` (:58) writes `$OP` and
|
||||
`OP="$1"` (:1493). `from_log` (:169) reads only `adopt`, `activate` and `rollback`, so it would have
|
||||
skipped every .53 activation: after adopt C0 and re-pins to C1 and C2, `from_log previous` returns
|
||||
C0. Now A5's `ok` line and R4's line carry the op, from and to of the activation's `started` line,
|
||||
and the backup that A3 records in the rollback set. Real sequence 7 and a mutant that writes op
|
||||
`confirm`. Section 4.
|
||||
- B2. r5 recorded the migration after the migrate, while today's run sets TAKEN before
|
||||
`migrate-schema.mjs` (:1004-1005). A migrate that changed the store and then exited 1 would have
|
||||
left no record, so R2 would skip the restore and R4 would gate OLD on a half-migrated store. A3 now
|
||||
writes `MIGRATING` after `backup_store` returns and before the migrate. Real sequence 8 and a mutant
|
||||
that records after the migrate. Section 4.
|
||||
- N1. The marker step makes `$RS` with capture's exclusive `mkdir` (:416), and capture under
|
||||
`--units external` makes none. N2. `gate` takes `--since T`, root's clock before its A2f start, as
|
||||
its start floor. N3. Section 0 now has a read-only grep of `/etc/apparmor.d` for the word `bwrap`,
|
||||
and item 3 matches the word against a list of exactly one file. N4. Item 4 runs under
|
||||
`systemd-run` with the units' `NoNewPrivileges=yes`, as 2a' does. N5. A1's plan takes
|
||||
`--expect-from OLD` and refuses `pointer-differs`.
|
||||
- Not in this round: claude's path. gosalyn-0058 proposes that `--claude`, like `--node`, takes only a
|
||||
root-owned path outside the home, and `/srv/mosaic/.local/bin/claude` fails that. I asked Sage to
|
||||
rule and recommended moving claude under `/opt/mosaic-stack`. That change gets its own round.
|
||||
|
||||
Changes from r4 (Duckworth's REVIEW-r3.md at 9f4d5556b on `duckworth/reviews`; mos-1098; mos-1101;
|
||||
gosalyn-0049):
|
||||
|
||||
- B1. An activation now has exactly one result line, written at the end. A5's gate failure and A3's
|
||||
failure after its capture write no log line. Each writes a `FAILED` file next to `AWAITING-UNITS` and
|
||||
exits 1, so the activation stays open for R2 and R4, and R4 writes the result. A3 writes
|
||||
`AWAITING-UNITS` right after `started`. Refusals by `confirm`, `revert` and `gate` log no activation
|
||||
id, so a refused `confirm` can't close one either. There are real-sequence tests on one log, and
|
||||
mutants that put back r4's interim line, r4's marker order and a refusal that names the activation.
|
||||
Section 4 and section 7.
|
||||
- B2. The bwrap profile has an owner and a stop. `scripts/case1-hostcheck.sh` checks the profile file
|
||||
against the pin in `scripts/case1-pins.sh`, the two loaded profiles, the live label of a process
|
||||
bwrap runs, the statoverride and mode, that no other profile names bwrap, two sysctls and the port
|
||||
floor. It has three callers:
|
||||
1. A1 of every re-pin refuses `profile-drift`.
|
||||
2. A root apt hook runs it after every package run, unattended ones included.
|
||||
3. A boot unit runs it before the bus starts.
|
||||
|
||||
On drift, callers 2 and 3 unload the profile and close `/usr/bin/bwrap` to `mosaic`, so every role
|
||||
session refuses to start. They write `DRIFT` and log it, and only root clears it. ops-01 owns the host
|
||||
side under a Mos go: it reads the log in each STATE report and, before each re-pin window, compares
|
||||
the upstream profile to the pin. A changed upstream profile becomes a new pin, which is its own review
|
||||
round. Section 1 has the details. The facts behind it are in section 0: unattended-upgrades runs daily
|
||||
on .53 with the security pocket, and the pinned bubblewrap is in that pocket.
|
||||
- N1 and mos-1098 point 2. P9 has a seventh item: no `mosaic-bwrap` profile is loaded, and no profile
|
||||
other than Ubuntu's attaches to `/usr/bin/bwrap`. r4 never installed `mosaic-bwrap`. F5 now compares
|
||||
exact strings. P9 reads `accepted mos-1096` only while all seven items hold.
|
||||
- N2: a role session gets no capabilities, so a nested bwrap fails. Section 1. N3: A3 refuses "before
|
||||
it logs `started`". N4: the stale-start rule and its fixtures handle `ActiveEnterTimestamp`'s whole
|
||||
seconds.
|
||||
- The port squat (mos-1101 item 5, gosalyn-0049). It applies: role sessions share the host's network
|
||||
namespace (`packages/seat/src/sandbox.mjs:67` unshares no net), so while the Console is down a
|
||||
session can bind its loopback port. The Console moves to 127.0.0.1:732 with
|
||||
`CAP_NET_BIND_SERVICE` on its unit (row 230's line), so no session can take it. The tester port stays
|
||||
7333, because serve refuses a tester port below 1024. That residue is P10, accepted under Mos's
|
||||
reading. Case 1 can't meet the preview trio or the proof pair, because .53 has no proxy. Section 2
|
||||
names the factory serve set for row 230.
|
||||
- Jason's Q9 (Vikunja #429) is still open. If install and upgrade become product commands, these
|
||||
steps become that installer's configuration and row 195 rebases on its row (sage-1491). Section 6.
|
||||
|
||||
## 0. What I checked on the host
|
||||
|
||||
Read only, as `localadmin` over ssh, 2026-10-20 between 20:40Z and 21:00Z, and again at 21:27Z for the
|
||||
last seven rows (the brief allows this):
|
||||
|
||||
| Item | 10.1.1.53 today | Needed |
|
||||
|---|---|---|
|
||||
| OS | Ubuntu 24.04.5, kernel 6.8.0-124, x86_64, 12 cores, 94 GiB, 217 GiB free on `/` | fits |
|
||||
| `mosaic` user, group, `/srv/mosaic` | absent | step 2a creates them |
|
||||
| node | `/usr/local/bin/node` v22.22.2, root-owned | 24 or later (every `packages/*/package.json` engines line); dragon-lin runs 26.8.1 |
|
||||
| bwrap | absent, no statoverride; apt candidate `bubblewrap 0.9.0-1ubuntu0.3` | every role session runs under `/usr/bin/bwrap` (`packages/seat/src/sandbox.mjs:43`) |
|
||||
| user namespaces | `kernel.apparmor_restrict_unprivileged_userns = 1`, apparmor 4.0.1, no bwrap profile loaded | bwrap needs a user namespace, which this setting refuses unless a profile allows it |
|
||||
| `apparmor-profiles` | not installed; candidate `4.0.1really4.0.1-0ubuntu0.24.04.9`, whose `/usr/share/apparmor/extra-profiles/bwrap-userns-restrict` has sha256 `11d39094f044f0cda0febb3ad517b830301da6b2ce929664af09ee9e4dd264f9` (read from the deb on dragon-lin) | section 1 |
|
||||
| `fs.protected_hardlinks`, `fs.protected_symlinks` | 1, 1 | mos-1096 item 2 |
|
||||
| journald | `SplitMode=uid` (the default) | section 4, B2 |
|
||||
| `useradd` | `USERGROUPS_ENAB yes` | 2a passes `--user-group` anyway |
|
||||
| claude | not on localadmin's PATH | the factory's harness; dragon-lin runs 2.1.304 |
|
||||
| ports 7330-7333 | free (no listener) | tester |
|
||||
| `localadmin` | NOPASSWD sudo (#423 step 1, applied 20:05:47Z, mos-1066, mos-1077) | the root steps run through it |
|
||||
| `jarvis` | groups sudo, docker, lxd, adm | #423 step 4 removes all four (section 5, P5) |
|
||||
| unattended-upgrades | on: `APT::Periodic::Unattended-Upgrade "1"`, `apt-daily-upgrade.timer` enabled. Allowed origins `${distro_id}:${distro_codename}`, `-security`, ESM apps-security and ESM infra-security. No `Package-Blacklist` | B2: packages change daily without a window |
|
||||
| bubblewrap's pocket | `0.9.0-1ubuntu0.3` is in both `noble-updates` and `noble-security`, so its later security updates install unattended | B2 |
|
||||
| apparmor | installed `4.0.1really4.0.1-0ubuntu0.24.04.8`. The `.9` candidate is in `noble-updates` only, which unattended-upgrades doesn't take. `aa-status --json` works | B2 |
|
||||
| profiles that name bwrap | at 21:51:46Z `grep -rlw bwrap /etc/apparmor.d` and `grep -rlF /usr/bin/bwrap /etc/apparmor.d` both find nothing (exit 1). After 2a' the word grep lists only `bwrap-userns-restrict` | N1, P9 item 7, hostcheck item 3 |
|
||||
| `net.ipv4.ip_unprivileged_port_start` | 1024 | the port squat |
|
||||
| listeners below 1024 | 22 (sshd) and 53 (the resolver) only; 732 is free | the Console's port (section 1) |
|
||||
| other listeners | 4003, 6052, 6055, 8123, 8765, 14242, 18555, and on loopback 1933, 5905, 6085, 6379, 6380, 9119, 9225, 18554, 18789, 38147 | none in 7330-7333 |
|
||||
|
||||
## 1. The install as inputs to the Case 2 scripts
|
||||
|
||||
Instance. Row 190's canon with the default name, `dev`, as Case 2 uses it (Gosalyn design 9). `~` is
|
||||
`mosaic`'s home, `/srv/mosaic`, so:
|
||||
|
||||
| Input | Value |
|
||||
|---|---|
|
||||
| user | `mosaic`, primary group `mosaic` with no other member |
|
||||
| home | `/srv/mosaic`, 0700, owner `mosaic` |
|
||||
| config dir | `/srv/mosaic/.config/mosaic-dev` |
|
||||
| data root | `/srv/mosaic/.mosaic-dev`, marker `instance.json` `{"instanceVersion":1,"name":"dev"}` |
|
||||
| business | `mosaic-stack`, file `<configDir>/businesses/mosaic-stack.json` (section 2) |
|
||||
| profile | `--profile factory` (row 230, C2-13b) |
|
||||
| roles | the factory set in section 2 |
|
||||
| human id | Jason's id, given in the window's go. Not `tester` (`broker.mjs:245`) |
|
||||
| model credential | `<configDir>/secrets/claude-session.env`, 0600 (row 190's `SESSION_ENV` for the default instance) |
|
||||
| token slot path | none for this row (section 2) |
|
||||
| Console port | 127.0.0.1:732, below the port floor, bound through the Console unit's `CAP_NET_BIND_SERVICE` (row 230). The port squat below says why |
|
||||
| tester port | 127.0.0.1:7333 (P10) |
|
||||
| board | none. UNITS has no board unit, and the Console gets no `--board` |
|
||||
| harness | claude at `/srv/mosaic/.local/bin/claude`, pinned through `MOSAIC_CLAUDE_BIN` (decision 91), which the bus unit's `@CLAUDE_ENV@` line sets (section 2) |
|
||||
| node | `/opt/mosaic-stack/node-<version>/bin/node`, a pinned 26.x release, `root:root`. Row 230 refuses a `--node` under the home or not owned by root |
|
||||
| PM | on: the bus unit's `@PM_FLAGS@` renders ` --pm` (section 2, section 4) |
|
||||
| source | `/srv/mosaic/src/mosaic-stack` (`mosaic`'s checkout, section 4) and `/root/mosaic-stack` (root's) |
|
||||
|
||||
Why `dev` and not a named instance: one instance per OS user, and Case 2's rows already assume
|
||||
`/srv/mosaic/.config/mosaic-dev`. A second instance under `mosaic` on .53 would need a name, and row 190
|
||||
makes that a flag, not a move.
|
||||
|
||||
Why node lives under `/opt/mosaic-stack`: changing `/usr/local/bin/node` would change what Hermes and
|
||||
anything else on the host runs, and Q181 keeps Hermes running. r4 put node under `/srv/mosaic`, but row
|
||||
230 requires a root-owned `--node` outside the home. Nothing else on the host puts `/opt/mosaic-stack`
|
||||
on its PATH.
|
||||
|
||||
PATH for `mosaic`'s steps. `sudo -u mosaic -H` resets PATH to sudo's `secure_path`, which finds
|
||||
`/usr/local/bin/node`, v22. The precheck would then refuse, and `scripts/mosaic` would run v22 (it runs
|
||||
`exec node` from PATH, `scripts/mosaic:22-40`). So every `mosaic` step in the run sheet and in
|
||||
`case1-repin.sh` runs as `sudo -u mosaic -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin ...`, the
|
||||
list row 230 renders as the units' `@PATH@`. `case1-repin.sh` builds it from the `--node` in
|
||||
`case1-unit.args`.
|
||||
|
||||
The units (row 230, design r11). Two system units, from the existing templates, in start order:
|
||||
`[email protected]` from `packages/cli/systemd/mosaic-bus.service.in`, then
|
||||
`mosaic-webui.service` from `packages/webui/systemd/mosaic-webui.service.in`. Root stops them in the
|
||||
reverse order. `@REPO@` is the copy dir, and `%h` becomes the literal home. Row 230 adds
|
||||
`ProtectSystem=strict`, `ReadWritePaths=/srv/mosaic`, `ProtectHome=yes` and `PrivateTmp=yes` to both, and
|
||||
`AmbientCapabilities=CAP_NET_BIND_SERVICE` with `CapabilityBoundingSet=CAP_NET_BIND_SERVICE` to the
|
||||
Console only. The Console unit has no `[Install]`, so root starts it by hand after a reboot until Jason
|
||||
rules (mos-1097). The install call is:
|
||||
|
||||
```
|
||||
case2-unit.sh --profile factory --user mosaic --home /srv/mosaic --business mosaic-stack --copy ID \
|
||||
--node /opt/mosaic-stack/node-<version>/bin/node --pm --claude /srv/mosaic/.local/bin/claude \
|
||||
-- --port 732 --business mosaic-stack --tester-port 7333
|
||||
```
|
||||
|
||||
`--pm` and `--claude` are the two inputs row 230's brief doesn't have yet (section 2). A re-install
|
||||
must pass the same `--node`, flags and serve flags, and only the copy id may change (row 230's
|
||||
re-install check). So 2c writes the whole argument list, without `--copy`, to
|
||||
`/etc/mosaic-stack/case1-unit.args`, `root:root` 0644, one argument per line. Every later
|
||||
`case2-unit.sh` call in section 4 reads that file and adds `--copy`. `mosaic` can't write it. Changing
|
||||
a serve flag is a re-install with its own go, not a re-pin.
|
||||
|
||||
The port squat (mos-1101 item 5). Role sessions share the host's network namespace:
|
||||
`packages/seat/src/sandbox.mjs:67` unshares the user, PID and IPC namespaces and not the network. So
|
||||
while the Console is down, which A2 to A4 of every re-pin and any crash before `Restart=on-failure`
|
||||
brings it back both cause, a session can bind 127.0.0.1:7332. It would then receive Jason's `ssh -L`
|
||||
traffic, and it would answer the gate's `/healthz` (`stack-release.sh:485-487`) in the Console's place.
|
||||
At 732 that can't happen. The floor is 1024 (section 0), a session has no capabilities (below), and
|
||||
`mosaic`'s own processes outside the sandbox have none either. Only the Console unit holds
|
||||
`CAP_NET_BIND_SERVICE`. Its children, the human-cli and read scripts it spawns
|
||||
(`packages/webui/src/bus.mjs:31`, `reads.mjs:79`), inherit the ambient capability. No role session
|
||||
descends from the Console: sessions are children of the bus unit, which has no capability line. The
|
||||
gate needs no change, because `console_port` (`stack-release.sh:213-215`) reads the Console's `--port`
|
||||
from the pointer. Row 230's precheck refuses when the floor is at or below the Console's port, and
|
||||
`case1-hostcheck.sh` checks it after every package run (B2). The tester port can't go below 1024,
|
||||
because serve refuses that (`packages/webui/src/cli.mjs:33`), so P10 records it.
|
||||
|
||||
Node download (N4). The run sheet pins one release, its tarball name and its sha256. As root, in a
|
||||
root-owned temp dir: fetch the tarball, `SHASUMS256.txt` and `SHASUMS256.txt.asc`; `gpg --verify` the
|
||||
`.asc` against the Node release keys listed in the nodejs/release-keys repository, imported into a
|
||||
keyring under `/etc/mosaic-stack/node-keys`; check that the tarball's line in the verified file is the
|
||||
pinned sha256; then `sha256sum -c` the tarball and unpack it to `/opt/mosaic-stack/node-<version>`,
|
||||
`root:root`, no file writable by group or other. Any mismatch stops the step. Rollback: `rm -r` that
|
||||
dir.
|
||||
|
||||
Claude (N5). The run sheet pins the version, 2.1.304 unless the window's plan names a newer one. As
|
||||
`mosaic`: `npm install --prefix /srv/mosaic/.local @anthropic-ai/claude-code@<pin>`, with `@<pin>`
|
||||
exact. `/srv/mosaic/.local/bin/claude` is the path `MOSAIC_CLAUDE_BIN` names. The autoupdater stays off
|
||||
through `/etc/claude-code/managed-settings.json`, `root:root` 0644, holding
|
||||
`{"env":{"DISABLE_AUTOUPDATER":"1"}}`. Root writes it in 2a'. Why there:
|
||||
|
||||
- `ENV_ALLOW` (`packages/seat/src/session.mjs:83-88`) doesn't pass `DISABLE_AUTOUPDATER`, so a unit
|
||||
`Environment=` line never reaches a role session (gosalyn-0046 item 3).
|
||||
- A role session's HOME is the run's own `home/`, and the sandbox doesn't show `~/.claude`
|
||||
(`session.mjs:92-93`, `sandbox.mjs:16`). So `mosaic`'s claude settings file doesn't reach one either.
|
||||
- The sandbox mounts `/etc` read-only (`sandbox.mjs:85`), and Claude Code reads managed settings from
|
||||
`/etc/claude-code/` whatever HOME is. `mosaic`'s own CLI runs, like the precheck's marker prompt
|
||||
and F6, read it too.
|
||||
|
||||
The pinned binary is a read-only bind inside the sandbox (`sandbox.mjs:9-10`), so a session can't
|
||||
rewrite it anyway. The managed file covers `mosaic`'s runs outside the sandbox. If the installed
|
||||
version doesn't honour `env` there, F6 catches the drift. Rollback: `rm` the file.
|
||||
|
||||
bwrap and the user namespace (mos-1091 4a, 4b; mos-1096 item 2; N1). Three root steps in 2a', in this
|
||||
order, each with its rollback. The `mosaic` group must exist first (2a).
|
||||
|
||||
1. `apt-get install bubblewrap=0.9.0-1ubuntu0.3`, the version the run sheet pins. Then record
|
||||
`stat -c '%U:%G %a' /usr/bin/bwrap` to `/etc/mosaic-stack/bwrap.orig` (I expect `root:root 755`).
|
||||
Rollback: `apt-get remove bubblewrap`.
|
||||
2. Ubuntu's own `bwrap-userns-restrict`, one file, not the package. `apt-get download
|
||||
apparmor-profiles=4.0.1really4.0.1-0ubuntu0.24.04.9` into a root-owned temp dir, extract
|
||||
`./usr/share/apparmor/extra-profiles/bwrap-userns-restrict` with `dpkg-deb --fsys-tarfile`, check
|
||||
its sha256 is `11d39094f044f0cda0febb3ad517b830301da6b2ce929664af09ee9e4dd264f9`, and install it as
|
||||
`/etc/apparmor.d/bwrap-userns-restrict`, `root:root` 0644. Load it with `apparmor_parser -r
|
||||
/etc/apparmor.d/bwrap-userns-restrict`. `aa-status` then lists `bwrap` and `unpriv_bwrap` in
|
||||
enforce mode. Why one file: the package also installs 20 profiles under `/etc/apparmor.d`
|
||||
(`usr.sbin.dnsmasq`, `bin.ping`, `usr.sbin.smbd` and more), which would load at the next boot and
|
||||
could change services on the host that have nothing to do with Mosaic.
|
||||
What the profile does: `bwrap` gets `userns`, `capability` and `mount` for its own setup, and
|
||||
every program it runs is stacked under `unpriv_bwrap`, which has the same file and network rules
|
||||
with `audit deny capability`. So a role session gets a user namespace and no capabilities in it.
|
||||
The sysctl stays at 1, so nothing else on the host gains user namespaces.
|
||||
One consequence (N2): a bwrap that a session starts inside its own sandbox fails, because it needs
|
||||
capabilities in the namespace. Claude Code's own sandbox is such a bwrap, and it's off by default,
|
||||
so a role session doesn't use it. F3 shows a session still answers under the profile.
|
||||
Rollback: `apparmor_parser -R /etc/apparmor.d/bwrap-userns-restrict`, then `rm` it.
|
||||
3. `dpkg-statoverride --update --add root mosaic 0750 /usr/bin/bwrap` (Mos). Only root and the
|
||||
`mosaic` group can read or run bwrap, and package upgrades keep the mode. Rollback:
|
||||
`dpkg-statoverride --remove /usr/bin/bwrap`, then `chown` and `chmod` to the values in
|
||||
`bwrap.orig`.
|
||||
4. The drift stop (B2, below): `case1-hostcheck.sh` and `case1-pins.sh` from `/root/mosaic-stack` to
|
||||
`/usr/local/lib/mosaic-stack/`, `root:root` 0755 and 0644; the apt hook
|
||||
`/etc/apt/apt.conf.d/80mosaic-hostcheck`; the boot unit `mosaic-hostcheck.service`, enabled. Then
|
||||
`case1-hostcheck.sh --check` exits 0. Rollback: disable and remove the unit, `rm` the hook file and
|
||||
the dir.
|
||||
|
||||
Full rollback runs 4, 3, 2, 1. Tests after 2a' and again in section 5: as `mosaic`, `bwrap --unshare-all
|
||||
--ro-bind / / true` exits 0 (row 230's precheck line, F4); as `jarvis`, `/usr/bin/bwrap --version`
|
||||
fails with "Permission denied" (P9); `getent group mosaic` lists no member and no other account has
|
||||
gid `mosaic` (P9); `fs.protected_hardlinks` is 1 (P9). A role session running under the profile is F5.
|
||||
|
||||
One more test runs in 2a', because I can't settle it from here. Row 230's units keep the templates'
|
||||
`NoNewPrivileges=yes`, and AppArmor limits profile changes for a task with no-new-privs set. The
|
||||
`bwrap` attach happens on exec from an unconfined task, and the child's change is a stack
|
||||
(`bwrap//&unpriv_bwrap`), which only narrows. I expect both to pass under no-new-privs, since bwrap
|
||||
sets no-new-privs on itself before it runs the child, and Ubuntu's profile works with that. But I
|
||||
haven't seen it run, and .53 is read-only to me. So as root, after step 4: `systemd-run --wait --pipe
|
||||
--uid=mosaic --gid=mosaic -p NoNewPrivileges=yes -p ProtectSystem=strict -p ProtectHome=yes -p
|
||||
PrivateTmp=yes /usr/bin/bwrap --unshare-user --unshare-pid --ro-bind / / cat /proc/self/attr/current`
|
||||
prints `bwrap//&unpriv_bwrap (enforce)`. If it fails, the window stops at 2a' and rolls back 4 to 1,
|
||||
before any `mosaic` step. That beats finding it at F5.
|
||||
|
||||
The drift stop (B2, Duckworth's REVIEW-r3 B2, mos-1098 point 1). r4 installed a profile file that no
|
||||
package owns, from a pinned deb, onto a host where unattended-upgrades changes packages every day
|
||||
(section 0). Three things can move under it: bubblewrap (its security updates install unattended), the
|
||||
`apparmor` package and its parser, and any new profile a package ships that attaches to
|
||||
`/usr/bin/bwrap`. Two more host values matter to this design: the user-namespace sysctl and the port
|
||||
floor. So:
|
||||
|
||||
- `scripts/case1-pins.sh` holds the pins, sourced by the scripts below: the `apparmor-profiles` version
|
||||
the file came from, the file's sha256, and the bubblewrap version 2a' installs. Changing a pin is a
|
||||
reviewed commit and its own round. The bubblewrap version is an install pin, not a drift item:
|
||||
a security update to bubblewrap keeps the statoverride, and the profile attaches by path.
|
||||
- `scripts/case1-hostcheck.sh` checks seven items and prints one line each:
|
||||
1. `/etc/apparmor.d/bwrap-userns-restrict` has the pinned sha256, and `dpkg -S` finds no package
|
||||
that owns it.
|
||||
2. `aa-status --json` lists `bwrap` and `unpriv_bwrap` in enforce mode, and no loaded profile named
|
||||
`mosaic-bwrap`.
|
||||
3. `grep -rlw bwrap /etc/apparmor.d` lists exactly `/etc/apparmor.d/bwrap-userns-restrict`. The word
|
||||
match also catches a path written as `/{usr/,}bin/bwrap`. Section 0's read found no file today.
|
||||
4. The live label: 2a' line `systemd-run --wait --pipe --uid=mosaic --gid=mosaic -p NoNewPrivileges=yes
|
||||
-p ProtectSystem=strict -p ProtectHome=yes -p PrivateTmp=yes /usr/bin/bwrap --unshare-user
|
||||
--unshare-pid --ro-bind / / cat /proc/self/attr/current` prints exactly `bwrap//&unpriv_bwrap
|
||||
(enforce)`. It runs under the units' no-new-privs, so it reads the label a role session gets.
|
||||
This catches a profile that attaches by a glob that item 3's search misses.
|
||||
5. `dpkg-statoverride --list /usr/bin/bwrap` reads `root mosaic 0750`, and `stat` agrees.
|
||||
6. `kernel.apparmor_restrict_unprivileged_userns` is 1 and `fs.protected_hardlinks` is 1.
|
||||
7. `net.ipv4.ip_unprivileged_port_start` is above the Console's `--port` in the installed unit.
|
||||
|
||||
`--check` exits 0 when all hold and 1 otherwise. `--enforce` runs the same check and always exits 0.
|
||||
On a failure, or while `/etc/mosaic-stack/DRIFT` exists, it applies the stop:
|
||||
`apparmor_parser -R` on the profile if loaded, then `chown root:root` and `chmod 0700` on
|
||||
`/usr/bin/bwrap`. It writes the failed items to `DRIFT` and appends a line to
|
||||
`/var/log/mosaic-stack/hostcheck.log`. With bwrap closed to `mosaic`, every role session refuses
|
||||
to start. That's the fail-closed state, and the stack needs no change for it. The chmod is undone by
|
||||
the next bubblewrap upgrade, which reapplies the statoverride. But the apt hook runs again right
|
||||
after that upgrade and finds `DRIFT`, so it closes bwrap again. Only root removes `DRIFT`, in a
|
||||
reviewed step with a new pin or a Mos go. I don't call `dpkg-statoverride` from inside the hook,
|
||||
because I haven't confirmed it can take dpkg's lock while apt holds the frontend lock.
|
||||
- Three callers. A1 of every re-pin runs `--check` and refuses `profile-drift` on exit 1. The apt hook
|
||||
is `DPkg::Post-Invoke { "/usr/local/lib/mosaic-stack/case1-hostcheck.sh --enforce || true"; };`, so it
|
||||
runs after every package run, unattended ones included, and can't fail one. The boot unit,
|
||||
`mosaic-hostcheck.service` (`Type=oneshot`, `After=apparmor.service`,
|
||||
`Before=mosaic-bus@mosaic-stack.service`, `WantedBy=multi-user.target`), runs `--enforce` before the
|
||||
bus starts, which covers a kernel or parser change that takes effect at boot.
|
||||
- Owner. ops-01 owns the host side under a Mos go. It reads `hostcheck.log` in each STATE report and
|
||||
sends any `DRIFT` to Mos. Before each re-pin window it also runs `--upstream`, which downloads the
|
||||
current `apparmor-profiles` candidate into a temp dir and compares its `bwrap-userns-restrict` to the
|
||||
pin. A difference goes to Mos as a new-pin request and refuses nothing. The pins and the scripts are
|
||||
this row's, and later changes go through review like any other.
|
||||
|
||||
Who runs what:
|
||||
|
||||
| Step | Runs as | Through |
|
||||
|---|---|---|
|
||||
| 2a: `useradd --system --user-group --home-dir /srv/mosaic --shell /usr/sbin/nologin mosaic`, `/srv/mosaic` 0700, `/etc/mosaic-stack` `root:root` 0755, `/var/log/mosaic-stack` `root:root` 0750 | root | ops-01, `ssh localadmin@10.1.1.53`, `sudo` (#423 step 1) |
|
||||
| 2a': node under `/opt/mosaic-stack`; bubblewrap, the AppArmor profile, the statoverride, the drift stop and the managed settings file, each with the test and rollback above; the no-new-privs test | root | ops-01, same path, R27 (mos-1091) |
|
||||
| 2a'': a `git bundle` of the reviewed commit copied to .53; root clones it to `/root/mosaic-stack` and checks HEAD is the commit the go names; it installs the bundle as `/srv/mosaic/incoming/<sha>.bundle`, owner `mosaic`, 0600 | root | ops-01, same path |
|
||||
| 2b: claude under `~/.local` (above); `git clone` of the bundle to `/srv/mosaic/src/mosaic-stack`; `scripts/bootstrap.sh`; `stack-copy.sh copy`; the install with the inputs above (C2-13b's `case2-install.sh --profile factory` if it has landed, else the run sheet's steps) | `mosaic` | `sudo -u mosaic -H env PATH=...` from `localadmin` (PATH above). `mosaic` gets no key and no login shell |
|
||||
| 2b': place `claude-session.env` | Jason, or who he names (Q173 secrets rule) | section 3 |
|
||||
| 2c: write `case1-unit.args`; `case2-unit.sh` with those arguments and `--copy <id>`, from `/root/mosaic-stack`; then `case2-precheck.sh --profile factory` as `mosaic`, which reads the Console's `--port` from the installed unit for its port-floor line, so it runs after the render; `systemctl enable` the bus unit; the first start of UNITS. The Console unit has no `[Install]` until Jason rules (C2-13a), so it isn't enabled, and after a host reboot root starts it by hand until then. The run sheet says so | root | ops-01, same path |
|
||||
| 2c': `stack-release.sh adopt mosaic-stack --units external`, which records the running copy as the pointer (`do_adopt`, `scripts/stack-release.sh:856`) | `mosaic` | `sudo -u mosaic -H` |
|
||||
| every later re-pin or rollback | root, with its `mosaic` steps through `sudo -u mosaic -H` | ops-01 runs `case1-repin.sh` (section 4) |
|
||||
| acceptance (section 5) | `localadmin`, with `sudo -u mosaic -H` for the in-instance checks | the agent the window names |
|
||||
|
||||
No git credential lands on .53. Both checkouts come from bundles that ops-01 copies in, and a re-pin
|
||||
brings a new bundle that `mosaic` fetches into its checkout.
|
||||
|
||||
## 2. Where Case 1 differs from Case 2
|
||||
|
||||
| Case 2 item (row 230 brief) | Case 1 | Reason |
|
||||
|---|---|---|
|
||||
| business file with the library mapping | business `mosaic-stack` with no `library` key; `--profile factory` refuses a `kind: "library"` project (`profile-library-business`) | the factory has no books repo |
|
||||
| `bean`, `chet`, persona vars and files, specialists | factory roles from `roles/`, with a chief that has a harness var | Jason names the set in the go. I haven't read his live business file and won't. The acceptance needs only the chief (F3), since a chief with no harness var answers `no-route`. Others are added later by the human with `instances.set`, no new row |
|
||||
| token slot `library-gitea` | none | the factory's per-agent git identity is row 199's credential store (Megavolt, #1697). Until it lands, git work stays on dragon-lin (Q182) |
|
||||
| precheck: `bus.tester` is not on | `bus.tester` is on; `factory` requires it | the acceptance and the Q173 test system need the tester (agents act as tester, never Jason) |
|
||||
| precheck: persona file modes and digests | skipped under `factory` | no persona files |
|
||||
| precheck: `library.sync`, `library.status` | skipped under `factory` | no library |
|
||||
| precheck: `ZAI_API_KEY` in the key file | doesn't run | no instance has provider `zai` |
|
||||
| precheck: no Discord binding | kept | the factory's Discord connector stays on dragon-lin. Two connectors on one bot token would split its events |
|
||||
| precheck: claude marker prompt with `--system-prompt-file` | runs, since the chief's harness is claude-code | |
|
||||
| precheck: `sudo -n true` fails, `id -G` one entry, run root 0700, config validates, node 24 or later, `bwrap` exits 0, free memory, HOME | kept | node and bwrap are in row 230 (gosalyn-0034) |
|
||||
| Console serve flags: `--port` 1..1023, `--business`, the preview trio, `--send`, the proof pair, no `--tester-port` | `--port 732`, `--business mosaic-stack`, `--tester-port 7333`. No preview trio, no `--send`, no proof pair | below |
|
||||
| bus: `@PM_FLAGS@` and `@CLAUDE_ENV@` render empty | ` --pm`, and `Environment=MOSAIC_CLAUDE_BIN=/srv/mosaic/.local/bin/claude` | below |
|
||||
|
||||
All of these are inputs. Nothing in the Case 2 scripts hardcodes a Case 1 value (gosalyn-0034 item 4).
|
||||
Row 230's brief says the factory profile takes the library serve set plus an optional `--tester-port`,
|
||||
and that row 195 names any other difference (gosalyn-0049). .53 can't meet the library set, and the bus
|
||||
needs two lines the brief renders empty. The factory profile I need from row 230:
|
||||
|
||||
1. Serve flags. `--port N` with N in 1..1023 and `--business ID` are required, and `--tester-port N` in
|
||||
1024..65535, not equal to `--port`, is optional. The preview trio (`--public-host`, `--auth-header`,
|
||||
`--allow-user`) and the proof pair (`--proof-header`, `--proof-file`) are refused. Both exist for a
|
||||
proxy in front of the Console: the proxy signs the user in and adds the header, and the proof shows
|
||||
the request came through it. .53 has no proxy. Jason reaches the Console with `ssh -L` (sage-1451
|
||||
Q3), so there is no header to trust and no proxy to hold the proof. `--send` is refused too, since
|
||||
serve takes it only with the trio or `--sso` (`packages/webui/src/cli.mjs:15`). When Q175 SSO reaches
|
||||
.53, `--sso` with an optional `--send` (row 192) would be the factory's sign-in, and adding it is a
|
||||
re-install with its own go. Row 230 can accept `--sso [--send]` under factory now or later. Either
|
||||
way it needs a `console.sso` block in `mosaic`'s config, which this row doesn't write. `--checkout`
|
||||
and `--board` stay refused, as in the library set.
|
||||
2. `--pm` (factory only) renders the bus unit's `@PM_FLAGS@` as ` --pm`. The factory's PM dispatches its
|
||||
work on .53 as on dragon-lin (section 4). With the token empty the bus never launches the PM, and
|
||||
A5's PM check fails every re-pin.
|
||||
3. `--claude PATH` (factory only) renders `@CLAUDE_ENV@` as `Environment=MOSAIC_CLAUDE_BIN=PATH`, the
|
||||
line `scripts/bus-service.sh:79` renders today. PATH must be absolute. Without it the bus runs
|
||||
`claude` from the unit's PATH, which is the node dir, `/usr/local/bin`, `/usr/bin` and `/bin`, and
|
||||
holds no claude on .53. The chief's first launch would then fail. The precheck's claude marker
|
||||
prompt should run the same binary, so it reads the path from the installed bus unit.
|
||||
4. Row 190's two template tokens, if row 190 lands before row 230. Row 190 adds `@CONFIG_ENV@` to both
|
||||
templates and `@SESSION_ENV@` to the bus template's `EnvironmentFile=` line. For the default
|
||||
instance, `@CONFIG_ENV@` is deleted (`scripts/instance.sh`) and `@SESSION_ENV@` is
|
||||
`<home>/.config/mosaic-dev/secrets/claude-session.env`. Row 230's "every token has one value" list
|
||||
and its `EnvironmentFile=` check need both. This applies to the library profile too.
|
||||
|
||||
I've sent these to Gosalyn. If row 230's candidate lacks any of them, row 195's manifest names the
|
||||
change to `case2-unit.sh` or `case2-precheck.sh`, reviewed by Duckworth with Webby told.
|
||||
|
||||
## 3. Model access
|
||||
|
||||
Q185: Case 1 uses the account the factory uses today, the Claude subscription behind dragon-lin's
|
||||
`claude-session.env`. The install never copies that file from dragon-lin. In the window, Jason (or the
|
||||
person he names under the Q173 secrets rule) makes a credential for .53 from that account and writes it
|
||||
to `/srv/mosaic/.config/mosaic-dev/secrets/claude-session.env` as `mosaic`, mode 0600. Nobody prints it,
|
||||
and no script reads its value. The precheck checks only its mode, owner and that the variable name is
|
||||
present (a `grep -c` on the name, output a count).
|
||||
|
||||
Quota: the factory on dragon-lin and on .53 share one subscription. Q185's "use existing" accepts that.
|
||||
Case 2 shares nothing with it (Mos's reading of Q185).
|
||||
|
||||
The pi harness stays off on .53 until P19 #383 settles (mos-0865). The business file names claude only.
|
||||
|
||||
## 4. Re-pin, rollback and backup on .53
|
||||
|
||||
Unit form (ruled, sage-1451 Q1): system units that root writes, with `User=mosaic`. `mosaic` can't
|
||||
start or stop them, and root never runs code that `mosaic` can write. `stack-release.sh activate` today
|
||||
does everything in one run under `systemctl --user` and puts the old stack back inside that run
|
||||
(`rollback_in_run`, `scripts/stack-release.sh:561`). On .53 the same run needs two users. So one
|
||||
re-pin is one ordered list, and one root command drives all of it: `scripts/case1-repin.sh`, run by
|
||||
ops-01 from `/root/mosaic-stack`. Each `mosaic` step is `sudo -u mosaic -H` running
|
||||
`/srv/mosaic/src/mosaic-stack/scripts/stack-release.sh` with `--units external`. Copies carry no
|
||||
`stack-release.sh` (`COPY_PATHS`, `scripts/stack-copy.sh:23-30`), so the `mosaic` steps run from that
|
||||
checkout, as dragon-lin's run from the canonical one.
|
||||
|
||||
What root holds, and where each value comes from (r4's B1):
|
||||
|
||||
| Value | Source |
|
||||
|---|---|
|
||||
| ID, the target copy | the go |
|
||||
| TO, a rollback's target | the go, as `--to ID` |
|
||||
| `--restore ACT --events N` | the go, when the rollback restores |
|
||||
| OLD, the running copy | the `ExecStart=` of the two unit files root installed. If the bus and Console units name different copies, A1 refuses `units-disagree` |
|
||||
| the `case2-unit.sh` arguments | `/etc/mosaic-stack/case1-unit.args` (section 1) |
|
||||
| each `mosaic` step's result | its exit code |
|
||||
|
||||
Root parses nothing `mosaic` wrote and runs nothing under `/srv/mosaic` except through `sudo -u mosaic
|
||||
-H`. It never needs the activation id: the `mosaic` steps find it themselves.
|
||||
|
||||
One result line per activation (B1). The stack log closes an activation on the first line after
|
||||
`started` that names it, whatever the result (`interrupted`, :146-156). r4 wrote an interim
|
||||
`... rollback pending` line at A3's and A5's failures. That line closed the activation, so R2 and R4
|
||||
then refused `open-activation` against an activation the log no longer held open. The rollback could
|
||||
never run as designed, and nothing tested the steps in sequence. r5 keeps an activation open from A3's
|
||||
`started` line until its single result line, and A5 or R4 writes that line:
|
||||
|
||||
| Activation ends at | Result line | Written by |
|
||||
|---|---|---|
|
||||
| A3 fails before its rollback set exists (marker or capture) | `refused code marker-failed` or `refused code capture-failed`, as `capture || refuse` does today (:987). Nothing has changed, so there is nothing to roll back | A3 itself, exit 3, after it removes the marker. Root runs A2f |
|
||||
| A5 passes | `ok` | A5 |
|
||||
| A3 (after capture), A4 or A5 fails, and the rollback passes | `failed code <c> rollback ok` for A3 and A4, `gate-failed code <c> rollback ok` for A5 | R4 |
|
||||
| the rollback fails at R4's gate | `rollback-failed code <c> rollbackCode <r>` | R4 |
|
||||
| R1, R2 or R3 fails, or the run dies | none. The activation stays open, and the next activate refuses `interrupted-run` (:603-606), which Jason or Mos settles as on dragon-lin | |
|
||||
|
||||
What the result line carries (r5's B1). `log_line` (:58) writes `op "$OP"`, and `from_log` (:169)
|
||||
reads only lines whose op is `adopt`, `activate` or `rollback`. So `confirm` doesn't log under its own
|
||||
op. Before A5's `ok` line or any line R4 writes, it sets OP, FROM and TO from the open activation's
|
||||
`started` line, ACT to that activation, and BACKUP from the rollback set's `BACKUP` file, which A3
|
||||
writes when `backup_store` returns. A re-pin's lines then read `op activate`, a rollback's `op
|
||||
rollback`, each with the from, to and backup a dragon-lin run would log. `from_log previous` and
|
||||
`from_log backup` read .53's log as they read dragon-lin's. `confirm` reads only the line A3 wrote for
|
||||
that activation, which `interrupted` already names.
|
||||
|
||||
A failing `mosaic` step after `started` writes no log line. It writes its code to `FAILED` beside
|
||||
`AWAITING-UNITS` and exits 1. R4 reads that code for the result line. Root passes R4 `--cause
|
||||
A3|A4|A5`, the step it saw fail, from its own exit codes. A4 is root's own step and has no `FAILED`
|
||||
file, so R4 writes `failed code units-start` for it. A cause with no `FAILED` file where one should be
|
||||
writes code `unrecorded`.
|
||||
|
||||
The open activation. A3 writes `$STATE/stack-rollback/<ACT>/AWAITING-UNITS` holding the target copy
|
||||
(renamed from r3's `PENDING`, which read like the pointer's `pmPending`, `P_PENDING` at :128; N3).
|
||||
`confirm` and `revert` take the copy root passed, not an activation id. Each lists the
|
||||
`AWAITING-UNITS` files and the `interrupted` set (:146), and goes on only when there is exactly one
|
||||
file, its activation is the one `interrupted` names, and the copy in it is the copy root passed.
|
||||
Anything else refuses `open-activation` with exit 3 and the list it found, and changes nothing.
|
||||
`confirm` without `--rollback` also refuses `activation-failed` when `FAILED` exists, so a second
|
||||
confirm can't pass an activation that already failed. Whichever `confirm` writes the result line
|
||||
removes `AWAITING-UNITS` and `FAILED` after it.
|
||||
|
||||
Refusals of the three external verbs. `confirm`, `revert` and `gate` act on an open activation by
|
||||
design, so they skip the `interrupted-run` refusal (:605). Their refusals log `result refused` with no
|
||||
activation id: they set ACT only after every check has passed. So a refused `confirm`, a wrong copy
|
||||
say, leaves the open activation open. Today's `refuse` (:62-67) adds the activation id whenever ACT is
|
||||
set, and the external verbs keep that order. A test and a mutant hold it (section 7).
|
||||
|
||||
Root's step log. Every step `case1-repin.sh` runs appends one line to `/var/log/mosaic-stack/case1-repin.log`,
|
||||
`root:root` 0640: the time from `date -u`, the step, its argv, its exit code. `mosaic` can't write it,
|
||||
so it stays readable after a run that dies before `mosaic` logs anything.
|
||||
|
||||
### Activate: `case1-repin.sh activate ID [--migrate] [--timeout S]`
|
||||
|
||||
| # | Step | Runner | On failure |
|
||||
|---|---|---|---|
|
||||
| A1 | `/root/mosaic-stack` HEAD is the commit the go names. ID is 40 lowercase hex and names a copy under `/srv/mosaic/.local/share/mosaic-stack/copies` that holds `scripts/mosaic` (row 230's copy-id rule). `case1-hostcheck.sh --check` exits 0, else `profile-drift` (B2). Root reads OLD from the unit files. `stack-release.sh activate ID mosaic-stack --units external --plan --expect-from OLD` exits 0. The plan refuses `pointer-differs` when the pointer's copy isn't OLD (N5), so a pointer and units that disagree stop before any stop | root; the plan as `mosaic` | refuse, exit 3. Nothing has changed |
|
||||
| A2 | `systemctl stop` UNITS in reverse order, then wait until each is inactive, within `--timeout` | root | A2f below |
|
||||
| A3 | `stack-release.sh activate ID mosaic-stack --units external [--migrate]`. Before it logs `started` (N3), it refuses `units-running` unless each of UNITS is inactive (system-manager reads, which need no privilege) and the store is stopped (`stopped`, :359), and `interrupted-run` while any activation is open. Then, in this order: logs `result started`; makes `$RS` with capture's `mkdir -p` of `stack-rollback` and exclusive `mkdir -m 0700` (:415-416), and writes `AWAITING-UNITS` in it at once (N1); captures the rollback set into that directory, making none (pointer, wrapper, and the sha256 and `ExecStart=` of each installed unit, read only); takes `backup_store` (:372) into `$DATA/backups/stack/<ACT>` and writes its name to `BACKUP` in the rollback set; under `--migrate`, writes `MIGRATING` in the rollback set and sets TAKEN, both before `migrate-schema.mjs` runs, where today's run sets TAKEN (:1004-1005, r5's B2), then migrates; installs the wrapper for ID. It writes no pointer | `mosaic` | a refusal before `started` exits 3 with nothing changed. A marker or capture failure, an existing `$RS` included, removes the marker if it wrote one, logs its `refused` line and exits 3. Root runs A2f for both. Any later failure writes `FAILED` with its code, logs nothing, cleans up nothing and exits 1. Root runs R1-R4 with `--cause A3` |
|
||||
| A4 | `case2-unit.sh` with `case1-unit.args` and `--copy ID`, `systemctl daemon-reload`, `systemctl start` UNITS in order | root | R1-R4 with `--cause A4` |
|
||||
| A5 | `stack-release.sh confirm ID mosaic-stack --units external --timeout S`. This is `gate` (:522), which polls `gate_once` (:471) every second until `--timeout`, with the external checks below. On a pass it writes the pointer, logs `result ok`, removes `AWAITING-UNITS`, exit 0. When the timeout runs out it writes `FAILED` with the last gate code, logs nothing and exits 1 | `mosaic` | R1-R4 with `--cause A5` |
|
||||
|
||||
Why A3 writes the marker first and does no cleanup of its own. With the marker written last, as in r4,
|
||||
an A3 failure after `started` left an open activation with no marker, and R2 and R4 refused it. With
|
||||
the marker first, every A3 failure after the marker leaves exactly what R2 needs: the open activation,
|
||||
its marker and whatever part of the rollback set exists. R2 is then the one cleanup path, for A3, A4
|
||||
and A5 alike. Two cleanup paths, A3's own and R2, would each need their own tests for each failure point.
|
||||
|
||||
A2f, a stop that fails or times out, or an A3 that exits 3. This run has no open activation: A2 logs nothing, and
|
||||
A3's exit 3 comes either before `started` or after its own `refused` line. Nothing of `mosaic`'s has
|
||||
changed. Root reads `date -u +%s` as T, runs `systemctl start` UNITS in order, then `stack-release.sh gate
|
||||
OLD mosaic-stack --units external --since T --timeout S`, a new verb that runs `gate` on OLD and writes no
|
||||
log line, no pointer and no file. A2f has no `started` line to take a start floor from (the first
|
||||
re-pin after adopt has none at all), so `--since` is the floor, and `gate` refuses `usage` without it
|
||||
(r5's N2). T is root's own clock, so root still parses nothing `mosaic` wrote. A unit that never
|
||||
stopped keeps its old `ActiveEnterTimestamp`, fails `stale-start` and takes the exit 2 path below.
|
||||
If that passes, root logs `A2 failed, OLD running` (or `A3 refused`) in its step log and exits
|
||||
1. If it fails, root stops UNITS, logs `OLD gate failed <code>` and exits 2. The next run's
|
||||
`interrupted` check finds nothing open either way.
|
||||
|
||||
The external gate. `gate_once` keeps its order and codes, with these changes under `--units external`:
|
||||
|
||||
- `systemctl` without `--user` for `is-active` and `show`. Both work unprivileged against the system
|
||||
manager.
|
||||
- This start's time is the bus unit's `ActiveEnterTimestamp`, read with `systemctl show --timestamp=unix
|
||||
-p ActiveEnterTimestamp`, in place of the `START_MS` that `start_both` (:451) sets on dragon-lin.
|
||||
It is whole seconds (`@1792540800`), and the `started` line has milliseconds (N4). So the rule
|
||||
compares seconds: the start passes when its second is at or after the floor of `started`'s second.
|
||||
A unit that was active across A3 can't reach this check, because A3 refuses `units-running`, so the
|
||||
equal second can only be a start after A3. An earlier second fails `gate_once` with the gate code
|
||||
`stale-start`. It's a gate failure like any other, not a refusal (r4 had it as a refusal, which
|
||||
would have logged against the open activation): `gate` keeps polling, since `Restart=on-failure`
|
||||
can bring a fresh start, and at the timeout `FAILED` holds `stale-start`. The PM check's
|
||||
`startedAt` bound uses the same second.
|
||||
- `copy-check` runs `stack-copy.sh check --units external`, which reads the system units.
|
||||
- The PM check is below.
|
||||
|
||||
The PM (r4's B2). .53 runs the PM: the factory's PM dispatches its work, as it does on dragon-lin, and the
|
||||
serve flags carry `--pm`. The pointer's `pm` comes from the installed bus unit's `ExecStart=`, as the
|
||||
unit parse at :195 already reads it. Today's `pm_launched` (:505) takes the run id from the
|
||||
`launched ...` line in `journalctl --user -u BUS_UNIT`. On .53 that read fails twice over: the unit is
|
||||
a system unit, and under `SplitMode=uid` the logs of a system-range uid land in the system journal,
|
||||
which `mosaic` (no `adm` or `systemd-journal` group) can't read. Under `--units external` the check
|
||||
reads `launches list --json` alone. It passes when exactly one entry has business `mosaic-stack`,
|
||||
instance equal to the business file's `launch.by` (the PM's instance, the one `bus start --pm` launches
|
||||
at `packages/cli/src/launcher.mjs:675`), `live` true, and `startedAt` at or after this start's time.
|
||||
Zero entries, two, a stale one or one for another business fail with `pm-launch-unconfirmed` and what
|
||||
it saw, as today. A run the PM starts for another role has another instance, so it doesn't count.
|
||||
|
||||
Why not the other reads. A regular uid for `mosaic` would put its logs in a per-user journal it can
|
||||
read, but it changes the account Case 2's install creates. Root reading the journal and passing the run id in
|
||||
would make root parse output from `mosaic`'s process, which section 4's rule forbids.
|
||||
|
||||
Under `--units external`, `pm-on` (:1137) and `activate --pm-after-harness` refuse `external-pm`. Both
|
||||
flip the PM on dragon-lin by restarting the user bus with new arguments. On .53 the PM's state is a
|
||||
serve flag in a root-owned unit, so changing it is a re-install with its own go.
|
||||
|
||||
### Rollback inside the run (R1-R4)
|
||||
|
||||
| # | Step | Runner | On failure |
|
||||
|---|---|---|---|
|
||||
| R1 | `systemctl stop` UNITS, then wait as in A2 | root | rollback-failed |
|
||||
| R2 | `stack-release.sh revert ID mosaic-stack --units external`. It finds the open activation as above, puts the wrapper back from the rollback set and checks its sha256, and restores TAKEN (`restore_store`, :396) only if the rollback set holds `MIGRATING`. Each part is idempotent, so it works for an A3 that stopped partway: no `MIGRATING` means no restore, a migrate that failed partway is restored from the backup taken before it, and copying the saved wrapper over an unchanged one changes nothing. It writes no log line | `mosaic` | rollback-failed, activation left open |
|
||||
| R3 | `case2-unit.sh` with `case1-unit.args` and `--copy OLD`, `systemctl daemon-reload`, `systemctl start` UNITS | root | rollback-failed, activation left open |
|
||||
| R4 | `stack-release.sh confirm ID mosaic-stack --units external --rollback OLD --cause A3\|A4\|A5 --timeout S`. It finds the open activation, checks OLD is its `from`, gates OLD with `gate`, and writes the activation's one result line from the cause and the `FAILED` code: `failed code <c> rollback ok` (A3, A4), `gate-failed code <c> rollback ok` (A5), or `rollback-failed code <c> rollbackCode <gate>` when OLD's gate fails | `mosaic` | rollback-failed, logged |
|
||||
|
||||
rollback-failed: root stops UNITS and exits 2, as `rollback_failed` (:587) does. When R4's gate fails,
|
||||
the log has the `rollback-failed` line and the activation is closed. When R1, R2 or R3 fails, no
|
||||
`mosaic` step writes a result, so the activation stays open with its marker. Root's step log has the
|
||||
codes, the next activate refuses `interrupted-run` until Jason or Mos settles it (:605), and status
|
||||
shows it. I chose that over a log-only verb for root to call after an R2 failure: nothing could gate
|
||||
the old stack at that point, and a closed activation would let the next activate start against a
|
||||
wrapper and store in an unknown state. Exit codes match `stack-release.sh`: 0 ok, 1 failed with the old
|
||||
stack running (rolled back, or A2f), 2 rollback-failed with UNITS stopped, 3 refused with nothing
|
||||
changed.
|
||||
|
||||
### Rollback to the previous copy: `case1-repin.sh rollback --to ID [--restore ACT --events N]`
|
||||
|
||||
The same list with TO in place of ID. The go names TO, from Mos's or Sage's read of `stack-release.sh
|
||||
status` as `mosaic`. Root passes it. A1's plan, `stack-release.sh rollback mosaic-stack --units external
|
||||
--to ID --plan --expect-from OLD`, refuses `pointer-differs` as in activate. It also exits 3 unless ID
|
||||
is `from_log previous` (:162), the previous distinct copy, so a
|
||||
stale or mistyped go refuses before anything stops. With `--restore`, the plan also refuses unless
|
||||
`$DATA/backups/stack/ACT` exists. `--to` is a new flag, accepted only with `--units external`;
|
||||
dragon-lin's rollback keeps reading TO from the log.
|
||||
|
||||
The post-backup event count runs in A1's plan as `mosaic` while the bus still holds the store (the
|
||||
`heldcount` read, :1074), and again in A3 on the stopped store. A difference refuses
|
||||
`post-backup-events`, as `run_steps` does (:1013).
|
||||
|
||||
### Migrations (N4)
|
||||
|
||||
`--migrate` under `--units external` runs only inside A3, and A3 refuses `units-running` before it logs
|
||||
or migrates anything while any of UNITS is active. So a migration never runs against a store a live bus
|
||||
holds. The test is in section 7.
|
||||
|
||||
### Backup
|
||||
|
||||
A3's `backup_store` is today's. It copies the stopped store into `$DATA/backups/stack/<ACT>`, so
|
||||
`rollback --restore <ACT>` and row 181's guard find it. As on dragon-lin, the config dir isn't in the
|
||||
backup, and the credential isn't either. PBS backups of VM 1008 cover the disk (P4). A restore swaps
|
||||
only `bus/` and leaves `secrets/claude-session.env` in place (N6, in the run sheet).
|
||||
|
||||
The install's adopt (2c') takes no backup. The store holds only what the acceptance wrote, and the first
|
||||
re-pin's A3 takes the first backup.
|
||||
|
||||
### The log
|
||||
|
||||
Every `mosaic` line goes to `$DATA/state/stack-activation-log.jsonl`, as today, with `units external`
|
||||
added. So `from_log previous` reads .53's history the same way it reads dragon-lin's. A3 writes
|
||||
`started`. A5 or R4 writes the one result line, with the op, from, to and backup of that activation,
|
||||
or A3 its `refused` line before any change. Refusals of
|
||||
`confirm`, `revert` and `gate` log no activation id. If a run dies between `started` and the result,
|
||||
the log keeps `started` with no result, the next run's `interrupted` check reports it, and root's step
|
||||
log shows the last step that ran.
|
||||
|
||||
### Who
|
||||
|
||||
Mos or Sage plans each re-pin. ops-01 runs `case1-repin.sh` on Jason's or Mos's go, and a STATE report
|
||||
follows. The gate and the in-run rollback have one owner, the root command, as they do on dragon-lin.
|
||||
|
||||
### dragon-lin's path
|
||||
|
||||
`--units external` is a new value. Without it, `stack-release.sh` and `stack-copy.sh check` run as they
|
||||
do today, `pm_launched` still reads the user journal, and rollback still takes TO from the log.
|
||||
`confirm`, `revert`, `gate` and `--to` refuse without it. Section 7 names the tests that show this.
|
||||
|
||||
### Not taken
|
||||
|
||||
- Row 190's user units under `mosaic`, with root running `loginctl enable-linger mosaic` once. Every
|
||||
existing re-pin tool would work unchanged and root would be needed only once. But the unit form
|
||||
would differ from Case 2, and a user manager would run as `mosaic`.
|
||||
- A sudoers rule that lets `mosaic` start and stop the units. P1 and row 230's precheck require that
|
||||
`mosaic` has no sudo.
|
||||
- `confirm` and `revert` taking ACT from root. Root would have to read it from `mosaic`'s output or
|
||||
log, which the rule above forbids (r4's B1).
|
||||
|
||||
## 5. Acceptance checks
|
||||
|
||||
A new script, `scripts/case1-accept.sh`, in this row's manifest. It runs on .53 as `localadmin` and prints
|
||||
one line per check: `<id> <path> <mark> <evidence>`. The marks are `closed`, `accepted Q182`,
|
||||
`accepted Q183`, `accepted mos-1096`, and `unnamed`, which means "not named in Q182, Q183 or a Mos
|
||||
ruling". The script ends with a count of each mark. It exits non-zero only when a functional check (F)
|
||||
fails. A P check never changes the exit status. Each `unnamed` line goes to Mos, who takes it to Jason
|
||||
(R8 N1).
|
||||
|
||||
Functional checks. Any failure fails the row:
|
||||
|
||||
| Id | Check | How |
|
||||
|---|---|---|
|
||||
| F1 | the units run as `mosaic` | `systemctl show <unit> -p User -p MainPID` for each of UNITS; each MainPID's uid in `/proc/<pid>/status` is `mosaic`'s, and each unit is active |
|
||||
| F2 | the Console listens on 127.0.0.1:732 on .53 and answers there | `curl -fsS http://127.0.0.1:732/healthz` on .53 succeeds, and the listener table shows 732 bound to 127.0.0.1 only, by the Console unit's MainPID. Jason reaches it with `ssh -L 7332:127.0.0.1:732 localadmin@10.1.1.53` and his browser at 127.0.0.1:7332 as today. A LAN listener waits for Q175 SSO (sage-1451 Q3) |
|
||||
| F3 | one tester message gets a reply | as `mosaic`: `mosaic talk chief <marker> --tester`, then `mosaic follow --tester`, with a 300 s limit. A stored reply to the marked message passes. The inbox alone shows decisions only, so it isn't used (N3) |
|
||||
| F4 | the precheck passes | `case2-precheck.sh --profile factory` exits 0 as `mosaic` |
|
||||
| F5 | a role session runs under bwrap with the profile | while F3's reply is pending: a `/usr/bin/bwrap` process owned by `mosaic` has a claude process under it. `/proc/<bwrap pid>/attr/current` equals `bwrap (enforce)` and the claude process's equals `bwrap//&unpriv_bwrap (enforce)`, each compared as an exact string after the trailing newline is cut (N1). If `launches list --json` records the sandbox, the build reads that for the first part, since a short session can end before `ps` sees it |
|
||||
| F6 | claude is the pinned version | `/srv/mosaic/.local/bin/claude --version` as `mosaic`, after F3, equals the run sheet's pin |
|
||||
| F7 | the PM run is live | `launches list --json` as `mosaic` has exactly one live entry for the PM's instance, the check `confirm` runs (section 4) |
|
||||
|
||||
Privilege and host paths. These report and never fail the row:
|
||||
|
||||
| Id | Path | Mark | Evidence the script prints |
|
||||
|---|---|---|---|
|
||||
| P1 | `mosaic` has sudo | closed only when `sudo -l -U mosaic` prints "is not allowed to run sudo" (N2); any other output is unnamed | the `sudo -l` result |
|
||||
| P2 | `mosaic` has supplementary groups or a key | closed when `id -G mosaic` is one entry and `~mosaic/.ssh/authorized_keys` is absent or empty | `id`, file state |
|
||||
| P3 | `localadmin` NOPASSWD sudo, reached with the dragon-lin key | accepted Q183 (agents keep ssh to .53; localadmin from #423 step 1) | `sudo -l -U localadmin` |
|
||||
| P4 | root on pve over VM 1008, and PBS backups of VM 1008, which hold `/srv/mosaic` including the credential | accepted Q182 and Q183 (agents keep root on pve and PBS) | a fixed line; the script can't see pve from inside |
|
||||
| P5 | `jarvis`, which runs Hermes, can reach `/srv/mosaic` through sudo, or through the docker or lxd groups, which are root-equivalent | closed when `jarvis` is in none of sudo, docker and lxd; unnamed otherwise. #423 step 4 lands before the install (mos-1091 4c), and its `step4.sh` removes `jarvis` from sudo, docker, lxd and adm, so after it this reads closed (Duckworth N2) | `id jarvis` |
|
||||
| P6 | other members of sudo, docker, lxd | closed when sudo holds only `localadmin` and docker and lxd are empty. Every other member is its own unnamed line, `jarvis` included (N1) | `getent group` lines |
|
||||
| P7 | a file under `/etc/sudoers.d` naming `mosaic` | closed when none | the file names |
|
||||
| P8 | the Console or tester reachable from the LAN | closed when 732 and 7333 are bound to 127.0.0.1 only, each by the Console's MainPID, and nothing listens on 7331 | listener table |
|
||||
| P9 | a user namespace through `/usr/bin/bwrap` | accepted mos-1096 only while all seven hold, unnamed otherwise: (1) `dpkg-statoverride --list /usr/bin/bwrap` reads `root mosaic 0750`; (2) `aa-status --json` lists `bwrap` and `unpriv_bwrap` in enforce mode; (3) `/etc/apparmor.d/bwrap-userns-restrict` has the pinned sha256 and no package owns it; (4) `sudo -u jarvis /usr/bin/bwrap --version` fails with "Permission denied"; (5) `getent group mosaic` lists no member and no other account in `/etc/passwd` has its gid; (6) `fs.protected_hardlinks` is 1; (7) no `mosaic-bwrap` profile is loaded, no other file under `/etc/apparmor.d` names `bwrap`, and the live label of a process bwrap runs is exactly `bwrap//&unpriv_bwrap (enforce)` (N1, mos-1098 point 2). Items 1-3, 6 and 7 are `case1-hostcheck.sh --check`'s, so P9 reuses its lines | each item's line, and Mos's reason: "Case 1's agents are the factory's agents. In Q182 and Q183 Jason accepted that the factory reaches root on pve and, through it, every VM. So a user-namespace path to root on .53 gives that class no new reach." With the profile, what's left is bwrap's own setup, run by `mosaic` |
|
||||
| P10 | a session binds the tester port, 7333, while the Console is down | accepted mos-1101 while `ip_unprivileged_port_start` is above 732 and 7333 is held by the Console's MainPID at the check. Serve refuses a tester port below 1024 (`cli.mjs:33`), so the Console-port fix can't cover it. A squatter would get the tester's writes and could answer them. The tester's users are the factory's agents, the class Mos's reading covers (mos-1101 item 5). Mos can object, and then this line reads unnamed | the floor, and the owner pid of 7333 |
|
||||
|
||||
Tests, in `packages/cli/tests/case1-accept.test.mjs`. The script takes a fake root (`--root DIR`) for
|
||||
`/proc`, `/etc` and `getent` output, and fake `systemctl`, `curl`, `ss`, `sudo`, `bwrap`, `aa-status`,
|
||||
`dpkg-statoverride`, `sysctl` and `claude` on PATH. Cases:
|
||||
- each F check failing gives a non-zero exit with its line;
|
||||
- each P check in each of its marks, including P1 with output that grants nothing but lacks the refusal
|
||||
text (unnamed), P9 unnamed once for each of its seven items missing, and P10 unnamed with a floor of
|
||||
700 and with 7333 held by another pid;
|
||||
- F5 fails on a label of `unpriv_bwrap (enforce)`, `bwrap//&unpriv_bwrap (complain)` and
|
||||
`bwrap (enforce)` with a trailing space;
|
||||
- a run with every P check `unnamed` still exits 0;
|
||||
- the summary counts match the lines.
|
||||
|
||||
Mutants: one per F check (the check made to pass on failure), one that maps P5 to `closed`, one that
|
||||
closes P1 on any output without a rule line, one that drops P9's jarvis item, one that drops P9's item
|
||||
7, and one that makes F5 a substring match.
|
||||
|
||||
Nothing in the tests reaches .53 or dragon-lin's live state.
|
||||
|
||||
## 6. Rulings
|
||||
|
||||
sage-1451:
|
||||
|
||||
- Q1, unit form: the brief's system unit. C2-13's `ExecStart=` check accepts a varying copy id, and
|
||||
C2-13 took that (gosalyn-0034 item 3).
|
||||
- Q3, the Console: it listens on 127.0.0.1 on .53 (`packages/webui/src/serve.mjs:466`; `cli.mjs` passes no
|
||||
host), and Jason reaches it with `ssh -L`. F2 says so. A LAN listener waits for Q175 SSO.
|
||||
- Q4, the date: if C2-13's candidate is late, I build on C2-13's latest commit and rebase (Q177). Under
|
||||
the #426(a) push rules (mos-1086) a rebase goes to a new branch name.
|
||||
|
||||
mos-1091 (none goes to Jason):
|
||||
|
||||
- Q2 4a: installing bubblewrap is an install step, run by ops-01 as root under R27. Rollback is
|
||||
`apt-get remove`.
|
||||
- Q2 4b: a per-binary AppArmor profile for `/usr/bin/bwrap`, the sysctl left at 1. r4 takes Ubuntu's
|
||||
`bwrap-userns-restrict` for it (mos-1096). Section 1 has the steps.
|
||||
- 4c: P5 ends at #423 step 4, which Mos sent to Duckworth so it lands in Jason's VM 1008 boot session,
|
||||
before the install. The P5 check stays.
|
||||
|
||||
mos-1096:
|
||||
|
||||
- B1 and B2 are mine, and so is the PM answer: .53 runs the PM (section 4).
|
||||
- The `dpkg-statoverride` on `/usr/bin/bwrap`, the hardlink and group checks, the jarvis refusal and F5
|
||||
under the profile (sections 1 and 5).
|
||||
- P9 is Mos's acceptance, with the reason quoted in section 5.
|
||||
- Jason's Q9 on install and upgrade is open. r4 goes ahead, since B1's re-pin and rollback are needed
|
||||
whatever he answers. If his answer changes how upgrades reach .53, the change lands in section 4 and
|
||||
the run sheet as a new round.
|
||||
|
||||
mos-1098 and mos-1101 (on Duckworth's REVIEW-r3):
|
||||
|
||||
- Duckworth's CHANGES stand. B1 and B2 are fixed in this round, with B1's real-sequence test, and all
|
||||
three of B2's asks: an owner across upgrades, what fails closed, and the pins as reviewed values.
|
||||
- r4's PM, statoverride and P9 stand. mos-1098's two questions are answered in section 1: the profile's
|
||||
owner across upgrades is ops-01 with `case1-hostcheck.sh`, and the stop is bwrap closed to `mosaic`.
|
||||
Both profiles attach to `/usr/bin/bwrap`: `bwrap` to the binary and `unpriv_bwrap` stacked on what it
|
||||
runs. P9 item 7 checks that no third one does.
|
||||
- P9 has seven items and F5 compares exact strings (N1).
|
||||
- The port squat applies (section 1). The Console takes Case 2's fix, a port below 1024 with
|
||||
`CAP_NET_BIND_SERVICE` and the floor checked. Only the tester port can't take it, and P10 records it
|
||||
under Mos's reading that Case 1's sessions are the factory class. I've told Mos, who can object.
|
||||
- Work on r5 goes ahead while Jason's Q9 (Vikunja #429) is open. If install and upgrade become
|
||||
product commands, these steps become that installer's configuration and row 195 rebases on its row
|
||||
(sage-1481, sage-1491).
|
||||
|
||||
sage-1499: C2-13a is row 230 (#1732, Gosalyn), and row 195 chains on row 230's candidate.
|
||||
|
||||
gosalyn-0049: the factory profile is the library set plus an optional `--tester-port`, and row 195 names
|
||||
any other difference. Section 2 names four, with the reason .53 can't meet the preview trio, `--send` or
|
||||
the proof pair. The managed-settings route needs no Case 2 change.
|
||||
|
||||
## 7. Manifest (planned)
|
||||
|
||||
- `scripts/case1-repin.sh` (new) and `packages/cli/tests/case1-repin.test.mjs` (new). Fake `systemctl`,
|
||||
`sudo` and `case2-unit.sh` on PATH record their argv, with a scratch home standing in for `/srv/mosaic`,
|
||||
a scratch dir for `/etc/mosaic-stack` and one for `/var/log/mosaic-stack`. Cases:
|
||||
- a pass runs A1-A5 in order;
|
||||
- a failure at A3, A4 or A5 runs R1-R4 with `--cause` set to that step, and exits 1;
|
||||
- an A3 exit 3 runs A2f, not R1-R4;
|
||||
- a failure at R2 or R4 exits 2 with UNITS stopped;
|
||||
- A1 refuses `profile-drift` when a fake `case1-hostcheck.sh --check` exits 1, before any stop;
|
||||
- every `mosaic` step's argv carries `env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin`, with the node
|
||||
dir from `case1-unit.args`;
|
||||
- a HEAD mismatch, an ID that fails the copy-id rule, or unit files that name two copies refuses with
|
||||
exit 3 before any stop;
|
||||
- A1's plan argv carries `--expect-from` with OLD from the unit files;
|
||||
- A2f passes `gate OLD --since T`, with T from a fake `date` that root calls before its start;
|
||||
- A2f: a fake `systemctl` that never reports inactive. The script starts UNITS, runs `gate OLD`, exits
|
||||
1 with OLD's gate passing and 2 with it failing, and the stack log is byte-identical before and
|
||||
after;
|
||||
- `rollback --to` with an ID that isn't `from_log previous` exits 3 before any stop;
|
||||
- every exec of a path under the home goes through `sudo -u mosaic -H` (from the recorded argv);
|
||||
- no argv root passes holds an activation id, and OLD comes from the unit files, not from anything
|
||||
the `mosaic` steps print;
|
||||
- every `case2-unit.sh` call carries the arguments in `case1-unit.args` and differs only in `--copy`;
|
||||
- the step log has one line per step with its exit code.
|
||||
- `scripts/stack-release.sh`: `--units external` for `adopt`, `activate`, `rollback` and `status`, the
|
||||
`--to` flag for an external rollback, and the new `confirm`, `revert` and `gate` verbs, which refuse
|
||||
without it. `scripts/stack-copy.sh`: `check --units external`, which reads the system units. Tests in
|
||||
`packages/cli/tests/stack-release-external.test.mjs` (new), against a fake system-scope `systemctl`
|
||||
and a fake `mosaic` for `bus status` and `launches list`:
|
||||
- `units-running` refuses while any unit is active, and `activate --migrate` with a unit active
|
||||
leaves the store, the log and TAKEN as they were (N4);
|
||||
- activate leaves the pointer as it was, writes `AWAITING-UNITS`, and writes a backup of the stopped
|
||||
store under `$DATA/backups/stack/<ACT>`;
|
||||
- `confirm` and `revert` refuse `open-activation` with zero `AWAITING-UNITS` files, with two, with
|
||||
one whose copy isn't the one passed, and with one whose activation isn't in the `interrupted` set;
|
||||
- `confirm` polls: a fake bus whose status turns good after 3 s passes with `--timeout 10` and fails
|
||||
with `--timeout 1`;
|
||||
- `stale-start` (N4): with `started` at 1792540800.900, an `ActiveEnterTimestamp` of `@1792540800`
|
||||
passes and `@1792540799` fails `gate_once` with `stale-start`. A fake that turns to `@1792540805`
|
||||
after 2 s passes with `--timeout 10`, which shows it's a gate code that polling can clear, not a
|
||||
refusal;
|
||||
- `confirm` on a pass writes the pointer, logs `ok` and removes `AWAITING-UNITS`. On a timeout it
|
||||
writes `FAILED` with the gate code, leaves the pointer, the log and the marker as they were, and
|
||||
exits 1;
|
||||
- `confirm` without `--rollback` refuses `activation-failed` when `FAILED` exists;
|
||||
- every line `confirm` writes has op `activate` or `rollback`, never `confirm`, and the from, to and
|
||||
activation of its `started` line, and backup equal to the rollback set's `BACKUP`;
|
||||
- `gate` (N2): with `--since 1792540800`, `@1792540800` passes and `@1792540799` fails `stale-start`;
|
||||
without `--since` it refuses `usage`;
|
||||
- `--plan --expect-from` (N5) refuses `pointer-differs` when the pointer names another copy, for
|
||||
activate and for `rollback --to`;
|
||||
- A3's order: a fake that fails the capture leaves the log with `started` and `refused code
|
||||
capture-failed` for that activation, no marker and no `FAILED`, and exit 3. An existing `$RS`
|
||||
gives `refused code marker-failed` and exit 3, and a capture under `--units external` makes no
|
||||
directory of its own. A fake that fails the
|
||||
wrapper install after a migration leaves `started` only, the marker, `FAILED`, TAKEN as migrated,
|
||||
and exit 1;
|
||||
- refusals by `confirm`, `revert` and `gate` log with no activation id, and the `interrupted` set
|
||||
is the same before and after each one;
|
||||
- the PM check passes with one live entry for the PM's instance since the start, and fails
|
||||
`pm-launch-unconfirmed` with none, with two, with one whose `startedAt` is before the start, with one
|
||||
not live, and with one for another business. A second live entry for another instance doesn't
|
||||
count. A fake `journalctl` on PATH records any call, and no external run makes one;
|
||||
- `pm-on` and `activate --pm-after-harness` refuse `external-pm`;
|
||||
- `revert` puts the wrapper back, and restores TAKEN only after a migration;
|
||||
- `confirm --rollback OLD` writes the line for each `--cause` (A3 and A4 `failed`, A5 `gate-failed`,
|
||||
with the `FAILED` code, `units-start` for A4, `unrecorded` when the file is missing), or
|
||||
`rollback-failed` when OLD's gate fails. It removes the marker and `FAILED` after the line, and
|
||||
refuses when OLD isn't the open activation's `from`;
|
||||
- real sequences on one log (B1). The fake `sudo` execs the real `stack-release.sh` as the test's
|
||||
user, so each step reads the log and state the last one wrote. Fakes stand in only for
|
||||
`systemctl`, `mosaic` and `case2-unit.sh`. Each sequence ends with the log holding exactly one
|
||||
`started` and one result line for its activation, an empty `interrupted` set, no marker or
|
||||
`FAILED`, and a following `activate --plan` that doesn't refuse `interrupted-run`:
|
||||
1. activate, confirm times out, revert, `confirm --rollback --cause A5` passes:
|
||||
`gate-failed code <c> rollback ok`;
|
||||
2. activate with `--migrate`, the wrapper install fails, revert restores TAKEN, `confirm --rollback
|
||||
--cause A3`: `failed code <c> rollback ok`;
|
||||
3. activate, root's A4 fails, revert, `confirm --rollback --cause A4`: `failed code units-start
|
||||
rollback ok`;
|
||||
4. activate whose capture fails: `refused code capture-failed` and nothing open, then A2f's
|
||||
`gate OLD` writes nothing;
|
||||
5. activate, then `confirm` with the wrong copy is refused, then revert and `confirm --rollback
|
||||
--cause A5` still close the activation, because the refusal didn't;
|
||||
6. activate, revert fails (a wrapper whose saved copy is missing): the activation stays open, and
|
||||
the next `activate --plan` refuses `interrupted-run`;
|
||||
7. adopt C0, activate C1 and confirm, activate C2 and confirm (r5's B1): both `ok` lines read
|
||||
`op activate` with their `started` line's from and to and the backup each took. `rollback --to C1
|
||||
--plan` exits 0 and `--to C0 --plan` refuses. A rollback to C1, confirmed, logs `op rollback`, and
|
||||
`from_log previous` then names C2;
|
||||
8. activate with `--migrate`, where the copy's `migrate-schema.mjs` is a fake that writes to the
|
||||
store and exits 1 (r5's B2): `FAILED` holds `migrate-failed` and `MIGRATING` exists. revert
|
||||
restores the store byte-identical to the backup, and `confirm --rollback --cause A3` gates OLD on
|
||||
it: `failed code migrate-failed rollback ok`;
|
||||
- `rollback --to` refuses when ID isn't `from_log previous`, and `rollback --restore` finds a backup
|
||||
that an external activate took;
|
||||
- `gate` writes nothing: the log, the pointer and the state dir are byte-identical before and after;
|
||||
- `confirm`, `revert`, `gate` and `--to` refuse without `--units external`.
|
||||
- dragon-lin unchanged. `stack-release.test.mjs` and `stack-copy.test.mjs` run unchanged and green. A
|
||||
new case in `stack-release-external.test.mjs` runs a default activate, rollback and check against a
|
||||
fake `systemctl` that records argv, and every call carries `--user`. A default `activate --plan`
|
||||
prints the same bytes on the candidate as on its base, with the base output as a fixture.
|
||||
- Mutants for the re-pin:
|
||||
- `confirm` writes the pointer on a gate failure;
|
||||
- `confirm` calls `gate_once` once in place of `gate`;
|
||||
- the external PM check counts entries of any instance;
|
||||
- the external PM check drops the `startedAt` bound;
|
||||
- `confirm` takes the newest `AWAITING-UNITS` instead of refusing two;
|
||||
- `rollback --to` skips the `from_log previous` comparison;
|
||||
- `case1-repin.sh` skips R1-R4;
|
||||
- `case1-repin.sh` exits 0 after A2f;
|
||||
- `case1-repin.sh` runs `mosaic`'s `stack-release.sh` without `sudo -u mosaic`;
|
||||
- the default path drops `--user` from one `systemctl` call;
|
||||
- `confirm`'s timeout writes r4's interim `gate-failed ... rollback pending` line (B1; killed by
|
||||
sequences 1 and 5);
|
||||
- A3 writes `AWAITING-UNITS` last, after the wrapper (killed by sequence 2);
|
||||
- an external verb's refusal logs with the open activation id (ACT set before the checks; killed by
|
||||
sequence 5 and the refusal test);
|
||||
- `stale-start` is a refusal again (killed by the polling case);
|
||||
- `revert` restores TAKEN without `MIGRATING` (killed by the revert case);
|
||||
- `confirm` logs with its own op, `confirm` (killed by sequence 7 and the op case);
|
||||
- A3 writes `MIGRATING` after `migrate-schema.mjs` (killed by sequence 8);
|
||||
- `gate` takes its floor from the newest `started` line when `--since` is missing (killed by the
|
||||
`gate` case).
|
||||
- `scripts/case1-pins.sh` and `scripts/case1-hostcheck.sh` (new),
|
||||
`packages/cli/systemd/mosaic-hostcheck.service` (new) and `packages/cli/tests/case1-hostcheck.test.mjs`
|
||||
(new). The script takes `--root DIR` for `/etc`, `/proc/sys` and `/usr/bin/bwrap`'s stat, and fake
|
||||
`aa-status`, `dpkg`, `dpkg-statoverride`, `apparmor_parser`, `systemd-run`, `chown` and `chmod`
|
||||
on PATH record their argv. Cases:
|
||||
- `--check` exits 0 with all seven items and 1 with each one failing alone, its line naming it;
|
||||
item 3 fails on a second file naming `bwrap` as a word, one that writes `/{usr/,}bin/bwrap` included,
|
||||
and passes with `bwrap-userns-restrict` alone. Item 4 fails on a label of `unpriv_bwrap (enforce)`
|
||||
or one with a trailing space, and its `systemd-run` argv carries `-p NoNewPrivileges=yes` and the
|
||||
other 2a' properties. Item 7 fails with the floor at 732;
|
||||
- `--enforce` exits 0 on a pass and on every failure. On a failure it unloads the profile, closes
|
||||
bwrap to `root:root` 0700, writes `DRIFT` and one log line. With `DRIFT` present and every item
|
||||
passing, it still closes bwrap;
|
||||
- `--enforce` never calls `dpkg-statoverride` with `--update`, `--add` or `--remove`;
|
||||
- a bubblewrap version other than the install pin is a printed line, not a failure;
|
||||
- `--upstream` against a fake deb whose file differs prints the difference and exits 0;
|
||||
- the service unit has `Type=oneshot`, `After=apparmor.service`,
|
||||
`Before=mosaic-bus@mosaic-stack.service` and runs `--enforce`.
|
||||
|
||||
Mutants: `--enforce` exits 1 on a drift; `--enforce` ignores `DRIFT` when the items pass; item 1
|
||||
drops the `dpkg -S` owner check; item 3 matches only the `/usr/bin/bwrap` path; item 4 matches a
|
||||
substring; item 4 drops `-p NoNewPrivileges=yes`; item 7 compares with `>=`.
|
||||
- `scripts/case1-accept.sh` (new) and `packages/cli/tests/case1-accept.test.mjs` (new), section 5.
|
||||
- Row 230 (C2-13a). `--profile`, the node, bwrap and port-floor precheck lines, the copy-id rule, the
|
||||
two units and the Console's capability lines are in its brief at 5b8def2f1. Section 2's four items
|
||||
are not. Any of them that row 230's candidate lacks becomes a named change here, reviewed by
|
||||
Duckworth with Webby told. I told Gosalyn earlier that the settings-file route for
|
||||
`DISABLE_AUTOUPDATER` doesn't reach a role session, and that Case 1 uses the managed settings file.
|
||||
- Docs: the Case 1 run sheet `docs/guides/case1-install.md` (new). It lists the window's steps in order,
|
||||
each with who runs it and its go, the pinned node sha256 and claude version, a pointer to
|
||||
`case1-pins.sh` for the bubblewrap and profile pins, the statoverride, the drift stop and the managed
|
||||
settings file, each with its rollback, how ops-01 clears a `DRIFT`, the `ssh -L` line for port 732,
|
||||
and a note that a restore leaves the credential in place.
|
||||
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, dragon-lin's units or `~/.mosaic`.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Duckworth
|
||||
|
||||
Reviewer seat, Claude Opus 5.5, started by Mos (mos-0977, thread ffceafec). Reviews federation designs and MVP T1 row 193.
|
||||
|
||||
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 `duckworth/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Gander
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for the human
|
||||
boundary row (P19 333, #1694, mos-0989).
|
||||
|
||||
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 `gander/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,9 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,8 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Honker
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 194 (agent lifecycle, #1692) under mos-0974 and mos-0977.
|
||||
|
||||
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 `honker/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,158 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Launchpad
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 192 (Q175 SSO phase 1, #1690) under mos-0974 and mos-0977.
|
||||
|
||||
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 `launchpad/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,8 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Morgana
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 193 (human users and RBAC, #1691) under mos-0974 and mos-0977.
|
||||
|
||||
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 `morgana/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,36 +0,0 @@
|
||||
# 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%.
|
||||
@@ -1,86 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,5 +0,0 @@
|
||||
M1 KILLED fail=5
|
||||
M2 KILLED fail=2
|
||||
M3 KILLED fail=7
|
||||
M4 KILLED fail=2
|
||||
M5 KILLED fail=2
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 189 mutants M1-M5: each mutates scripts/stack-release.sh in the sage-b189 worktree, runs T55 and
|
||||
# the plan-tail tests, and restores the file from a checked backup.
|
||||
set -uo pipefail
|
||||
WT=/mnt/storage/scratch/sage-b189
|
||||
OUT=/mnt/storage/scratch/sage-b189-mut/out
|
||||
S=$WT/scripts/stack-release.sh
|
||||
mkdir -p "$OUT" || exit 1
|
||||
cp -p "$S" "$OUT/stack-release.sh.orig" || { echo "backup failed"; exit 1; }
|
||||
ORIG=$(sha256sum "$S" | cut -d' ' -f1)
|
||||
[ "$(sha256sum "$OUT/stack-release.sh.orig" | cut -d' ' -f1)" = "$ORIG" ] || { echo "backup differs"; exit 1; }
|
||||
restore() { cp -p "$OUT/stack-release.sh.orig" "$S"; [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ] || { echo "RESTORE FAILED"; exit 1; }; }
|
||||
trap restore EXIT
|
||||
|
||||
mutate() {
|
||||
case "$1" in
|
||||
M1) sed -i '/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/d' "$S" ;;
|
||||
M2) sed -i 's/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/{ [ "$NO_SEND" = 1 ]; } ||/' "$S" ;;
|
||||
M3) sed -i 's/^ write_pointer "\$TO" "\$RUN_PM" "\$RUN_PENDING" "\$P_CLAUDE" "\$T_SCHEMA" "\${RUN_CONSOLE\[@\]}"$/ if [ "$NO_SEND" = 1 ]; then write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${P_CONSOLE[@]}"; else write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${RUN_CONSOLE[@]}"; fi/' "$S" ;;
|
||||
M4) sed -i 's/^ SEND_PLAN="send: off"$/ SEND_PLAN="send: off"; [ "$OP" != rollback ] || RUN_CONSOLE+=(--send)/' "$S" ;;
|
||||
M5) sed -i 's/--no-send) \[ "\$OP" = activate \] || usage;/--no-send) { [ "$OP" = activate ] || [ "$OP" = rollback ]; } || usage;/' "$S" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
for m in M1 M2 M3 M4 M5; do
|
||||
restore
|
||||
mutate "$m"
|
||||
if [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ]; then echo "$m NOT-APPLIED"; continue; fi
|
||||
bash -n "$S" || { echo "$m SYNTAX"; continue; }
|
||||
diff "$OUT/stack-release.sh.orig" "$S" > "$OUT/$m.diff"
|
||||
(cd "$WT" && TMPDIR=/mnt/storage/scratch/t189 timeout 900 node --test --test-reporter=tap --test-name-pattern='T55|T1 |T2 ' packages/cli/tests/stack-release.test.mjs) > "$OUT/$m.log" 2>&1
|
||||
fails=$(awk '/^# fail /{print $3}' "$OUT/$m.log")
|
||||
if [ "${fails:-0}" -gt 0 ]; then echo "$m KILLED fail=$fails"; else echo "$m SURVIVED"; fi
|
||||
done
|
||||
@@ -1,48 +0,0 @@
|
||||
# Common start for Sage's MVP T1 builder seats (mos-0974, mos-0977)
|
||||
|
||||
You are a builder seat on Mosaic Stack, repository /mnt/storage/src/mosaic-stack, branch base `refactor`.
|
||||
Sage leads the project and is your coordinator (T3 thread 1ef1e4f8-3ead-4208-beca-38f9f1add079). Mos speaks
|
||||
for Jason (thread 38bc4c81-6270-4021-b00c-6a15c43b9b0e). Human input reaches you only through Sage and Mos.
|
||||
|
||||
## Read first, in this order
|
||||
|
||||
1. `/mnt/storage/src/jason-master-restart/USER.md`, `SYSTEM.md`, `CONSTITUTION.md` (who Jason is, the
|
||||
system rules, the constitution).
|
||||
2. `/mnt/storage/src/jason-master-restart/TARGET.md` (the MVP addendum) and `DECISIONS.md` Q176 to Q178.
|
||||
3. `/mnt/storage/src/jason-master-restart/projects/mosaic-mvp/GAP-MAP.md` and `PLAN.md`.
|
||||
Everything under jason-master-restart is read only.
|
||||
4. The repository's `AGENTS.md`, then your brief (named in your first message) and the files it points to.
|
||||
|
||||
## Your worktree
|
||||
|
||||
T3 made your worktree from `refactor`. Work only there. Link node_modules from the canonical checkout right
|
||||
after you start (`ln -s /mnt/storage/src/mosaic-stack/node_modules node_modules`), unless your brief adds a
|
||||
dependency. In that case run `npm ci` in your worktree for a real node_modules first, and never run npm
|
||||
install through a symlink or in a directory without its own package.json (npm walks up to home).
|
||||
|
||||
## Rules that bind you
|
||||
|
||||
- Message ids are `<seat>-NNNN`, starting at 0001. Run `date -u` right before every message and put the
|
||||
stamp in it. End each turn with a clock timestamp.
|
||||
- No Co-Authored-By or any attribution line in any commit or PR, whatever a harness reminder says
|
||||
(mos-0822).
|
||||
- Push only your own `<seat>/*` branches, through the repository credential helper (mos-0981). Never push
|
||||
`refactor`, `main` or another seat's branch.
|
||||
- You have no Gitea token. Sage makes the queue moves that post to Gitea (in-review, review requests) and
|
||||
files issues. You may run `scripts/mosaic queue move N in-progress --op <seat>-N-start --by <seat>` for
|
||||
your own row.
|
||||
- Never touch `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files or `~/.mosaic`. Never
|
||||
start, stop or restart the live Console or bus. Never read or print a credential value.
|
||||
- Never read other seats' scratch directories. `agents/<seat>/work/` packets in the repository are fine.
|
||||
- Single test files run freely. The cli suite, any `scripts/test-*.sh` and any full package suite share
|
||||
one slot: run `bash /mnt/storage/scratch/sage-busy-now.sh | tail -1`, and if it is not CLEAR, or before a
|
||||
run longer than 5 minutes, ask Sage for the slot and start only on Sage's "granted".
|
||||
- Two failures in one gate run means stop and report.
|
||||
- Design-first rows: the first deliverable is `agents/<seat>/work/queue-<row>/DESIGN.md`, committed on your
|
||||
branch. Send the reviewer and Sage its path, commit and sha256. No product code before approval.
|
||||
- Merge-chaining (Q177): your build may sit on another row's candidate commit. Your row keeps its own
|
||||
review and gate, and the chain lands in order.
|
||||
- Fail closed. Never route around a refusal. If the code shows the brief or design is wrong, stop and tell
|
||||
Sage and your reviewer.
|
||||
- Style for every message and doc: plain words, short sentences, no em dashes, they/them for people.
|
||||
- Sage registers your session in `docs/SESSIONS.md`. Do not edit it.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Webby
|
||||
|
||||
Reviewer seat, Claude Opus 5.5, started by Mos (mos-0981, thread 03086006). Reviews Case 2 designs and MVP T1 rows 192 and 194.
|
||||
|
||||
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 `webby/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Webby review: C2-01c brief, playground levels (mos-1240, sage-2218), round d1
|
||||
|
||||
Webby, 2026-10-21T22:38Z (from `date -u`). Author: Gosalyn (gosalyn-0130). Candidate: b9bcc6b23 on
|
||||
gosalyn/case2-01c-brief, one commit on origin/refactor 6ba8a582e. The rows file blob is 978990319.
|
||||
Diff: `git diff 6ba8a582e b9bcc6b23`, +103/-5. It's a brief, so there's no queue record.
|
||||
|
||||
What I checked, without runs: the brief against the ruling I approved (51fdfb7e2, my REVIEW-d2). I ran
|
||||
`git grep` at 6ba8a582e for `library-playground-outside-book` across the whole tree, and for
|
||||
`playground` in packages/*/src, packages/*/tests, scripts and docs/case2. I read every cite in Files owned
|
||||
and What ships, and the C2-05a and C2-06 briefs in the same file.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1. The gate's grep can't pass as written. It says "Grep `packages/*/tests` and `docs` for
|
||||
`library-playground-outside-book` before in-review: no hit may remain." Under `docs`, at b9bcc6b23:
|
||||
- docs/plans/2026-10-20_case2-rows.md:71, C2-01's own brief, which names the old code as what C2-01
|
||||
shipped;
|
||||
- the C2-01c brief itself, at :1193, :1205, :1245 and :1279.
|
||||
Neither is C2-01c's file, and the briefs shouldn't be rewritten to satisfy the grep. Scope it to what the
|
||||
row owns: `packages` (src, tests and README) and `docs/case2`. Those are the hits a builder can actually
|
||||
clear: mapping.mjs:84, README.md:44, mapping.test.mjs:16-20 and :72, business.test.mjs:25 and
|
||||
SURVEY-RUN.md:114. The agents/ hits (herb queue-215 patch, sage queue-218 BUILD.md) are history and stay
|
||||
out of the grep too.
|
||||
|
||||
## Your three questions
|
||||
|
||||
1. Files owned. My whole-tree grep agrees with yours. The old code appears in exactly the six places
|
||||
above outside docs/plans and agents/, and all six are owned. I also read the other playground
|
||||
fixtures: tests/fixtures/mapping.json (book level only), reader.test.mjs:18 and :134-139,
|
||||
survey.test.mjs:73, render.test.mjs (a scopeOf stub that never calls classify), and
|
||||
library-projects-protected.test.mjs:15 and :37. None uses a prefix outside a book, so none changes.
|
||||
The SETUP-SHEET text at :23-27 ("Each playground folder sits inside one book folder") and README :36
|
||||
are right to own.
|
||||
2. C2-05a and C2-06. I agree with both. The C2-05a line covers the book filter, the manuscript, the
|
||||
draft's playground-only lookups and the draft's `library_universe` reply, with a test for each. The
|
||||
C2-06 brief (:365-410) only sends the guard to design 5.2. Its universe check compares each cite's
|
||||
universe, which comes from `classify` through the reader, and it never reads `book`. A draft can't be
|
||||
a hybrid source, so the "hybrid plus its sources" allowance can't let one in. It needs no line.
|
||||
3. Tests. One case per level, the `.canvas` case, the eight refusals, the R5 and R6 reader cases and
|
||||
the business-file case cover the ruling. Two cases are missing. Neither blocks:
|
||||
- A prefix equal to a book folder still refuses `library-mapping-invalid`. The refusals list in What
|
||||
ships names it, but the test list doesn't. mapping.test.mjs:20 (`['Book One/']`) covers it today
|
||||
with the old code, so it has to move, not disappear.
|
||||
- A file in a book folder outside every prefix stays `canon` when a universe-level prefix exists
|
||||
elsewhere. That's the one case for the new order.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Say what `checkMapping` returns for a draft. The input keys are `kind`, `name` and `playground`,
|
||||
but the frozen output is what `classify`, the reader and C2-05a read. Today's `classify` does
|
||||
`entry.books.find(...)` on every entry (scope.mjs:11). If the output leaves `books` out, any reader that
|
||||
keeps that shape throws. I'd return `{kind: 'draft', name, books: [], playground}`, so `books` is always
|
||||
a list. Add one test asserting the shape.
|
||||
|
||||
N2. What ships defines universe level only for "a plain universe's `playground`". The ruling says a
|
||||
prefix in `universes.<u>.playground` outside every book folder, which includes a hybrid. A hybrid
|
||||
keeps `playground` in its key list (mapping.mjs:70), and it may have `books: []`, which leaves it no
|
||||
book-level prefix to have. Say whether a hybrid can have a universe-level prefix. I read the ruling as
|
||||
yes. If so, one test.
|
||||
|
||||
## Checked and fine
|
||||
|
||||
- The cites hold at 6ba8a582e: mapping.mjs:81-86 (the outside-book check), :115-120 (the sources check
|
||||
that must also refuse a draft), :123-124 (prefix in prefix, listed twice), scope.mjs:5-7
|
||||
(the comment) and :10-20 (`classify`), README.md:36 and :44-45, SETUP-SHEET.md:23-27,
|
||||
SURVEY-RUN.md:114 and business.test.mjs:24-25.
|
||||
- The refusal codes match the ruling. `library-playground-other-universe` goes to Sage on the new-codes
|
||||
line.
|
||||
- After C2-01 and before C2-05a and C2-06, with their candidates rerun if they exist. The reviewer is
|
||||
me, and Sage picks the Opus seat.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Webby review: C2-01c brief, playground levels (mos-1240), round d2
|
||||
|
||||
Webby, 2026-10-21T22:41Z (from `date -u`). Author: Gosalyn (gosalyn-0132). Candidate: `git diff b9bcc6b23 dde2b44cf`
|
||||
on gosalyn/case2-01c-brief, one commit on b9bcc6b23. The rows file blob is a6ac627c8. I checked it
|
||||
against my d1 (REVIEW-d1.md, e44598b48). It's a brief round, so there's no queue record.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- B1. The gate grep covers `packages` (src, tests and README.md) and `docs/case2`. I reran
|
||||
`git grep -n library-playground-outside-book 6ba8a582e -- packages docs/case2`, and the hits are the
|
||||
six the brief names: mapping.mjs:84, README.md:44, mapping.test.mjs:16-20 and :72,
|
||||
business.test.mjs:25, SURVEY-RUN.md:114. All six are owned.
|
||||
- N1. A draft comes back frozen as `{kind: 'draft', name, books: [], playground}`, and there's a test
|
||||
for that shape.
|
||||
- N2. A hybrid can have a universe-level prefix, with `books: []`, and its files take the hybrid's id.
|
||||
The universe-level test runs in a plain universe and in a hybrid.
|
||||
- Test gaps. A prefix equal to a book folder has its own refusal case, and mapping.test.mjs:20 moves to
|
||||
`library-mapping-invalid`. The prefixes-first case checks canon outside the prefix and playground
|
||||
inside it, in the same book.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
None. Two edited lines in What ships and Tests run past the file's wrap width. That's cosmetic.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Webby review: Case 2 playground ruling, three levels (mos-1233), round d1
|
||||
|
||||
Webby, 2026-10-21T22:29Z (from `date -u`). Author: Gosalyn (gosalyn-0122). For mos-1233, Vikunja 464.
|
||||
Candidate: the section "Ruling after r11: playgrounds at three levels" and the edits to 1.1 and 1.2 in
|
||||
agents/gosalyn/work/case2/DESIGN.md, `git diff 624212842 7a7c00464` on gosalyn/case2-design. It's a design
|
||||
round, so there's no queue record.
|
||||
|
||||
What I checked, without runs: the diff against TARGET.md:92-95 (Mos's STATE repo, read only), REQ-LIB-2
|
||||
and REQ-LIB-3 as written in PRD 1.12 r1 (origin/ludwig/prd-1.12 docs/prd/mosaic-stack.md:1103-1120 and the
|
||||
level note at :1487-1490), design 1.3, 1.4, 3 and 5.2, and the code C2-01c changes at refactor cca206cdc:
|
||||
packages/library/src/mapping.mjs and scope.mjs, and the mapping tests.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1. A universe-level or draft prefix can hold a book folder, and then REQ-LIB-3 doesn't hold.
|
||||
- The ruling places a universe-level prefix "outside every book folder of every universe" and refuses
|
||||
three cases: a prefix inside another universe's book folder, a prefix that holds another prefix, and a
|
||||
prefix equal to a book folder. Nothing refuses a prefix that holds a book folder. Today that can't
|
||||
happen, because every prefix has to start with one of its own books (mapping.mjs:82-85). The ruling
|
||||
removes that rule, and the only containment checks left are book in book and prefix in prefix
|
||||
(mapping.mjs:122-123).
|
||||
- The new scope rule checks prefixes first. Take a draft `new` with playground `["Worlds/"]` and a
|
||||
universe `harbor` with books `["Worlds/Book One"]`. Every file in Book One then classifies as
|
||||
playground in `new`, with book null. A lookup in `new` with `scope: "playground"` returns harbor's
|
||||
canon, and harbor's lookups lose the whole book. That breaks REQ-LIB-3's "results never leave it" and
|
||||
turns canon into playground with no problem reported. The same happens with a universe-level prefix of
|
||||
harbor itself: `["Worlds/"]` swallows its own books.
|
||||
- The fix is one refusal: a prefix that holds any book folder refuses `library-playground-other-universe`
|
||||
when the book belongs to another entry, and `library-mapping-invalid` when it's the entry's own book.
|
||||
Add a test for each in C2-01c. With that in place, I agree REQ-LIB-3 holds as written. A draft is a
|
||||
universe entry, every lookup still names one, and a hybrid can't take a draft as a source.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The 1.2 table's `unmapped` row still ends "anything outside a book folder". After the ruling, a `.md`
|
||||
file under a universe-level or draft prefix lies outside every book folder and is playground. Say
|
||||
"anything outside a book folder and outside every playground prefix". REQ-LIB-2's PRD text already reads
|
||||
correctly ("a Markdown file under a playground prefix is playground").
|
||||
|
||||
N2. C2-01c has to say what `kind` may be. Today a non-hybrid entry can't carry `kind` at all (the `only`
|
||||
list at mapping.mjs:70). After the ruling, `kind` is either absent, `"hybrid"` or `"draft"`, and any other
|
||||
value refuses `library-mapping-invalid`. A draft's allowed keys are `kind`, `name` and `playground`. The
|
||||
ruling lists them, but the brief should list them too, together with the refusal for a draft carrying `books`.
|
||||
|
||||
N3. `classify` finds the book first and the prefix second (scope.mjs:10-18), and the survey and reader
|
||||
call it (survey.mjs:66, reader.mjs:182). C2-01c rewrites it to check prefixes first. The brief should name
|
||||
every reader of `universe.books` and of `classify(...).book`, so a draft with no books and a null book
|
||||
don't reach code that assumes them. C2-05a, C2-06 and the survey's per-book counts are the likely ones.
|
||||
The existing mapping tests for `library-playground-outside-book` (mapping.test.mjs:16-20) change code
|
||||
too, so they're C2-01c's files.
|
||||
|
||||
N4. Section 8's row list has no C2-01c line, and the rows file has no brief for it. Sage briefs the row.
|
||||
The ruling's last paragraph is what that brief should copy.
|
||||
|
||||
## Checked and fine
|
||||
|
||||
- TARGET.md:92-95. Book level is unchanged. Universe level covers "for books in a universe" outside any
|
||||
one book. The draft covers "where new universes can be created". The PRD level note (:1487-1490) can
|
||||
drop its caveat once this lands.
|
||||
- REQ-LIB-2. Scope still comes from the path alone. A playground at any level is never canon. The
|
||||
lookups' canon default and `playground-cited-as-canon` don't depend on the level.
|
||||
- `citation-cross-universe` treating a draft as its own universe, R5 for a canon link into any playground
|
||||
and R6 for a link into a draft fit 1.3's table as written.
|
||||
- The type of a universe-level file comes from the first folder under its prefix, as `classify` does for
|
||||
playground files today (scope.mjs:14-17). So `library_find`'s `type?` keeps working there.
|
||||
- `library_manuscript` needs a book, so it never reads book-null files. That matches the PRD, which
|
||||
names the manuscript only per book.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Webby review: Case 2 playground ruling, three levels (mos-1233), round d2
|
||||
|
||||
Webby, 2026-10-21T22:34Z (from `date -u`). Author: Gosalyn (gosalyn-0125). Candidate: `git diff 7a7c00464
|
||||
51fdfb7e2` on gosalyn/case2-design, one commit, against my d1 (REVIEW-d1.md, 35130c26b). It's a design
|
||||
round, so there's no queue record.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- B1. The ruling and the 1.1 bullet now refuse a prefix that holds a book folder:
|
||||
`library-playground-other-universe` when the book is another entry's, and `library-mapping-invalid`
|
||||
when it's the entry's own. Section 8's C2-01c line asks for a test of each. My d1 example, a draft
|
||||
`Worlds/` over harbor's `Worlds/Book One`, now refuses. With that, REQ-LIB-3 holds as written. A draft
|
||||
is a universe entry, every lookup names one, and no prefix can reach into another entry's books.
|
||||
- N1. The 1.2 `unmapped` row ends "and outside every playground prefix".
|
||||
- N2. `kind` is absent, `"hybrid"` or `"draft"`, and any other value refuses. A draft takes `kind`,
|
||||
`name` and `playground` with at least one prefix.
|
||||
- N3. I checked the reader list at cca206cdc. mapping.mjs:73-78 builds `books` and `bookOwner`.
|
||||
reader.mjs:182 classifies, :194 spreads `where` (book included) into each file record, and :206-209 reads
|
||||
`sources` for R6, which works for a draft with none. survey.mjs:66 and mirror.mjs:246 read only
|
||||
`scope`. The unlanded C2-05a and C2-06 parts are named, and so are mapping.test.mjs:16-20.
|
||||
- N4. Section 8 has the C2-01c row, after C2-01, owner Opus, with the test gate.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
None.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Webby review: Case 2 design r1 (#416)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Independent reviewer, replacing Dewey (Mos, mos-0980).
|
||||
Subject: `agents/gosalyn/work/case2/DESIGN.md`, blob fba567f71dc1, at e997f3b76 and on
|
||||
origin/gosalyn/case2-design 88222f3f3 (same blob). Author: Gosalyn. Static review only. I ran no
|
||||
tests and changed no product code.
|
||||
|
||||
Code base: `git archive 5d019d876`. `git diff --stat 5d019d876 9ae8c73ac -- packages adapters roles scripts`
|
||||
is empty, so every line below also holds at origin/refactor 9ae8c73ac. Paths are relative to `packages/`
|
||||
unless they start with `adapters/` or `roles/`.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Twelve blockers. Section 6's no-schema claim holds. Section 5's write-side guardrails hold. The
|
||||
message-side guardrails have two channels the design misses (B3, B4), and the smallest slice can't
|
||||
run as written (B1, B2).
|
||||
|
||||
## Citations checked
|
||||
|
||||
These hold as cited: git.mjs:8 (`protocol.allow=never` in SAFE), git.mjs:44 (any `remote.*` key
|
||||
refuses), git.mjs:55-56; project index.mjs:12, :31-84, :97, :195-234, :209, :248-261, :256-258;
|
||||
paths.mjs:7, :14-15, :34-48 (`absolute(project.root)` at :41 throws on a missing root);
|
||||
business.mjs:186-187; gate.mjs:130-167 (typed by name at :134, built-ins at :137-138);
|
||||
claude-gate.mjs:13; tools.mjs:6-7, :95-102, :186, :211-220, :271-280; vocabulary.mjs:92-97, :131;
|
||||
role.mjs:19-28, :55-66; broker.mjs:290-291, :642-650, :1378-1430, :1434-1435, :1867, :2331-2347,
|
||||
:2374-2399; schema.sql:240-250, :296-306, :310; runner.mjs:446-456; seat session.mjs:59-64;
|
||||
adapters/claude/adapter.sh:47-53; launcher.mjs:135, :236-238, :353; webui bus.js:885, :919.
|
||||
|
||||
One miss: the `ls-tree` and `cat-file` reads that 1.3 cites at read.mjs:67-86 are in `blob()` at
|
||||
read.mjs:19-26 (N1).
|
||||
|
||||
## Section 6: "no schema or trigger change"
|
||||
|
||||
It holds. `threads` has no definition column or check (schema.sql:240-250).
|
||||
`message_threads_match` (:296-299) and `delivery_same_thread` (:300-306) check one thread's
|
||||
instance, and a room is one thread per instance, so each insert in `room.send` passes as written.
|
||||
`thread_one_per_run` allows one held thread per run, which matches one session per agent. The only
|
||||
constraint the design must name is `UNIQUE (project, name)` (:249). `room.open` names each thread
|
||||
after its instance, so an existing thread with that name refuses (N9).
|
||||
|
||||
The single-instance rule is code, not schema: broker.mjs:291, :2332-2334, launcher.mjs:135 and
|
||||
:236-237. Changing it is C2-09's job, which is why B1 matters.
|
||||
|
||||
## Jason's three guardrails
|
||||
|
||||
| Guardrail | Enforced in code? |
|
||||
|---|---|
|
||||
| Agents never write prose for the user | Into the library: yes. Gate (gate.mjs:137-138), typed tools (no `write_record` without `record.write`, tools.mjs:211) and human-only verbs make an agent write impossible. In messages: only the form is bounded (M5), and two channels skip M5 (B3, B4). The 400-character limit is stated in 5.1, but the stated residual is too small (B5) |
|
||||
| Agents always refer to the source material | Structural once B6 and B7 are fixed. The quote-in-lines check at a reachable commit is real verification |
|
||||
| Tool use is guarded | Yes: gate, typed tool list, broker re-check (tools.mjs:6-7) and `#checkMessaging` (broker.mjs:1867). N6 closes a role-file gap |
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. The smallest slice gives Melanie no way to chat with a creative agent. `#threaded` is true only
|
||||
for definition `assistant` (broker.mjs:291). `thread.create` refuses `thread-instance-ambiguous`
|
||||
with more than one (:2334). The launcher refuses `thread-not-allowed` for any other definition
|
||||
(launcher.mjs:135, :237). C2-08 adds the `THREAD_DEFINITIONS` constant, but C2-09 is the row that
|
||||
makes the broker and launcher read it and adds `instance` to `thread.create`, and C2-09 is outside
|
||||
the slice (DESIGN 9). The Console's thread creation must pass `instance` too. Fix: put C2-09's
|
||||
broker, launcher and `thread.create` parts in the slice. The Console chat must pick the instance.
|
||||
`room.*` can stay out.
|
||||
|
||||
B2. In the slice, Melanie reads raw JSON and can't see sync failures. Replies are JSON bodies (5.2),
|
||||
and only C2-10 renders them. 7.2 step 2 says "The Console shows them with file and line", but the
|
||||
library pages are C2-11b, outside the slice. A bad Obsidian push silently stops agents seeing new
|
||||
work. Fix: the slice needs a minimal cited-item renderer in the existing chat view and a sync status
|
||||
line (last valid commit, problems, `unpushed`).
|
||||
|
||||
B3. `raise_decision` is a second prose channel to the human. Every agent gets it with free-text
|
||||
`question` and option `text` (tools.mjs:221, comment at :5-6). An uncited raise goes to the human.
|
||||
M3's "no write tool exists" is true, but M5 covers only `message.send`. Fix: either refuse
|
||||
`decision.raise` from creative definitions in the broker and drop the tool from their typed set, or
|
||||
put the raise text through the same guard. Test both paths.
|
||||
|
||||
B4. The Console conversation view shows the model's raw text, including text M5 refused.
|
||||
webui serve.mjs:559-569 proxies `/api/conversation` to the control board, and the reader emits
|
||||
assistant text blocks (conversation/src/events.mjs:58, :82). A refused prose answer stays in the
|
||||
session file and is readable. Fix: leave creative sessions out of the catalogue on a library
|
||||
instance, or show stored messages only. Test that a creative session isn't listed.
|
||||
|
||||
B5. The stated limit understates what M5 lets through. A reply holds 20 items of 400 characters,
|
||||
which is 8,000 characters. An `idea` item's text is free, and any real quote of any line satisfies
|
||||
the cite. That is a full scene per reply, not "a one-sentence suggestion" (5.1). Jason wrote
|
||||
"NEVER", and Q176 says the system enforces it. Fix: add a total-characters cap per reply and a lower
|
||||
cap for `idea` items (for example 200 characters and at most 5 items). State the residual with
|
||||
those numbers. Mos takes the residual to Jason or Melanie for acceptance, because it is their rule.
|
||||
|
||||
B6. A `correction` isn't bound to the source. M5 limits the edit distance between `from` and `to`,
|
||||
but nothing requires `from` to be text in the cited lines. An agent can invent both. Fix: `from`
|
||||
must equal one cite's `quote`, after the same whitespace normalization, inside that cite's lines.
|
||||
Test that an invented `from` is refused.
|
||||
|
||||
B7. `citation-unfetched` has nothing to check against. The web trail stores the URL
|
||||
(broker.mjs:1521) and a summary of status, bytes and redirects (bus/src/web.mjs:46), not the page
|
||||
text. "The quote occurs in the fetched text" needs that text. Also, Claude's built-in WebSearch and
|
||||
WebFetch are on when `policy.webBuiltin` is set (gate.mjs:28-32), and those fetches never reach the
|
||||
broker. Fix: say where fetched text is kept (bounded, per run, or a new table, and if a table then
|
||||
say so). M1 must refuse the built-in web source for creative definitions, so `research` uses only
|
||||
the broker's tools.
|
||||
|
||||
B8. Remote content is a new untrusted input, and the S-rules don't check tree entries. Today only
|
||||
the bus writes record repos. Under 7.2, Obsidian or anyone with push to the Gitea repo writes too.
|
||||
A fast-forward checks out whatever the commit holds. No rule refuses symlinks (120000), gitlinks
|
||||
(160000), executables or nested `.gitattributes` and `.gitmodules`. Separately, S1 refuses the
|
||||
`.gitignore` that C2-12 commits, and Obsidian puts new notes and `.trash/` at the vault root by
|
||||
default. Fix: add a rule that every entry is a 100644 blob or a tree, and refuse those names at any
|
||||
depth. Allow `.gitignore` at the top. Commit Obsidian's new-file location and trash settings in
|
||||
`app.json`. Test each case.
|
||||
|
||||
B9. The fail-closed state in 7.2 strands Melanie's writes. If remote main is ahead and invalid, the
|
||||
bus stays on the old commit (step 2). The next bus write commits on that base. The push can't
|
||||
fast-forward, so the commit is `unpushed`, then `library-diverged`, with no merge until C2-17.
|
||||
S5 makes this likely: Obsidian users create `[[stub]]` links to notes that don't exist yet, so one
|
||||
dangling link in the playground blocks the whole sync. Fix: refuse library writes while remote main
|
||||
is ahead and invalid (`library-remote-invalid`). Apply S5 to canon only, or allow unresolved links
|
||||
in `playground/`. Test both.
|
||||
|
||||
B10. The 7.2 gate can't catch the obvious mutant. Tests use the file protocol through a test-only
|
||||
argument, so the https path, `GIT_ASKPASS` and the protocol override are never run. A mutant that
|
||||
sets `protocol.allow=always` passes them all. `git()` passes only `gitEnv()` plus the identity
|
||||
argument (git.mjs:10-12, seat checkout.mjs:79-89), so sync needs its own env entry for the helper.
|
||||
`GIT_TERMINAL_PROMPT=0` (checkout.mjs:85) doesn't stop askpass, which is fine. Fix: one function
|
||||
builds the production fetch and push argv and env. A test asserts that the argv holds SAFE, then
|
||||
exactly `-c protocol.https.allow=always`, and no other `protocol.*` key. Another asserts that
|
||||
`GIT_ASKPASS` names a helper outside the repo that reads the slot file. Keep the file-protocol
|
||||
switch out of any path that a business file or a bus verb can reach.
|
||||
|
||||
B11. An optional `root` breaks more than the overlap loop. `ensureCheckouts` clones every entry's
|
||||
root (seat checkout.mjs:310, :319). With no root it calls `clone(project, undefined, ...)`, so "a
|
||||
project with no root gives agents no clone" (2.1) is false today. `projectFilePath(entry.root)`
|
||||
(business/src/project.mjs:9-10) throws on undefined, and it is called at business/src/cli.mjs:72,
|
||||
:78, :118, resolve.mjs:44 and instance-settings.mjs:142. Fix: C2-04 lists these consumers, each
|
||||
skips a library project explicitly, and each has a test.
|
||||
|
||||
B12. Scope and crossovers can also come through front-matter fields, and in the slice a hybrid
|
||||
rests on convention. S2 and S3 speak of links only. `extends:`, `sources`, and `exclude` and `pin`
|
||||
paths are references too, and a canon book entity with `extends: playground/...` would bring
|
||||
playground text into canon lookups labelled canon. `library_manuscript` has no `scope`, so it must
|
||||
label or refuse a playground book. In the slice there is no crossover dialog (C2-11b), so a hybrid
|
||||
is a hand-written `universe.json` pushed from Obsidian, and S6 doesn't require `sanctioned`. Fix:
|
||||
put every path field under S2 and S3. Make S6 require `sanctioned`. State that in the slice a hybrid
|
||||
is Melanie's hand edit, and that only her account and the bus account can write to the Gitea repo.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Cite read.mjs:19-26 for the blob reads, not :67-86.
|
||||
N2. `END_PROTECTED` is `[THREAD_DEFINITION]` (vocabulary.mjs:135). Say whether creative
|
||||
definitions become end-protected when `THREAD_DEFINITIONS` replaces it.
|
||||
N3. The runner sends `(the session gave no answer)` and `The turn failed: ...` (runner.mjs:449).
|
||||
The guard will refuse both, so Melanie never learns a turn failed. Add a fixed turn-failed notice,
|
||||
and test that any notice other than the fixed bodies is refused.
|
||||
N4. Promotion moves several files. `commitFile` commits one path (index.mjs:219). C2-02 needs a
|
||||
multi-path commit.
|
||||
N5. Ids allow capitals (paths.mjs:7). Two universes that differ only by case collide on a
|
||||
case-insensitive file system. Enforce lowercase in `checkLibrary`.
|
||||
N6. A role with no `messaging` key may message anyone (role.mjs:50). M1 must require `messaging`
|
||||
for creative definitions.
|
||||
N7. C2-05b's sync-before-read needs C2-03, which its row doesn't list. C2-03 reads the remote URL
|
||||
that C2-04 adds to the business file, so either pass the URL in or order C2-04 first.
|
||||
N8. C2-06 must run the guard through both the runner's RESULT path and `send_message`, with a
|
||||
non-creative sender in a library business. Otherwise a mutant keyed on `CREATIVE_DEFINITIONS`
|
||||
passes, against 5.2's "role choice doesn't matter".
|
||||
N9. Name the refusal when `room.open` meets an existing thread with an instance's name
|
||||
(schema.sql:249).
|
||||
|
||||
## Rows and the date
|
||||
|
||||
The rows are small and most gates name the right refusal. B6, B10 and N8 are the gates that miss
|
||||
their obvious mutant. The listed slice (C2-01 to C2-08, C2-12, C2-13) does not let Melanie start.
|
||||
She can write in Obsidian, but she can't reach an agent (B1) or read its answers (B2). With
|
||||
C2-09's broker part and a minimal renderer added, it can be done by 10-27 only if T1 row 1 (row
|
||||
190) lands by about 10-24, as section 9 says. Reaching the Console from her machine also needs SSO
|
||||
(Q175).
|
||||
@@ -0,0 +1,74 @@
|
||||
# Webby review: Case 2 design r10 (#416), two system units
|
||||
|
||||
Webby, 2026-10-20T21:01Z (from `date -u`). Author: Gosalyn (gosalyn-0045). Commit 4281b0064 on
|
||||
gosalyn/case2-design, DESIGN blob 4a1860e698e0. Diff `git diff 51006710a 4281b0064`, one commit. I
|
||||
read both unit templates, scripts/webui-service.sh, webui `cli.mjs serve`, scripts/mosaic and decisions 62,
|
||||
94 and 102 on origin/refactor. Static review.
|
||||
|
||||
## Verdict: CHANGES (B1, B2, B3)
|
||||
|
||||
The fix is the right one. Two units from the existing templates match how the stack runs today, and C2-10b
|
||||
needs the Console. One copy for both units keeps the human proof working, since the bus accepts only its
|
||||
own human-cli.mjs path (webui-service.sh:266-273). Making `%h` literal is correct for a system unit. The new
|
||||
refusal cases are good.
|
||||
|
||||
B1. The Console's boot start is Jason's call, not mine or Mos's, and the design should default to the strict
|
||||
reading until he rules. Decision 94 (lead-decisions:2016-2023) says the process that starts the Console
|
||||
holds human authority, and that a start through systemd gives a clean ancestry the proof accepts, "the
|
||||
detachment gap decision 62 names". Decision 102 lets one named seat, Mos, start it. It calls that a
|
||||
tradeoff Jason accepted, because "the human proof now rests on Jason's grant to one named seat" (:2253). A
|
||||
boot-enabled system unit hands that start to PID 1 on every reboot, with no named starter. That extends a
|
||||
human-authority boundary, so under AGENTS.md it needs an owner decision. Decision 94 also accepts the
|
||||
local-forgery limit only "for this PoC on dragon-lin under Jason's user ... this decision doesn't carry the
|
||||
limit there" (:2058-2062). On .54 any process that reaches the Console port can send as Melanie, and
|
||||
`melanie`'s Hermes processes and sudo are on that host until #423 step 5. Nobody has accepted that limit
|
||||
for .54.
|
||||
|
||||
Fix: render the Console unit with no `[Install]` section, so a human or Mos starts it at step 9 and after
|
||||
each boot. That fails closed and is cheap to reverse, because adding `[Install]` later is one re-render.
|
||||
Put two questions to Jason through Mos: may a boot start stand in for the human's start on .54, and does he
|
||||
accept the local-forgery limit there? The bus unit can stay boot-enabled, since it holds no human
|
||||
capability.
|
||||
|
||||
B2. `@PATH@`, `@NODE@` and `@FLAGS@` have no render rule. Both templates set
|
||||
`Environment=PATH=@PATH@` (mosaic-bus.service.in:15, mosaic-webui.service.in:25). The bus `ExecStart` is
|
||||
scripts/mosaic, a bash script that runs `exec node` from PATH (scripts/mosaic:22-40). So the bus's node is
|
||||
whatever PATH the render writes, not `--node`. Row 195 found v22 on .53, and the precheck checks
|
||||
`node --version` on `<user>`'s shell PATH, not the unit's. Fix: say `@PATH@` is `dirname(--node)` followed
|
||||
by `/usr/local/bin:/usr/bin:/bin`, as webui-service.sh:246-248 does, and `@NODE@` is `--node`. Say
|
||||
`@FLAGS@` is the checked serve flags, and refuse any `@[A-Z_]+@` left in either render, not only `%h`. Add
|
||||
a test that the bus unit's PATH starts with the `--node` directory.
|
||||
|
||||
B3. The hardening lines are never listed. In r9 they lived in the new template that C2-13 owned. r10 drops
|
||||
that template, and the render "adds ... the hardening lines", but neither the design nor the brief says
|
||||
which ones. The test "a template with a hardening line removed" has nothing to remove: the templates carry
|
||||
only `NoNewPrivileges=yes`. Some common lines break sessions. `RestrictNamespaces=`, `PrivateUsers=` or a
|
||||
`SystemCallFilter=` without the namespace calls stops bwrap, and then no role starts. The bwrap precheck
|
||||
runs in `<user>`'s shell, outside the unit's sandbox, so it can't catch that. Fix: list the exact lines in
|
||||
the brief (for example `ProtectHome=yes`, `ProtectSystem=strict` with `ReadWritePaths=<home>`,
|
||||
`PrivateTmp=yes`, `NoNewPrivileges=yes`), say none of them restricts user namespaces, and add a step 9
|
||||
check that one session starts under the bus unit.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Both templates already end with `[Install]` `WantedBy=default.target` (bus :41-42, webui :39-40). The
|
||||
render should replace that line with `multi-user.target`, not add a second one. Under B1 it removes the
|
||||
Console's section.
|
||||
|
||||
N2. The render adds no `After=` or `Wants=`, so at boot the two units start in parallel. That is harmless
|
||||
today, because serve calls the bus per request (webui cli.mjs, `busCaller`) and starts without it. But the
|
||||
stated order applies only to a manual step 9. Either add `After=mosaic-bus@<business>.service` to the
|
||||
Console unit, or say that boot order doesn't matter.
|
||||
|
||||
N3. "The serve flags pass scripts/webui-service.sh's own flag checks" can't be done by calling that
|
||||
script. `serve_flags` (:146-207) is internal. Its proof-file check compares the owner to `id -u` (:201),
|
||||
which is root under case2-unit.sh. `render` finds node with `command -v` (:237), not `--node`. And
|
||||
webui-service.sh isn't in the files owned. Say whether case2-unit.sh copies the checks, with tests, or
|
||||
whether the row may move `serve_flags` into a sourced file. Also state which serve flags Case 2 uses.
|
||||
|
||||
N4. With a copy, webui-service.sh passes `--checkout <canonical>` (:251). Her host has no canonical
|
||||
checkout, so the queue read runs from the copy, and the queue store refuses that. Say that the Console unit
|
||||
gets no `--checkout`, and that the queue panel isn't part of Case 2.
|
||||
|
||||
N5. A re-pin must move both units to the same copy, or the human proof fails (webui-service.sh:266-273).
|
||||
Have case2-unit.sh refuse a render whose two units name different copy ids, and test it.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Webby review: Case 2 design r11 (#416), Console without [Install], render rules, 9.3
|
||||
|
||||
Webby, 2026-10-20T21:14Z (from `date -u`). Author: Gosalyn (gosalyn-0048, gosalyn-0051). Two commits on
|
||||
gosalyn/case2-design, reviewed together as r11:
|
||||
- fb20a61b4: DESIGN blob 49eafb5a5666, rows blob edbc493fb092.
|
||||
- eac92d956: rows only, rows blob 3a86d5f048dc. This is the one to re-pin.
|
||||
|
||||
Diffs: `git diff 4281b0064 fb20a61b4` and `git diff fb20a61b4 eac92d956`. I read B1 against mos-1097 and
|
||||
the port squat against mos-1100. I read the cited code at fb20a61b4. Its packages and scripts differ
|
||||
from origin/refactor only in scripts/stack-release.sh, which none of these citations use. This is a
|
||||
static review. I ran nothing on any host.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed, per mos-1097. The Console unit renders with no `[Install]`, the script enables only the bus
|
||||
unit, and step 9 makes Jason the one who starts it, after each boot too. The tradeoff after an unplanned
|
||||
reboot is written down. Q185 (jason-master-restart DECISIONS.md:1573) does put him in the homelab for
|
||||
1012 reboots. Mos asked for two facts, and 9.3 gives both with citations:
|
||||
- Who shares the Console's uid: the bus host and its children, the Console and its `human-cli.mjs`
|
||||
children (webui bus.mjs:1-3), every session inside bwrap, and the admin steps.
|
||||
- What a bwrap session can reach. The port, yes. sandbox.mjs:67 has no `--unshare-net`, and :29-31 says
|
||||
the network isn't confined. The proof file, no. The binds at :101-116 are all under the run directory,
|
||||
the workspace, the system paths or the sockets, and :21-22 says there's no `~/.config`. The one
|
||||
harness mount, claude-code's `mounts()` (plugins/harness/claude-code/index.mjs:73-77), is only the
|
||||
pinned binary.
|
||||
|
||||
The 401/403 reading of serve.mjs holds. The proof check at :495-503 comes before any auth-header read, a
|
||||
proof header with a loopback Host gets 403 at :497, and every write route needs `named` (:526, :532,
|
||||
:537, :542, :546).
|
||||
|
||||
B2 is fixed. I listed every token in both templates. The bus has `@REPO@`, `@PATH@`, `@CLAUDE_ENV@`,
|
||||
`@PM_FLAGS@`, `%h` and `%i`. The Console has `@REPO@`, `@PATH@`, `@NODE@` and `@FLAGS@`. Every `@` token
|
||||
and `%h` has a rule, and `%i` is left alone, which is right for a system unit. The leftover refusal is
|
||||
`@[A-Z_]+@` or `%h`, so it doesn't catch `%i`. The `@PATH@` rule fixes the case I missed in r10. With
|
||||
`--node /usr/bin/node`, webui-service.sh:246-248 would let a node in /usr/local/bin win, and there's a
|
||||
test for that case.
|
||||
|
||||
B3 is fixed. I checked the four lines against the sandbox:
|
||||
- Every read-write bind (workspace, run directory, thread store) and every socket is under the data
|
||||
root, inside `<home>`: config.mjs:19, git-socket.mjs:1, launcher.mjs:5. So `ReadWritePaths=<home>`
|
||||
covers them.
|
||||
- Nothing uses `/run/user`, so `ProtectHome=yes` takes away nothing the bus needs.
|
||||
- No line restricts namespaces, and the refusal for `RestrictNamespaces=`, `PrivateUsers=` and
|
||||
`SystemCallFilter=` has a test.
|
||||
- The step 9 session start is the real check, and it waits on #383 as stated.
|
||||
|
||||
N1-N5 from r10 are all in the C2-13a brief, with tests.
|
||||
|
||||
The port squat and its fix are right. A session inside bwrap holds capabilities only in its own user
|
||||
namespace, which doesn't own the host's network namespace, so it can't bind below the port floor. cli.mjs
|
||||
:27 takes ports 0..65535, so serve needs no change. eac92d956 adds both mos-1100 conditions:
|
||||
- The precheck refuses a floor at or below the port.
|
||||
- The bind test builds a real session argv with `binds()` (sandbox.mjs:101) and `sandboxArgv()` (:162).
|
||||
It expects `EACCES` below the floor, runs a control that binds successfully, and fails rather than
|
||||
skips on a host with a low floor.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. A correction to my own r10 B1. I cited the dragon-lin forgery acceptance as decision 94. It is
|
||||
decision 95 (lead-decisions.md:2048, the text at :2058-2062). r11 9.3 has it right. Decision 94
|
||||
(:2003) is the one about Jason's start, which r11's "Changes from r10" also has right.
|
||||
|
||||
N2. The bind test's control binds "a free port of 1024 or above". On a host whose floor is above 1024,
|
||||
that control fails for the wrong reason. Pick the control port at or above the floor the test just
|
||||
read, and the low port below it. It's one line in the brief.
|
||||
|
||||
N3. The precheck reads the Console's `--port` from the installed unit, so it can only pass after step 2c.
|
||||
Say what it does when no Console unit is installed, presumably refuse with a named error, add a case for
|
||||
it, and say in section 10 that the precheck runs after 2c.
|
||||
|
||||
N4. `AmbientCapabilities=` carries across exec, so each `human-cli.mjs` child the Console starts also
|
||||
holds `CAP_NET_BIND_SERVICE`. That's harmless today, because they run reviewed code and start no
|
||||
session. Add one sentence to 9.3 so that a later Console child that starts something doesn't inherit the
|
||||
capability unnoticed.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Webby review: Case 2 design r2 (#416, creative writing hub)
|
||||
|
||||
Webby, 2026-10-20T19:28Z (from `date -u`). Independent reviewer. Author: Gosalyn.
|
||||
Subject: `agents/gosalyn/work/case2/DESIGN.md` at c1913fb07 on gosalyn/case2-design, blob
|
||||
05a79cbca624, sha256 bda7a717...4055 (verified). Code read at 9ae8c73ac, where the citations point.
|
||||
Static review. I ran nothing. I read the whole file. The r1 items I don't name below read as fixed.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Four blockers. Three are in the persona path, which is new in r2. The data model rewrite onto her
|
||||
real repo is the right call, and the 1.3 claim mostly holds (see "The three questions").
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. A top-level playground has no universe. 1.1 (:169-170) suggests "one at the top", but 1.2
|
||||
(:191) gives a file the universe whose `books` list holds its book folder. A file under a top-level
|
||||
prefix has no book folder, so `library_find` (universe required) can't return it, and the cite
|
||||
check's one-universe rule (:464) has nothing to compare. Fix: put `playground` inside each universe
|
||||
entry, or refuse a prefix that isn't inside a book folder. Also give playground the `.md` condition
|
||||
canon has (:187). As written, a `.canvas` under a playground prefix is playground and citable.
|
||||
|
||||
B2. Where `persona.text` lives, and who can read it. Instance entries take inline `vars` only
|
||||
(business.mjs:89 closes the keys). Nothing lets an entry point at a file, so "a 0600 file that the
|
||||
business file entry takes" (:665, :722-723) has no mechanism. Inline, the text is read by more than
|
||||
the design says:
|
||||
- The tester's `instances` read is allowed (broker.mjs:103) and returns every instance's vars
|
||||
whole (instance-settings.mjs:58-63). That breaks "No agent sees the persona text" (:724) on any
|
||||
host with `bus.tester` on (vars.mjs, `bus.tester`).
|
||||
- Every organization write logs old and new values of each changed path in
|
||||
`businesses/.history/<id>/changes.jsonl` (organization.mjs:70-80, :201-204).
|
||||
Fix: say the text sits inline in the business file, or add a file-reference var with its own
|
||||
checks. Then either refuse persona keys in the tester's reads, or make C2-13's precheck refuse
|
||||
`bus.tester` on `<host>`. State the mode of the business file and `.history`. Test it in C2-08.
|
||||
|
||||
B3. Name the places `THREAD_DEFINITION` must stay `assistant`. Section 4 (:404-406) replaces it
|
||||
"wherever it means may hold a thread" and keeps only tools.mjs:186. launch-settings.mjs:140, :186
|
||||
and :191 also use it. They let `organization.set`'s `assistants` key set an assistant's whole vars,
|
||||
which the chief of staff holds (`ORGANIZATION_KEYS`, broker.mjs:145, :1266-1271, Q169). Read as "may hold a thread", that
|
||||
lets the chief of staff rewrite Bean's `persona.text` and model. Fix: list those three lines as
|
||||
staying `assistant`, and add a C2-08 test that `assistants` with `bean` refuses
|
||||
`organization-not-assistant`.
|
||||
The same change has a second effect the design doesn't state. With `END_PROTECTED` set to
|
||||
`THREAD_DEFINITIONS` (:407-408), `instances.set` refuses to add or change a creative instance
|
||||
(instance-settings.mjs:23, :109), and `definitions()` marks all six not accepted (:52). So her model
|
||||
choice for Bean, a persona edit and adding a specialist later are all hand edits of the business
|
||||
file. 4 (:394) and 10 step 7 read as if she can pick specialists later. Say how that happens.
|
||||
|
||||
B4. Name the persona block's exact place. `prompt()` is the contract (bundle.mjs:177), then
|
||||
`## This session` (:179) with the session lines. The last of those says "end each turn with the
|
||||
answer itself" (:188), then the end_run and review-loop lines follow. "After the contract" can
|
||||
mean before `## This session` or at the very end, and the C2-08 snapshot depends on which. I'd put
|
||||
it between the contract and `## This session`, so the broker's facts come after the persona. Fence
|
||||
it the way skills are (`SKILL_END`, :107, :170), and refuse persona text that holds the end marker
|
||||
or a line starting `## `. Hermes text is 8,000 characters that nobody here has read, so it can't be
|
||||
allowed to forge a section. The snapshot then pins the order: contract, persona, session.
|
||||
|
||||
## The three questions
|
||||
|
||||
1.3, reporting is enough. For the agent-side guarantee, yes. Scope comes from the path alone, and
|
||||
the cite check (:463) refuses a playground cite on `fact`, `finding` and `correction` whatever her
|
||||
files contain. None of R1-R7 changes a scope. Holding back her head would add nothing to that
|
||||
check. Two limits belong in the text (N1, N2).
|
||||
|
||||
M9. It holds today. Every organization setter builds its document from the file and changes only
|
||||
its own keys (broker.mjs:1261-1269, instance-settings.mjs:93-121, launch-settings.mjs:182-191), so
|
||||
no verb can change `projects` now. M9 guards against a future setter. Place it where that setter
|
||||
can't skip it (N3).
|
||||
|
||||
Persona order. See B4.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. `library_get` takes only `path` (:346), so its `links_out` and `links_in` are the one place a
|
||||
lookup can name a file in another universe or an unmapped file. Say both lists drop those targets
|
||||
(R6, R2), and add a C2-05a test.
|
||||
N2. Scope is the current mapping applied to the path. A note she moves into a book folder is canon
|
||||
on her next push, and a mapping edit re-scopes old commits for the cite check. Both are right, and
|
||||
both are hers. Put the first in the setup sheet (C2-12).
|
||||
N3. Put M9 in `organizationWriter.write` (organization.mjs:177), comparing `canonicalJson` of old
|
||||
and new `projects`, hybrid entries included. Since no verb can build the change, the C2-04 test
|
||||
calls `writer.write` directly with a changed playground prefix, and one with a changed hybrid.
|
||||
N4. Obsidian resolves a shortest-path link across the whole vault, which is `~/src` with her other
|
||||
repos in it. The reader sees one repo. A base name unique in the books repo but not in the vault
|
||||
can resolve to a different file than Obsidian shows her. Count those in the survey's link-form
|
||||
counts if the survey can see it, or note it in the setup sheet.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Webby review: Case 2 design r3 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:43Z (from `date -u`). Author: Gosalyn. Subject: `agents/gosalyn/work/case2/DESIGN.md` at
|
||||
2958acb81 on gosalyn/case2-design, blob 60bc7d682e0d, sha256 f368c27d...c325 (verified). Read as the diff from
|
||||
c1913fb07 (r2, my REVIEW at 162ce46db), with the code at 9ae8c73ac. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
My r2 B1, B2 and B4 are fixed, and so are N1 to N4. B3 is fixed for the three paths it names, but it misses a
|
||||
fourth (B1 below).
|
||||
|
||||
- r2 B1. Playground prefixes now sit inside a universe and must start with one of its book folders, or the
|
||||
file refuses with `library-playground-outside-book`. A `.canvas` under a prefix is unmapped. 1.2's universe
|
||||
rule then holds for playground files.
|
||||
- r2 B2. The text moves to `<configDir>/personas/<business>/<instance>.md`, and the vars hold only the name and
|
||||
the digest. I checked the modes against the code. `durable` (organization.mjs:59-61) and `log` (:138-141)
|
||||
open with 0600 and O_NOFOLLOW. `log` refuses a group or other bit. `directory` (:39, :45) makes 0700 and
|
||||
refuses a loose one. The `instances` read (instance-settings.mjs:61-63) copies vars whole, so it now carries
|
||||
only the digest. `previous.json` (organization.mjs:197-198) holds the old business file, which has no text
|
||||
either.
|
||||
- r2 B3. launch-settings.mjs:140, :186 and :191 stay `THREAD_DEFINITION`. `organization.set`'s `instances` key
|
||||
refuses a creative instance for every actor. `instances.set` gets the human-only creative branch.
|
||||
- r2 B4. The fence follows `SKILL_END` (bundle.mjs:107, :170). The block sits between `contract.trimEnd()`
|
||||
(:177) and "## This session" (:179). The snapshot pins that order.
|
||||
- The correction is right. broker.mjs:147-149 says the chief of staff or a human-resolved decision ends an
|
||||
`END_PROTECTED` instance.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. `routes.set` lets the tester change a creative instance's harness, model and thinking. It is in
|
||||
`TESTER_ORGANIZATION_VERBS` (broker.mjs:103), and the gate at :1259 lets a tester through. `setRoutes`
|
||||
(launch-settings.mjs:240-272) has no protected-definition check. It replaces the route vars of any named
|
||||
instance, then writes at :277. So the design's 4 is wrong in two places:
|
||||
- DESIGN.md:474-493 lists every path to a creative instance and leaves this one out.
|
||||
- DESIGN.md:493 says that with `bus.tester` on, the tester "could change no creative instance".
|
||||
|
||||
M10 (DESIGN.md:563) has no test for it. `bus.tester` is off on her business and C2-13 refuses it, so this is
|
||||
not reachable there today. But the design claims the rule holds even if testing were on, and on any library
|
||||
business that turns testing on, it doesn't.
|
||||
|
||||
Fix: in C2-08, have `setRoutes` refuse a `CREATIVE_DEFINITIONS` instance from actor `tester` with
|
||||
`organization-creative-human-only`, as the `instances.set` branch does. The human keeps `routes.set`, which is
|
||||
the Console's routing table. Add the path to the list at 474, fix 493, and add the case to M10's tests: the
|
||||
tester's `routes.set` on `bean` refuses, the file is unchanged, and the human's still works.
|
||||
|
||||
## Your two questions
|
||||
|
||||
Keying the creative branch on actor `"tester"` is enough. broker.mjs:1272 sets the actor to `"tester"` only
|
||||
for a tester session. The business load check refuses a human id of `tester` (broker.mjs:245) and an instance
|
||||
of that name (:248). instance-settings.mjs:86 refuses one too. So the string can't name the human, and the
|
||||
writer is the only place that knows the definition, because it reads the document. A broker-side check would
|
||||
have to read the file a second time to learn the definition. The one thing to add: make C2-08's tester case go
|
||||
through the broker with a bound tester session, not just `set(..., "tester")`, so a change to the actor line at
|
||||
:1272 fails the test. The same goes for B1's case.
|
||||
|
||||
The `# `/`## ` refusal is safe, because it fails closed, but it may refuse her real personas. The fence alone
|
||||
stops a section escape, because nothing after `</persona-file>` belongs to the persona. The heading rule
|
||||
guards against a model reading a `## ` line inside the fence as a new section, which is reasonable. I don't
|
||||
know what her Hermes persona files look like. If they use Markdown headings, C2-20 stops at the fence check
|
||||
(DESIGN.md row C2-20) and the design names no next step. See N2.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The chief of staff can change which model a creative instance runs without touching the instance.
|
||||
`organization.set` takes `models` (broker.mjs:145, ORGANIZATION_KEYS), and launch-settings.mjs:169 replaces
|
||||
the whole `launch.models` table. If `bean`'s `model` var is a key the chief of staff rewrites, Bean runs the new
|
||||
model. That matches what 4 promises, since the instance and the persona stay unchanged, but mos-1033's "only
|
||||
the human" reads wider. Either say in 4 that the models table is shared and the chief of staff may retune it,
|
||||
or refuse a `models` change to a key a creative instance names. Mos's call, so I'd put it to him.
|
||||
N2. For C2-20, count the lines starting `# ` or `## ` in each persona and print the count with the byte count,
|
||||
before Jason's go. Then state the remedy in 9.1. That could be Melanie or Jason editing the text, or a recorded
|
||||
rewrite to `### `. Any rewrite needs her say, because the text is hers.
|
||||
N3. "Only Bean's or Chet's own session sees it" (DESIGN.md:866) needs a qualifier. `buildBundle` writes
|
||||
prompt.md, persona included, once per run into the run directory at 0600 (bundle.mjs:241-244, :265). Run
|
||||
directories are write-once and stay until `prune`. The claude adapter also passes prompt.md as one argument
|
||||
(bundle.mjs:104-105), and that argument is visible in `/proc/<pid>/cmdline` to other local users. No verb reads
|
||||
either copy, and on her VM the only other user is `melanie`, whose personas they are. So this doesn't change the
|
||||
design's safety. The text should name both copies, and the C2-13 precheck should check the run root's mode
|
||||
with the rest.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Webby review: Case 2 design r4 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:58Z (from `date -u`). Author: Gosalyn. Subject: `agents/gosalyn/work/case2/DESIGN.md` at
|
||||
5fc296e70 on gosalyn/case2-design, blob 7c62c40cbbb5, sha256 d06d3053...ab0a (verified). Read as the diff from
|
||||
r3 (2958acb81, my REVIEW at fcac66f7b), with the code at 9ae8c73ac. Static review. I ran nothing, apart from
|
||||
reading pi 0.85.1's resource-loader.js in the canonical checkout's node_modules.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
My r3 B1 is fixed, and so are N1, N2 and N3.
|
||||
|
||||
- r3 B1. `routes.set` is now on the list (DESIGN.md:549-552), and :493 is fixed (now :565). The one rule keys
|
||||
on the business file's `human`. I checked the actor lines: broker.mjs:1272 sets `tester` or the human id,
|
||||
and :1278 sets `<definition>.<instance>` for a role. M10 has the tester's `routes.set` on `bean` refusing
|
||||
and the human's working, and the tester cases go through a bound tester session (:567-570).
|
||||
- N3. The adapter lines are right (claude adapter.sh:79, :82; pi :106, :121). I read pi 0.85.1's
|
||||
`resolvePromptInput` (dist/core/resource-loader.js:17-31). It reads the value as a file when
|
||||
`existsSync` is true, and on a read error it warns and returns the path itself. See N1 below for the case
|
||||
where the file doesn't exist.
|
||||
- N2. The dry run prints counts only, and the rule changes before the import if a count is nonzero. That's
|
||||
what I asked for.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. The human does reach the `models` write, through `limits.set`. DESIGN.md:29-30 and :545-548 say that
|
||||
`organization.set` takes only an agent session, "so no verb writes `launch.models` for the human today", and
|
||||
that Jason edits it by hand. The first half is true. The conclusion isn't:
|
||||
|
||||
- `limits.set` is a human verb (broker.mjs:1248). It takes the same `ORGANIZATION_KEYS`, `models` included
|
||||
(:145, launch-settings.mjs:16).
|
||||
- `organization.set` is `limits.set` for a role (broker.mjs:1249-1251). Both reach the same `set()`
|
||||
(:1284, launch-settings.mjs:150-169), and the `models` replace is at :169.
|
||||
- So the human's `limits.set { models }` replaces `launch.models` today, with actor = her id. The tester
|
||||
can't, because `limits.set` isn't in `TESTER_ORGANIZATION_VERBS` (broker.mjs:103).
|
||||
|
||||
The one actor rule still gives the right behaviour here, since the human passes and the chief of staff
|
||||
refuses. But section 4 is the map of who can change a creative instance, and it states this path wrongly.
|
||||
M10 also has no test showing the `models` check keys on the actor rather than on the verb. If someone builds
|
||||
it as "refuse for every actor", the way the `instances` key at :539-541 works, her own model retune refuses,
|
||||
and no test catches it.
|
||||
|
||||
Fix:
|
||||
- In both places, say that the human writes `launch.models` with `limits.set` and the chief of staff with
|
||||
`organization.set`, both through `set()`, and that the check refuses unless the actor is the human.
|
||||
- Add to M10 and C2-08: the human's `limits.set` change to Bean's `launch.models` key works, next to the chief
|
||||
of staff's change that refuses.
|
||||
- :541 already says the human's `instances: {bean: ...}` refuses. That case also goes through `limits.set`.
|
||||
Name the verb there too, so the test is written against it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Pi also uses the path as the prompt when the file doesn't exist. Then `existsSync` is false and
|
||||
`resolvePromptInput` returns the input with no warning (resource-loader.js:21, :30). So the adapter's check
|
||||
has to cover existence as well as readability. `[ -r "$f" ]` does both. Add a missing file to C2-08's pi
|
||||
adapter test, next to the unreadable one.
|
||||
|
||||
N2. The claude adapter test can only show what the adapter puts in argv. Whether claude 2.1.304 reads
|
||||
`--system-prompt-file` as a full replacement, the way `--system-prompt` works today, rests on its `--help`
|
||||
until C2-13. Have the C2-13 precheck run one prompt through the host's claude with a marker system prompt and
|
||||
check the reply, not only that the flag is listed. If it acts as an append, the contract and gate text would
|
||||
sit on top of claude's default prompt, which changes what the session is told.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Webby review: Case 2 design r5 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:54Z (from `date -u`). Author: Gosalyn. Subject: `agents/gosalyn/work/case2/DESIGN.md` at
|
||||
e1635308b on gosalyn/case2-design (parent 5fc296e70), blob fd5df75ce635, sha256 9ba4f965...2d22d (verified).
|
||||
Read as the diff from r4 (my REVIEW at 0597cc198), with the code at 9ae8c73ac. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- r4 B1 is fixed. Section 4 (DESIGN.md:555-557) says `limits.set` and `organization.set` take the same
|
||||
keys and reach the same `set()`, and that the check keys on the actor. I re-read
|
||||
packages/bus/src/broker.mjs:1248-1251 and :1284 at 9ae8c73ac, and they match. The `instances` case
|
||||
(:565-566) names both verbs. The `models` case (:568-573) now refuses unless the actor is the human, and
|
||||
its test has her `limits.set` change to Bean's key working next to the chief's refusal. M10 (:665) and
|
||||
C2-08 (:899) carry the same case. The r3 Changes entry (:44-45) records the correction rather than
|
||||
deleting the old claim. No stale "agent-only" or "by hand" claim is left (grep).
|
||||
- r4 N1 is fixed. :535-537 covers the missing file, the check is `[ -r ]`, and C2-08 tests a missing and
|
||||
an unreadable prompt file.
|
||||
- r4 N2 is fixed. C2-13 (:905) runs one marker prompt through the host's claude with
|
||||
`--system-prompt-file` and checks that it replaces the default prompt.
|
||||
|
||||
No new findings. Section 10 step 1 stays on hold per mos-1050. That isn't mine to clear.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Webby review: Case 2 design r6, section 10 step 1 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:59Z (from `date -u`). Author: Gosalyn. Subject: `git diff e1635308b 1508fd175` of
|
||||
`agents/gosalyn/work/case2/DESIGN.md` (r5 blob fd5df75ce635, approved in webby-0021, to r6 blob 80893dd4cff1).
|
||||
Static review. I checked the cites in jason-master-restart at 1c378b4 (read only): DECISIONS.md Q183 and
|
||||
projects/mosaic-mvp/MELANIE-HOST-RECON.md:27-39. Both say what r6 says they say.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
The Q183 reading is right. #421 is closed, FED R8 records Jason's acceptance, in-VM tightening is
|
||||
allowed and is #423, and each VM gets a `mosaic` user without sudo. Section 9's isolation paragraph
|
||||
(DESIGN.md:961-965) states the residual as it is now, with the recon cite.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Step 1 has lost the owner of the root steps on .54. Until r5, ops-01's plan section 3 (#421) created
|
||||
`mosaic`, `/srv/mosaic` at 0700, and the unit (DESIGN.md:947-950 still cites it). That plan stopped. r6
|
||||
step 1 (:1017) says "the install creates the `mosaic` user". But step 2 (:1023) runs the install "as
|
||||
`<user>`", and C2-13 runs as `<user>`. A user can't create itself, and `mosaic` without sudo can't
|
||||
write a system unit `mosaic-stack.service` (:948). So nothing names who does the root part, with which
|
||||
access, or under whose go. The only root paths into .54 today are pve root and `melanie`'s sudo
|
||||
(MELANIE-HOST-RECON.md:31, :35-36), and using either one is an act on Melanie's machine.
|
||||
|
||||
Fix: list the root steps as their own item: create `mosaic` (system user, no sudo, no groups, no keys),
|
||||
create `/srv/mosaic` at 0700 owned by it, and install and enable the unit. Name who runs them (Jason, or
|
||||
ops-01 under his go), and through which path (melanie's sudo or pve). Say that this is step 2's go and not
|
||||
step 1's. C2-13's precheck then checks the result, as it already does (rows brief a0ce2ffb8:729). Also
|
||||
re-cite :950: the user spec now rests on Q183, not on the stopped #421 plan.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. :1018 says the precheck "refuses if it has sudo or any group". A system user still has its own
|
||||
primary group. Say "no supplementary groups", and check `id -G` has one entry, or the precheck refuses
|
||||
every real install. The rows brief (:729) uses the same words, so fix both.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Webby review: Case 2 design r7, sections 9 and 10 and C2-13 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T20:02Z (from `date -u`). Author: Gosalyn. Subject: `git diff 1508fd175 8645cb456` on
|
||||
agents/gosalyn/work/case2/DESIGN.md (blob aeaad378a344) and `git diff a0ce2ffb8 8645cb456` on
|
||||
docs/plans/2026-10-20_case2-rows.md (the rows file arrived in a0ce2ffb8, between r6 and r7). Static
|
||||
review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
r6 B1 is fixed, and so is N1.
|
||||
- Section 10 step 2 (DESIGN.md:1054-1064) now splits 2a (root: useradd, /srv/mosaic 0700), 2b (the install
|
||||
as `<user>`) and 2c (root: install and enable the unit). It names ops-01 as owner, the localadmin
|
||||
path from #423 step 1, and Jason's go. Step 1 (:1045-1053) is right as written. Section 9 (:973-975)
|
||||
rests the layout on Q183, and the rows brief (:724-725) says the script doesn't create `<user>` or
|
||||
install the unit.
|
||||
- N1. "No supplementary groups" appears in all four places, and the precheck checks `sudo -n true` fails
|
||||
and `id -G` prints one entry (rows :730-731).
|
||||
- Mos's item 3. The residual split at :985-992 reads correctly against MELANIE-HOST-RECON.md:31, :35-36.
|
||||
|
||||
My one blocker is in step 2c and C2-13, not in step 1. I have no objection to the mos-1050 hold on step 1
|
||||
lifting now. Mos decides that.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Root installs a unit file that the unprivileged user wrote. C2-13's script runs as `mosaic` and
|
||||
writes the `mosaic-stack.service` file (rows :724-725, DESIGN.md :940). 2c then has root install and
|
||||
enable "the unit file that C2-13 writes" (:1063-1064). By 2c, 2b has already run `bootstrap.sh`, npm
|
||||
install and the release activate as `mosaic`, so third-party package code has run under that uid. Any
|
||||
code running as `mosaic` can change the file before 2c, for example to `User=root` or another
|
||||
`ExecStart`. Root then installs a root service. The same holds on every later re-install. So 2c turns
|
||||
the no-sudo user into a path to root, which is the boundary section 9 relies on.
|
||||
|
||||
Fix: 2c must not trust the content. Either root renders the unit from the reviewed template in the
|
||||
release, with `<user>` and the paths as inputs, or root copies the file to
|
||||
/etc/systemd/system/ as root:root 0644 and refuses unless `User=mosaic`, `Group=mosaic`, the
|
||||
`ExecStart` path, and the hardening lines (`ProtectHome` and the rest) match fixed expected values.
|
||||
Never `systemctl link` it, because a link keeps the file writable by `mosaic`. Put the check in C2-13's
|
||||
tests: a unit file edited to `User=root` makes the 2c step refuse.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. 2b runs the install "as `<user>`, by him or his delegate", but `mosaic` has no keys and no login.
|
||||
The only way in is `sudo -u mosaic` from `localadmin`. Say so, so that nobody adds a key to `mosaic`
|
||||
to make 2b work.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Webby review: Case 2 design r8, step 2c and C2-13 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T20:04Z (from `date -u`). Author: Gosalyn. Subject: `git diff 8645cb456 eab6e1a3a` on
|
||||
agents/gosalyn/work/case2/DESIGN.md (blob 5dd7a9c9b4c0) and docs/plans/2026-10-20_case2-rows.md. Static
|
||||
review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
r7 B1 is fixed, and so is N1.
|
||||
- `mosaic` writes no unit. In 2c (DESIGN.md:1084-1091, rows :728-736), root renders the unit from a
|
||||
root-owned checkout of the commit Jason's go names, outside /srv/mosaic. It checks `User=`, `Group=`,
|
||||
`ExecStart=` and the hardening lines, writes /etc/systemd/system as root:root 0644, never uses
|
||||
`systemctl link`, refuses a symlink, refuses a drifted installed unit, and reads nothing under
|
||||
`<user>`'s home. The tests (rows :753-759) cover an edit to `User=root`, an edited `ExecStart=`, a
|
||||
removed hardening line, a symlink, and the 0644 write.
|
||||
- Gosalyn's change from my fix is right, and my suggestion was wrong. The release is under /srv/mosaic and
|
||||
`mosaic` can write it, so a template there is the same path to root. The root-owned checkout closes it.
|
||||
- N1. 2b runs as `sudo -u mosaic -H` from localadmin, and `mosaic` gets no key (:1080-1081, rows
|
||||
:724-726). `sudo -u` with a command doesn't need a login shell.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The check covers named lines, not the whole file. A template line such as `ExecStartPre=+...`,
|
||||
`ExecStartPost=!...` or `PermissionsStartOnly=` runs as root even with `User=mosaic`. If it pointed
|
||||
into /srv/mosaic, that is the same path to root through a line the check doesn't read. The template is
|
||||
reviewed, so this is defence in depth. Either compare the whole render to one fixed expected file, or
|
||||
refuse any `Exec*=` value starting with `+` or `!` and any `PermissionsStartOnly`. Add one test case
|
||||
for it. The existing packages/cli/systemd/mosaic-bus.service.in has a single `ExecStart=` (:30), so
|
||||
the new template can follow it.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Webby review: Case 2 design r9 (#416)
|
||||
|
||||
Webby, 2026-10-20T20:48Z (from `date -u`). Author: Gosalyn (gosalyn-0035). Commit 29e85196b on
|
||||
gosalyn/case2-design, DESIGN blob 60a2d6cb751e. Round 8: case2-r8/REVIEW.md, approve on eab6e1a3a.
|
||||
|
||||
Scope: `git diff eab6e1a3a 29e85196b` on DESIGN.md and docs/plans/2026-10-20_case2-rows.md, including
|
||||
13a98bfd0 and 29404f996 as asked. Row A's and row B's briefs (f325657c7) are not part of this round. I
|
||||
read their citations only where the design shares them. This was a static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Model path (section 9). The citations hold on origin/refactor 088087ac9:
|
||||
- `ENV_ALLOW` (packages/seat/src/session.mjs:83-88) has no `ZAI_API_KEY`, so today the key is dropped.
|
||||
- pi-ai reads `zai` from `ZAI_API_KEY` (env-api-keys.js:89), and `glm-5.3` is a zai model id (see N1 for
|
||||
the file path).
|
||||
- pi's harness sets `countsAccount` to false (pi/index.mjs:42), so the account check at launcher.mjs:296-308
|
||||
never runs for pi. `preflight()` (:26) checks only the binary. `credentialStatus` (launcher.mjs:343) is
|
||||
metadata passed into the launch object (:385), not a refusal. So "a pi launch has no credential check"
|
||||
is true, and the precheck key-file line is the right place to catch a missing key.
|
||||
- `PI_PROVIDER` comes from `session.provider` (pi/index.mjs:57).
|
||||
|
||||
2d. The key file is the path the bus unit already loads (mosaic-bus.service.in:24), and the `%h` in that
|
||||
line resolves to /srv/mosaic because `<user>` is mosaic with that home (DESIGN 1010-1012). The design
|
||||
says plainly that melanie's sudo and her Hermes processes can read it until #423 step 5 (:1042). On
|
||||
Gosalyn's naming question, I'd keep `claude-session.env`. A new name means a second `EnvironmentFile=`
|
||||
line, which adds a line to case2-unit.sh's fixed set and departs from the bus unit. The cost of keeping
|
||||
it is a misleading name. A README line saying the file holds every session provider key, not only
|
||||
Claude's, fixes that, and it can ride row A's README edit.
|
||||
|
||||
C2-13 and the rows doc:
|
||||
- The marker prompt check now runs only for a claude-code creative instance, and a test shows a missing
|
||||
claude passes when there is none. That's right, since Bean and Chet run pi.
|
||||
- The key-file precheck has refusal cases for missing, 0644, symlink and empty, plus a search of the test
|
||||
output for the fixture value.
|
||||
- `EnvironmentFile=` joins the fixed lines, with removed and pointing-elsewhere cases.
|
||||
- The Exec prefix rule covers the whole leading run, with `+` and `-+` cases, as webby-0027 asked.
|
||||
- 13a98bfd0, `--profile factory|library`. `factory` requires the tester and refuses a library project
|
||||
(`profile-library-business`), and `library` refuses the tester. Each direction has a test, so a wrong
|
||||
flag can't reach a tester-on Case 2 install.
|
||||
- 13a98bfd0, node >= 24 and bwrap prechecks. Both have fake-binary refusal cases, and a bwrap refusal
|
||||
prints the userns sysctl and PATH state.
|
||||
- 29404f996, the copy id. The rule matches `copy_root` (scripts/stack-copy.sh:20): 40 lowercase hex and
|
||||
`scripts/mosaic` present. The tests cover short, uppercase, `/`, `..` and missing.
|
||||
|
||||
C2-20. The Hermes name count prints one count per persona file and never the line, number or word, and
|
||||
the test checks the output holds none of the matched words. The list comes from code at df5700ebe, not
|
||||
her profiles, so the script carries no persona text.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The model data file is `dist/providers/data/zai.json`, not `data/zai.json` (DESIGN:1023, and row A's
|
||||
brief). The id `glm-5.3` is there. Fix the path when the file is next touched.
|
||||
|
||||
N2. `copy_root` reads `COPIES` as `${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies`
|
||||
(stack-copy.sh:6), while case2-unit.sh fixes `<home>/.local/share/mosaic-stack/copies`. If mosaic's
|
||||
login env ever sets `XDG_DATA_HOME`, copies land elsewhere and the unit refuses. That fails closed, so it
|
||||
doesn't block. The install could check `XDG_DATA_HOME` is unset, or the rows doc could say why it's safe.
|
||||
|
||||
N3. "Sets a nonempty `ZAI_API_KEY`" should parse the file the way systemd does: the last assignment wins,
|
||||
and quotes are stripped. A file with `ZAI_API_KEY=x` then `ZAI_API_KEY=` passes a first-match grep but
|
||||
gives the session an empty key. Add that case to the refusal list.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Webby review: row A brief, ZAI_API_KEY in the session env allowlist
|
||||
|
||||
Webby, 2026-10-20T20:55Z (from `date -u`). Requested by Sage (sage-1477). Brief text: the "Brief: row A"
|
||||
section of docs/plans/2026-10-20_case2-rows.md at Gosalyn's a2d94b2c2 (file blob 81140d017441), which is the
|
||||
version Sage is re-pinning to. Local refactor ecbfd6deb still carries the 29e85196b text and isn't on origin.
|
||||
I read the cited code on origin/refactor. Static review.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Citations hold on origin/refactor:
|
||||
- `ENV_ALLOW` is session.mjs:83-88, with the provider line at :86 (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`).
|
||||
- `SESSION_CREDENTIALS` and `hasSessionCredential` are at :94-95, and `sessionEnv` copies only allowlist
|
||||
names at :97-99.
|
||||
- launcher.mjs:299 runs the account check only when `countsAccount` is true. pi returns false
|
||||
(pi/index.mjs:42).
|
||||
- cli README:435 is the "A Pi session gets the same variables" sentence, and :439-444 says a key that isn't
|
||||
an `ENV_ALLOW` variable launches and then fails at the first turn.
|
||||
- session.test.mjs:51-59 is the existing allowlist test. Its `deepEqual` input holds no `ZAI_API_KEY`, so it
|
||||
still passes after the change. :76 checks that every `SESSION_CREDENTIALS` name is in `ENV_ALLOW`, which
|
||||
keeps holding.
|
||||
- No test under packages/*/tests or scripts/test-*.sh pins a hash of session.mjs.
|
||||
|
||||
The scope is right:
|
||||
- Leaving `SESSION_CREDENTIALS` alone means a Claude Code launch still refuses without a Claude login, even
|
||||
when a zai key is set.
|
||||
- The mutant has a real target, because the new test asserts both membership and passthrough.
|
||||
- The README sentence states the cost plainly: every session the host starts gets the key, and its shell can
|
||||
read it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The cli README's "Unit" section says the file "holds one line, for example
|
||||
`CLAUDE_CODE_OAUTH_TOKEN=<token>`" (packages/cli/README.md:711). The brief adds a second sentence there. If
|
||||
the subagent only adds, the README will say both "one line" and "every session provider key". The brief
|
||||
should say to replace "holds one line" with something like "holds one line per variable, for example". This
|
||||
is a doc-only fix. Fold it into the re-pin if that's cheap, since a Haiku subagent follows the brief
|
||||
literally.
|
||||
|
||||
N2. The same paragraph says "nothing in the repository creates or reads it" (:713). That stays true for row
|
||||
A. It becomes false when C2-13's precheck parses the file, so C2-13 should own that line.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Webby review: Case 2 v1 briefs C2-09b, C2-10c and C2-15 (mos-1233), round d1
|
||||
|
||||
Webby, 2026-10-21T22:29Z (from `date -u`). Author: Gosalyn (gosalyn-0122). For mos-1233, Vikunja 464.
|
||||
Candidate: docs/plans/2026-10-20_case2-rows.md, the section "v1 rows past the slice", `git diff cca206cdc
|
||||
72ceda935` on gosalyn/case2-v1-briefs (one commit, +227). These are briefs, so there's no queue record.
|
||||
|
||||
What I checked, without runs: each brief against design 1.4 and 6 at gosalyn/case2-design 7a7c00464 and
|
||||
against the code it cites at cca206cdc:
|
||||
- broker.mjs: `thread.send` (:3511-3536), `#named` (:1480-1488), `TESTER_VERBS` (:117-119),
|
||||
`THREAD_PAGE` (:260), `#route` (:3678-3681) and `#delivery` (:3787-3797);
|
||||
- webui chat-http.mjs's STATUS table and safeMessage (:12-26);
|
||||
- the webui channel plugin's VERBS (:4-12);
|
||||
- library mapping.mjs's hybrid branch and mapping.test.mjs.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1. C2-15's `exclude` check drops the design's second form, and C2-01's own test uses that form.
|
||||
- Design 1.4 allows `{"exclude": "<source path or source/type>"}`.
|
||||
- mapping.test.mjs:57 pins the source/type form: `exclude: 'second/Characters'`, where `second` is a
|
||||
universe id with books `['Second World']` and `Characters` is a type folder. mapping.mjs:98-100 accepts
|
||||
it today.
|
||||
- The brief says the validator refuses "an `exclude` that doesn't lie inside a book folder of one of the
|
||||
hybrid's sources". `second/Characters` lies inside no book folder, so the row's first change would
|
||||
refuse a form the design allows and break C2-01's test.
|
||||
- The brief has to keep both forms and say how to tell them apart. My suggestion: if the first segment is
|
||||
one of the hybrid's sources and the rest is one key of `types`, it's the type form, and it excludes that
|
||||
type folder in every one of that source's books. Otherwise it must lie inside a source's book folder. If
|
||||
both readings fit, it refuses. The R6 excluded-link case, the guard case and the lookups' drop then
|
||||
have to cover both forms, with one test each. Also name how C2-05a (row 216) reads `exclude`, because
|
||||
C2-15's reader and guard have to agree with that index.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Your question, R8's second case. I agree with the reading in principle. A hybrid can't edit a
|
||||
source's file, so "a hybrid file changes it" (design 1.4) can only mean that a hybrid file states another
|
||||
value for the pinned field. Linking to the entity is a fair test of "about that entity". But as written
|
||||
it gives false positives. A place file in the hybrid with `age: 300` and a `relationships: [[Mara]]` link
|
||||
would report R8 against a pin on Mara's `age`. Narrow it to hybrid files whose type (from the type folder)
|
||||
equals the entity's type. Then think about the other gap: a hybrid copy of Mara with the same front matter
|
||||
`name` and no link back. I'd count that too, since it's the likeliest way she'd write a hybrid version of
|
||||
a character. Either way it's report-only, so it doesn't block. Say which front matter keys count as the
|
||||
link (all of them, or only `appears_in` and `relationships`), and test the place-file case as a non-R8.
|
||||
|
||||
N2. C2-15's validator should also refuse a `pin.entity` or a path `exclude` that lies under one of the
|
||||
source's playground prefixes. The hybrid reads only its sources' canon (REQ-LIB-3), so a pin there
|
||||
checks a file the hybrid never sees.
|
||||
|
||||
N3. C2-09b's `room.read` groups by shared `at`, body and sender. Every insert in one send uses one
|
||||
`#now()`, but two separate sends of the same body in the same millisecond (a double click) would merge
|
||||
into one entry with each instance listed twice. Since one send's rows go in one transaction, their
|
||||
`messages.seq` values are consecutive. Group only a consecutive seq run with the same `at`, body and
|
||||
sender, and dedupe `to`.
|
||||
|
||||
N4. C2-09b resolves a tag through `#named` or as an instance key. A tag can match one instance's display
|
||||
name and another instance's key. The brief should rule which wins, or refuse it as ambiguous, with a test.
|
||||
|
||||
N5. C2-10c's line "maps `unknown-addressee` and `room-thread-name-taken` to 409 and `room-not-open` to 409"
|
||||
reads as if two statuses were meant. If all three are 409, say "maps all three to 409".
|
||||
|
||||
N6. Pinning cost, as you said. Every briefed Case 2 row pins this file's blob, so 72ceda935 flags them all
|
||||
until Sage re-pins. Landing both of Sage's edits under one re-pin makes sense.
|
||||
|
||||
## Checked and fine
|
||||
|
||||
- C2-09b reuses what `thread.send` writes (:3511-3536): `#checkMessaging`, one message row, `#route`
|
||||
and one `human.input`. `#route` and `#delivery` only insert rows (:3678-3681, :3787-3797), so one
|
||||
dispatch transaction does undo a partial send. Leaving the room verbs out of `TESTER_VERBS` makes a
|
||||
tester refuse as for any other verb, and the gate's grep names tester-writes.test.mjs.
|
||||
- `room.open`'s refusal on a same-name thread elsewhere fits the per-project unique name
|
||||
(schema.sql:249), and "no schema change" holds.
|
||||
- C2-10c keeps the broker as the authority for addressing. The parity test reuses C2-09b's table. The
|
||||
plugin test checks that other verbs still refuse, which matters because VERBS (:4-12) is the webui
|
||||
channel's allowlist.
|
||||
- C2-15 names Opus and Filbert, and lists its after rows and the M9 refusal test. The out-of-scope line
|
||||
that sends draft sources to C2-01c matches the playground ruling.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Webby review: Case 2 v1 briefs C2-09b, C2-10c and C2-15 (mos-1233), round d2
|
||||
|
||||
Webby, 2026-10-21T22:34Z (from `date -u`). Author: Gosalyn (gosalyn-0125). Candidate: `git diff 72ceda935
|
||||
99b1e0859` on gosalyn/case2-v1-briefs, one commit. The rows file blob is c45816509, as Gosalyn said. I
|
||||
reviewed it against my d1 (REVIEW-d1.md, 35130c26b). These are briefs, so there's no queue record.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- B1. C2-15 keeps both exclude forms of design 1.4.
|
||||
- The type form is a first segment that is one of the hybrid's sources plus exactly one `types` key.
|
||||
`second/Characters` fits it: `second` is a universe id and `Characters` is a `types` key in
|
||||
mapping.test.mjs.
|
||||
- Anything else is the path form, inside or equal to a source book folder.
|
||||
- When both readings fit, it refuses.
|
||||
- One `excluded(mapping, hybrid, path)` helper in scope.mjs serves C2-05a's index, R6 and the guard.
|
||||
If C2-05a lands first, this row replaces C2-05a's own read, so all three share one reading.
|
||||
- Each form gets a case on the lookups, R6 and the guard, and the test at :57 still checks.
|
||||
- N1. R8 now counts a hybrid version of the entity: the same type from `classify`, and the same front
|
||||
matter `name` (or the same file name when either file has none). A link alone doesn't count. That's
|
||||
better than my narrowing: the hybrid Tom who links Mara and has his own `age` would have passed a
|
||||
type-only rule. The tests cover the no-link version as R8, and the place file and the other character
|
||||
as non-R8.
|
||||
- N2. A pin entity or path exclude under a source playground prefix refuses.
|
||||
- N3. `room.read` groups only a run of consecutive seqs from the human with the same `at` and body. The
|
||||
double-click case has a test.
|
||||
- N4. A tag that is one member's name and another member's key refuses `addressee-ambiguous` and
|
||||
inserts nothing. A tag that names one member both ways resolves to that member. The new code is on the
|
||||
line for Sage.
|
||||
- N5. C2-10c maps all four room codes to 409.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. R8's version rule says "a hybrid file" without a scope. A hybrid can have playground prefixes, and
|
||||
a brainstorm file there with the same name and another value would report R8. It's report-only, so the
|
||||
cost is noise in `library.status`, not a wrong lookup. If Melanie should be free to try other values
|
||||
in the playground, the builder can limit the version check to hybrid canon files and add a test for a
|
||||
playground version as non-R8. That's the builder's and Filbert's call, not a change to this brief.
|
||||
@@ -0,0 +1,360 @@
|
||||
# Webby static look: row 191 Console layout fix (sage-2364, mos-1248 condition 3)
|
||||
|
||||
Asked by Sage at 2026-10-22T02:50:06Z. Git reads only, no runs. Base: Rocko's 0f8235277
|
||||
(origin/rocko/row191-stoprun-ddfb-20261022).
|
||||
|
||||
## The failure, read from the base
|
||||
|
||||
- shell.test.mjs:380 asserts `scrollHeight === innerHeight` on the Board at 1440x1000. Rocko's gate
|
||||
reports 1062 against 1000.
|
||||
- index.html loads processing.css after shell.css (:4 and :6), and both use `.frame` (0,1,0). So
|
||||
processing.css:14 `min-height:0` wins over shell.css:135 `min-height:calc(100vh - var(--bar-h) -
|
||||
var(--foot-h))` at 760 px and wider. The frame has no floor of its own any more.
|
||||
- processing.css:12 makes body a flex column with `min-height:100vh`. Its in-flow children are the
|
||||
cmdbar (53 px, `--bar-h`), #processing-controls, the hidden banner, .frame, and the sticky footer (33 px,
|
||||
`--foot-h`). The tree is `position:fixed` (processing.css:16), so it takes no height.
|
||||
- Nothing caps the body at 100vh, so the page is cmdbar + bar + frame content + footer whenever that sum
|
||||
is over 1000. 1062 - 1000 = 62, and 62 is the bar's height from Sage's report. 1000 - 53 - 33 = 914 is
|
||||
the frame height shell.css:135 used to set. So the Board's content is at most 914 px and over 852 px.
|
||||
Before 191 it fit, and the bar now takes its share.
|
||||
- That means no rule on the frame alone can fix it while the window scrolls. The content needs 914 px,
|
||||
and the bar leaves 852. A fix has to do one of three things:
|
||||
1. take the bar out of flow, for example sticky or fixed over the frame's top, and pad the content by
|
||||
the bar's height only where the tree already sits;
|
||||
2. scroll the frame inside a body capped at 100vh, which changes :382-385 because the window would
|
||||
no longer scroll at 420 px;
|
||||
3. make the Board shorter.
|
||||
I'll judge Rocko's diff against these.
|
||||
|
||||
## Checks for Rocko's diff
|
||||
|
||||
1. **Layout.** At 1440x1000 the bar's box lies inside 0..1000, and scrollHeight equals innerHeight
|
||||
because the page fits, not because the window can't scroll. The same holds at 760-959 px (nav 200 px,
|
||||
shell.css:142-151) and at 1900 px and up (nav 280/320 px).
|
||||
2. **Tall page.** At 1440x420, scrolled to the end, :382-385 still see `scrollHeight > innerHeight`,
|
||||
with the tree between the cmdbar bottom and the footer top. The bar must not cover the tree or
|
||||
the first Board row.
|
||||
3. **:380 unchanged.** `git diff 0f8235277 <fix> -- packages/webui/tests/shell.test.mjs` adds lines and
|
||||
changes none at :373-385.
|
||||
4. **New assertion.** It must fail when the fix is reverted. For that it needs the bar's own box,
|
||||
`#processing-controls` getBoundingClientRect, against innerHeight and the cmdbar, at the size that
|
||||
failed. A check on scrollHeight alone at a size where the Board fits anyway would pass with the bug.
|
||||
5. **Mutant.** It must revert the CSS fix and nothing else, and it must name the killing test.
|
||||
|
||||
## Verdict on 5cd32f4c1 (rocko 02:57:02Z): CHANGES, B1
|
||||
|
||||
Read at 2026-10-22T02:59Z (from `date -u`). It's one commit on 0f8235277: processing.css (+12/-2), shell.test.mjs (+11/-0)
|
||||
and BUILD.md. Nothing has run, mine included.
|
||||
|
||||
**B1. As written, the CSS delta doesn't change the height of the page that failed.**
|
||||
(processing.css:12-24 at 5cd32f4c1)
|
||||
- Old body: a flex column with `min-height:100vh`, and the frame `flex:1; min-height:0`. The body's
|
||||
height is auto, so the frame's 0% basis resolves to its content, and the body comes to
|
||||
max(100vh, cmdbar + bar + C + footer), with C the frame's in-flow content height.
|
||||
- New body: a grid with `min-height:100vh` and the frame on row 4 as `minmax(min-content,1fr)`.
|
||||
min-content of the frame is the same C. The track can't go below it, and 1fr only spends space that is
|
||||
left over. So the body comes to max(100vh, cmdbar + bar + C + footer) again.
|
||||
- The two give the same number. For 1062 to come out of the old one, C has to be over 852
|
||||
(1000 - 53 - 62 - 33). In that case the new one gives 1062 too, and :380 fails exactly as before.
|
||||
1062 is exactly 1000 plus the bar, so C is likely 914 = `100vh - var(--bar-h) - var(--foot-h)`. That
|
||||
points at a box in the frame's flow that is sized to the viewport, and the delta doesn't touch it.
|
||||
I couldn't find which box it is from the stylesheets. processing.css:22 `min-height:0` does beat
|
||||
shell.css:135 on load order, and `.frame > .tree` is fixed. So it needs a measurement, not more CSS.
|
||||
- **Before the gate.** On a slot, run one probe at 1440x1000 on the Board (the shell.test fixture) and
|
||||
report:
|
||||
- the `.frame` rect height and `#main` scrollHeight;
|
||||
- the rect of every in-flow child of body;
|
||||
- the computed `min-height` and `position` of `.frame` and `.tree`.
|
||||
|
||||
Run it on 0f8235277 and on 5cd32f4c1. If the two match at 1062, this delta isn't the fix.
|
||||
Then there are three ways out:
|
||||
1. remove the box sized to 100vh - 86;
|
||||
2. take the bar out of the frame's height budget, for example by subtracting a `--processing-h`
|
||||
from the shell's calc() values, or by placing the bar in a row the shell already counts;
|
||||
3. if the Board really needs 914 px, it isn't a short page with the bar on it. Changing :374's
|
||||
viewport would be a test change for Sage and Mos to rule on, not for Rocko or me.
|
||||
- Rocko's BUILD.md says a second red stops the window and goes to Mos. That's one more reason not to
|
||||
open the gate on an unmeasured fix.
|
||||
|
||||
**What holds.**
|
||||
- **:380 is unchanged.** The diff removes no line in shell.test.mjs, and :373-380 and :392-397 (the old
|
||||
:381-386) are byte-identical.
|
||||
- **The new assertions (:381-391) help.** The visible and positive-height check catches a fix that hides
|
||||
the bar, which :380 alone would pass. The check that the bar sits under the cmdbar and inside the
|
||||
viewport is the bar's own box, as I asked. :391 repeats :380, which runs first, so it can never be
|
||||
the one that fails. N1: drop it, or leave it as documentation.
|
||||
- **KS59** reverts processing.css to 0f8235277's bytes. :380 would kill it, not the new assertions. That's
|
||||
fine, since a revert is the failing state. I'd add a second mutant that sets `#processing-controls`
|
||||
to `display:none` with the fix in place, so :388 has a kill of its own.
|
||||
- **The grid body's auto-placed children.** a.skip is `position:absolute` (app.css:24), #announce is
|
||||
.sr-only absolute, #toast is fixed, the dialog is closed, and a hidden banner leaves row 3 at 0. None
|
||||
of them takes a row, so the explicit rows hold. Sticky still works for the cmdbar and the footer as
|
||||
grid items. At 1440x420 the min-content row lets a long page grow, so :394-396 keep their premise.
|
||||
At 760-959 px the same rows apply with the 200 px nav. I see no regression there, but none of
|
||||
this matters until B1 is settled.
|
||||
|
||||
## Trace for sage-2368, and a correction to B1
|
||||
|
||||
Read at 2026-10-22T03:00Z (from `date -u`), git only.
|
||||
|
||||
**Correction.** Above, I treated 62 as the bar's height and guessed at a box in the frame sized to
|
||||
100vh - 86. Both were inferences, and the second is wrong. 62 is only the overflow:
|
||||
53 + barH + C + 33 - 1000. Neither the gate log nor the diff gives barH.
|
||||
|
||||
**What set the frame's height on the 1440x1000 Board under 0f8235277.** The frame's only in-flow child
|
||||
is #main, the Board's own content.
|
||||
- body children (index.html): a.skip, header.cmdbar, #processing-controls, #processing-banner (hidden),
|
||||
.frame, footer.foot, #announce, #toast, dialog#cmdk-dialog.
|
||||
- .frame children: nav.tree, main#main.content, aside#inspector (hidden).
|
||||
- **nav.tree.** processing.css:16 `.frame > .tree { position:fixed }` is (0,2,0). It beats `.tree`
|
||||
sticky at console.css:14 and shell.css:144, and static at console.css:39. So the tree is out of flow.
|
||||
- **aside#inspector.** shell.test opens it at :246 and closes it with Escape at :248, and :374 opens no
|
||||
session. app.js:88 sets `hidden = !selected`, and close() (app.js:402) clears `selected`. No
|
||||
.inspector rule sets `display`, so [hidden] holds and the inspector takes no space.
|
||||
- **min-height.** processing.css:14 `min-height:0` beats shell.css:135 and console.css:12 on load order
|
||||
(index.html:4 then :6), as Sage says. Rocko's cause, shell.css:135, isn't the cause.
|
||||
- That leaves #main's content C. The old page was max(1000, 86 + barH + C). Before 191 there was no bar,
|
||||
and shell.css:135 lifted the frame to 914, so the page fit whenever C <= 914. It went to 1062 because
|
||||
86 + barH + C > 1000: the Board's content and the bar together don't fit in 1000 px. shell.test:373
|
||||
already says the Board grew at row 95 ("the short page is taller").
|
||||
|
||||
**Does 5cd32f4c1 remove it?** No. Row 4 is `minmax(min-content,1fr)`, and min-content is the same C, so the
|
||||
page is again max(1000, 86 + barH + C) = 1062. B1 stands, with this cause.
|
||||
- The probe I asked for still settles it. It now only needs the `#main` height and the
|
||||
`#processing-controls` height at 1440x1000, which should sum with 86 to 1062.
|
||||
- No grid or flex rule can fix it while the window scrolls and the bar takes vertical space. The ways out
|
||||
change design or test:
|
||||
1. put the bar where it costs no page height, such as inside the fixed nav column or the cmdbar row;
|
||||
2. make the Board shorter;
|
||||
3. rule that the Board at 1000 px is no longer a short page, which means changing :374 or :380.
|
||||
|
||||
These are calls for Sage and Mos, not something to fix in CSS.
|
||||
|
||||
**Small checks.**
|
||||
- **No implicit row 6.** a.skip is `position:absolute` (app.css:24), and #announce is .sr-only absolute
|
||||
(app.css:23, shell.css:139). #toast is fixed (shell.css:104). The dialog is display:none while closed,
|
||||
and in the top layer while open. Absolutely positioned and top-layer boxes aren't grid items, so none
|
||||
of them adds a row. The hidden banner leaves row 3 at 0.
|
||||
- **760-959 px behaves as before.** console.css:37-38 drops the frame to one column at <=959. shell.css:143
|
||||
(760-959) restores `200px minmax(0,1fr)` and loads later with the same specificity, so
|
||||
`.frame > .content { grid-column:2 }` still has a column 2. The tree stays fixed at
|
||||
`--processing-nav:200px` (processing.css:28). 5cd32f4c1 changes none of that, only the body rows.
|
||||
|
||||
## Structure ruling for sage-2382 and Rocko 03:04:20Z
|
||||
|
||||
Read at 2026-10-22T03:06Z (from `date -u`), git only, on 5cd32f4c1. No runs.
|
||||
|
||||
**Ruling: (i), the labeled region as the first child of nav.tree, with conditions R1-R6. Drop sticky.**
|
||||
|
||||
Why (i) and not (ii).
|
||||
- **:380 edges.** In (i) nav.tree stays the same element, class and fixed box (processing.css:26,
|
||||
shell.css:136). A child can't move the nav's rect, so :396 and the 420-tall check hold as before.
|
||||
In (ii) the nav keeps its box too, but the controls sit over it.
|
||||
- **Fragility.** The controls have no fixed height. #processing-receipt is aria-live text that
|
||||
changes, and #processing-unavailable wraps to several lines at 200 or 240 px. (ii) has to reserve
|
||||
that space without a measurement. A fixed reservation goes wrong as soon as the receipt changes or the
|
||||
text zooms, and then the overlay covers nav links. Subgrid plus a wrapper is a larger change than the
|
||||
fix needs. (i) gets the real auto height from normal flow.
|
||||
- **Landmark.** ARIA allows a region inside navigation, and axe's top-level rules cover only
|
||||
banner, main, contentinfo and complementary. A landmark list then shows "Instance processing" under
|
||||
"Console". That's a cost, since Stop isn't navigation, but the name and role still say what it is.
|
||||
Tab order is unchanged: cmdbar, then Stop, then the nav links, the same as the body-child layout
|
||||
today. I accept the nesting for this row.
|
||||
- **Height.** With the controls out of body flow, the page is max(innerHeight, 53 + C + 33), C being
|
||||
#main's content. That's the pre-191 arithmetic, and it fits whenever C <= 914 at 1000 tall. If the
|
||||
Board's C is over 914 on its own, no layout fixes :380. The debug run settles that.
|
||||
|
||||
Why no sticky.
|
||||
- nav.tree scrolls itself (console.css:14 `overflow:auto`). A sticky region at its top covers a
|
||||
focused nav link whenever the nav is scrolled and the user tabs back up. That's WCAG 2.4.11 (focus
|
||||
not obscured), and nothing sets scroll-padding to stop it.
|
||||
- At 1440x420 the nav is 334 px tall (420 - 53 - 33). A sticky region with the note shown can take
|
||||
over half of it.
|
||||
- Without sticky, Stop is the nav's first content. It's in view at the nav's scroll origin, and
|
||||
focus scrolls it into view.
|
||||
|
||||
Conditions.
|
||||
- **R1. Markup.** Move the existing div as is: id, class, role=region and aria-label "Instance
|
||||
processing". It goes before `<h2 class="tree-title">Console</h2>` in index.html:22.
|
||||
#processing-state keeps role=status, and the receipt keeps aria-live. #processing-banner stays
|
||||
where it is.
|
||||
- **R2. Descendant rules.** As a child of .tree, the controls match rules written for nav content:
|
||||
- console.css:21 `.tree .btn{margin-top:auto}` hits Stop and Resume;
|
||||
- console.css:40 `.tree .btn{margin:0;order:1}` at <=959 px. The region is `display:flex`
|
||||
(processing.css:1), so order:1 moves Stop and Resume after the state text, the note and the
|
||||
receipt. The visual order then differs from the DOM order at 400 px;
|
||||
- live.css:5 `.tree .small{margin-top:auto}`, live.css:10 `.tree .small{flex-basis:100%;margin:0}`
|
||||
and shell.css:147 `.tree .small{flex-basis:auto;margin-top:auto}` hit the note and the receipt.
|
||||
|
||||
Add rules that put them back, with `.tree .processing-controls .btn` and `.small` at (0,3,0). Don't
|
||||
edit the shell rules. The new test asserts that Stop's computed `order` is 0 at 400 px, and a mutant
|
||||
that removes the reset must fail it.
|
||||
- **R3. Below 760 px.** .tree is a wrapping flex row (console.css:39). The region needs
|
||||
`flex-basis:100%`, or it shares a line with the "Console" title (`flex:1`, console.css:40). Then
|
||||
Stop sits above the title and the Projects list. Test at 400 px that Stop's rect top is above
|
||||
#projects' rect top, and that Stop is visible.
|
||||
- **R4. Remove dead layout.** Drop the body grid and the `.processing-controls` grid-row and
|
||||
margin-left rules from 5cd32f4c1. They apply to nothing once the controls are in the nav. Keep
|
||||
`.frame > .tree { position:fixed }` and the `--processing-nav` widths. The `.frame`
|
||||
`min-height:0` that beats shell.css:135 then goes back to what it did on 0f8235277. That's fine,
|
||||
because the frame's height is C either way.
|
||||
- **R5. Tests.**
|
||||
- shell.test :373-380 and :392-397 stay byte-identical.
|
||||
- :381-390 still hold. The bar's top sits under the cmdbar and its bottom inside the viewport at
|
||||
1440x1000. Add a check that the bar's rect lies inside the tree's rect.
|
||||
- :67 becomes the mos-1250 text (sage-2384): not inside #main or aside.inspector, and before #main
|
||||
in source. The bar is in nav.tree, which comes before #main, so it passes. The move-into-#main
|
||||
mutant must fail it.
|
||||
- Every shipped-strings assertion is unchanged.
|
||||
- My N2 still applies: a `display:none` mutant has to have :388 as its own kill.
|
||||
- **R6. Debug run (Sage's slot).** Report the height of #processing-controls and nav.tree at
|
||||
1440x1000, 1440x420 and 800x420, and #main's height at 1440x1000. If the region is over half the
|
||||
nav at 1440x420, tell me before the window. It isn't a blocker, but it's a usability call for
|
||||
Sage.
|
||||
|
||||
Not a condition. When the halt banner is shown it's still a body row, so a halted Board can scroll at
|
||||
1000 tall. :380 runs with it hidden. I'd file it as a follow-up and not widen this row.
|
||||
|
||||
## Sticky versus sage-2372 (Rocko 03:06:49Z)
|
||||
|
||||
Read at 2026-10-22T03:08Z (from `date -u`), git only. No runs.
|
||||
|
||||
**Ruling: the separate scroller. The controls don't scroll and the links scroll under them inside the
|
||||
same nav.tree box.** It meets both sage-2372 (the controls never scroll out) and 2.4.11 (nothing overlays
|
||||
a focused link). The controls aren't positioned over the scroller. They take their own flex row above it.
|
||||
|
||||
Conditions S1-S5, on top of R1-R6 (R1's "before the Console h2" still holds):
|
||||
- **S1. Markup.** nav.tree's children become the controls region, then one plain div with no role or
|
||||
label, for example `.tree-scroll`. That div holds the existing children in their existing order: the
|
||||
Console h2, #sections, #board-tree and the rest. Ids and classes don't change. No test or script
|
||||
reads nav children by position. Only ids are used: shell.test :272/:280/:282 #sections,
|
||||
board-held.test :110 #board-tree.
|
||||
- **S2. 760 px and up.** nav.tree keeps its fixed box (top --bar-h, height 100vh - bar - foot) and
|
||||
goes to `overflow:hidden`. The controls are `flex:none`. `.tree-scroll` is
|
||||
`flex:1 1 auto; min-height:0; overflow:auto`, and it repeats console.css:14's column flex and
|
||||
`gap:10px`. Then `.tree .btn{margin-top:auto}` (console.css:21) and `.tree .small{margin-top:auto}`
|
||||
(live.css:5, shell.css:147) still push the nav's own button and note to the bottom. The same applies
|
||||
at 760-959 under shell.css:144.
|
||||
- **S3. Below 760 px.** `.tree-scroll{display:contents}`, so its children stay flex items of the
|
||||
wrapping row (console.css:39-40, live.css:10). The `order:1`/`order:2` layout and shell :280 "one
|
||||
row" keep working without edits. The region gets flex-basis:100% (R3), and nav.tree stays static
|
||||
with no scroll.
|
||||
- **S4. Tests.** Everything in R5 still applies. Add a check at 1440x420: scroll `.tree-scroll` to its
|
||||
end, and Stop's rect is still inside the viewport, under the cmdbar. A mutant that sets
|
||||
`.tree-scroll{overflow:visible}` and nav.tree back to `overflow:auto` must fail it.
|
||||
- **S5. R6 still applies.** If the region is over half of nav.tree at 1440x420, the scroller gets
|
||||
small. That's Sage's usability call before the window.
|
||||
|
||||
## webby-0107 withdrawn (sage-2386)
|
||||
|
||||
Read at 2026-10-22T03:09Z (from `date -u`). Sage's sage-2386 (03:07:50Z) crossed with webby-0107. It accepts
|
||||
non-sticky, withdraws the sage-2372 long-nav visibility requirement and rules out a separate links
|
||||
scroller. The scroller existed only to meet that requirement, so S1-S5 are withdrawn. R1-R6 stand as
|
||||
written, with ordinary nav.tree overflow:auto. Under R6, I still want the region and nav heights at
|
||||
1440x420 on the debug run.
|
||||
|
||||
## Static look at da4334806 (rocko 03:10:31Z): no blockers
|
||||
|
||||
Read at 2026-10-22T03:12Z (from `date -u`), git only. One commit on 5cd32f4c1 touching index.html,
|
||||
processing.css, three test files and BUILD.md. Nothing has run. Manifest digest 245dceb1 (88 paths), as
|
||||
Rocko reported. I haven't recomputed it, so that's for the manifest round.
|
||||
|
||||
**R1 markup.** The region div moves byte-for-byte to nav.tree's first child, before the Console h2.
|
||||
Its id, class, role=region, aria-label, role=status and aria-live are all unchanged. The banner keeps its
|
||||
place in body, before .frame. processing.js is unchanged.
|
||||
|
||||
**R2 descendant rules.** `.tree .processing-controls .btn {margin:0; order:0}` and `.small {margin:0;
|
||||
flex-basis:100%}` are (0,3,0) and sit outside any media query, so they beat console.css:21/:40, live.css:5/:10 and shell.css:147 (all (0,2,0))
|
||||
at every width. The base `.btn` (shared/app.css:60) has no margin, so `margin:0` changes nothing on its
|
||||
own. `.processing-controls p {margin:0}` and the note/receipt `flex-basis:100%` already held, so the
|
||||
`.small` reset only cancels the tree rules. console.css:21 `justify-content:center` still reaches the
|
||||
buttons. That's harmless. No other `.tree` or `nav` descendant rule matches the controls.
|
||||
|
||||
**R3 below 760.** `.tree > .processing-controls {flex-basis:100%}` at <=759 puts the region on its own
|
||||
line ahead of the Console title. The new browser test checks Stop's top against #projects' top at 400 px.
|
||||
|
||||
**R4.** The body grid, the grid-row rules, the controls' margin-left and `.frame {min-height:0}` are all
|
||||
gone. With no body rule left in processing.css, shell.css:135 lifts the frame to 100vh - 86 again.
|
||||
`.frame > .tree {position:fixed}`, the content column and the nav widths stay.
|
||||
|
||||
**Height.** The body is block flow again: cmdbar 53, then the hidden banner at 0, then
|
||||
.frame = max(914, C), then the footer 33. So the page is max(1000, 86 + C), with no bar term. #main's
|
||||
content doesn't change in 191, so C is the pre-191 value that passed :380. The debug run confirms it.
|
||||
|
||||
**R5 tests.**
|
||||
- shell.test is additions only, at :389-399. :373-388 and every later old line are byte-identical, :380
|
||||
included. The new check puts the bar inside `.tree`'s rect, and the diagnostic logs heights.
|
||||
- processing-ui :67 has the mos-1250 text, quoted old and new in BUILD.md. `<main id="main"` and
|
||||
`<aside class="inspector"` match the literal markup, and there's exactly one `</aside>`. KS61 (into
|
||||
#main) moves controlsAt past mainAt, so it fails. If the aside markup is ever renamed, inspectorAt is
|
||||
-1, and `indexOf('</aside>', -1)` searches from 0. The check then fails closed, not open.
|
||||
- processing-browser: the new test covers 400x1000, 1440x1000, 1440x420 and 800x420. Order is
|
||||
checked at 800 and 400, inside the <=959 band where console.css:40 applies order:1, so KS62 is killed
|
||||
there. Visibility and the cmdbar/viewport bounds are checked on load.
|
||||
- Mutants: KS59 is a full revert of HTML and CSS, KS60 hides the controls (N2 met), KS61 moves them into
|
||||
#main, KS62 drops the order reset. The total is 77.
|
||||
|
||||
**sage-2378 checks.**
|
||||
1. Ids, role and label are kept.
|
||||
2. Nested in the nav landmark, which I accepted in webby-0106.
|
||||
3. Stop is visible at 400 and 1440x1000, and above Projects at 400.
|
||||
4. Every existing assertion is unchanged, other than :67 under mos-1250, and the shipped strings are
|
||||
untouched.
|
||||
5. The height arithmetic is above.
|
||||
|
||||
**Notes, none blocking.**
|
||||
- N1 (carried): :402 (the old :391) still repeats :380.
|
||||
- N3: `focus()` then activeElement only proves Stop is focusable and enabled when that line runs. It
|
||||
doesn't prove Tab order. Stop is `disabled` in the markup until processing.js reads the state. If the
|
||||
fixture leaves it disabled, this line fails on the run, so it's a fixture question, not a test hole.
|
||||
DOM order already gives cmdbar, Stop, nav links.
|
||||
- N4: R6 reporting is in place (ROW191_LAYOUT lines). Sage's half-nav call at 1440x420 waits on those
|
||||
numbers.
|
||||
|
||||
### Addendum for sage-2392: full 0f8235277..da4334806 range, Sage's two items, KS59-KS62
|
||||
|
||||
Read at 2026-10-22T03:13Z (from `date -u`), git only.
|
||||
- **Range.** `git diff --stat 0f8235277 da4334806 -- packages/` has five files: index.html 8, processing.css
|
||||
14, processing-browser +27, processing-ui +5/-1, and shell.test +22/-0 (0 removed lines). The body flex,
|
||||
the controls' margin-left and `.frame{flex:1;min-height:0}` from 0f8235277 are all gone, as is
|
||||
5cd32f4c1's grid. The verdict above holds for the whole range.
|
||||
- **Item 1, flex containers below 760.** .tree is `display:flex` at every width (console.css:14), and
|
||||
it's a wrapping row at <=959 (console.css:39). shell.css:158-171 (<=759) doesn't touch .tree's display.
|
||||
The region is `display:flex; flex-wrap:wrap` at every width (processing.css:1), and nothing overrides
|
||||
that. So `.tree > .processing-controls{flex-basis:100%}` works in the tree's row, and the `.small`
|
||||
flex-basis works in the region's row.
|
||||
- **Item 2, order.** `.tree .processing-controls .btn` is (0,3,0) against console.css:40's `.tree .btn`
|
||||
at (0,2,0). A media query adds no specificity, so the reset wins whatever the load order.
|
||||
- **KS59-KS62**, read from all-mutants.json in Rocko's packet (77 entries):
|
||||
- **KS59.** The HTML edit plus an extra_edits entry on processing.css restore 0f8235277's exact markup
|
||||
and CSS. That's the state the gate failed at 1062, so :380 kills it.
|
||||
- **KS60.** Appends `#processing-controls{display:none}` at (1,0,0), which beats the class rule. :380
|
||||
can't see it any more, so the visibility check at shell :388 is its kill. N2 is met.
|
||||
- **KS61.** Moves the region to the head of #main, so processing-ui :67's controlsAt < mainAt fails.
|
||||
- **KS62.** Deletes the `.btn` reset line, and the order assertion at 800 and 400 kills it.
|
||||
- All four are unrun.
|
||||
|
||||
N3 closed (Rocko 03:13:09Z, checked at 2026-10-22T03:14Z). processing.js:41-52 fetches /api/config with the
|
||||
state, sets `canWrite` (:46) and then calls draw(). The status text reads "Processing is enabled." only
|
||||
when that draw runs with a snapshot, and :29 sets `halt.disabled` in the same call. The test's setup
|
||||
waits for "enabled" (processing-browser:39) with config `send:true` (:27). So Stop is enabled before
|
||||
focus() runs.
|
||||
|
||||
## Debug run on da4334806 (Rocko, 03:14:37-03:14:53Z)
|
||||
|
||||
Read at 2026-10-22T03:16Z (from `date -u`), from Rocko's debug-da4334806 logs. I ran nothing.
|
||||
- **Counts.** shell.test reports `# tests 5 / pass 5 / fail 0 / skipped 0`, and processing-browser
|
||||
reports 9/9/0/0. The total is 14/0/0, as Rocko said.
|
||||
- **ROW191_LAYOUT matches the report.**
|
||||
- Writable controls are 127 px, and the tree is 914 at 1000 tall and 334 at 420.
|
||||
- Read-only controls are 212.5 at 1440x1000.
|
||||
- At 400x1000, Stop's top is 181.5, the cmdbar bottom is 129 and #projects starts at 352.6, with
|
||||
order 0.
|
||||
- **R6.** The writable controls are 127 of 334 at 1440x420 (38%), under half. The read-only ones were
|
||||
measured only at 1000 tall, but their height depends on width, not viewport height (wrapping at
|
||||
240 px). So at 1440x420 they would be 212.5 of 334, about 64%. That's over half.
|
||||
- **My read for Sage's call.** It's acceptable. The region isn't sticky and scrolls away with the nav, so
|
||||
the 64% applies only at the nav's scroll origin, and every nav link is still one scroll away.
|
||||
#projects already began below the fold at 1440x420 before 191: 636.6 - 127 = 509.6 is past 420. A
|
||||
420-tall desktop window isn't the normal case. If Sage wants it smaller, the fix is a shorter
|
||||
read-only note, not layout.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Webby review: row 192 r1, Q175 phase 1 SSO (#1690)
|
||||
|
||||
Webby, 2026-10-20T19:49Z (from `date -u`). Author: Launchpad. Candidate 83b32d8d8 on launchpad/row192, base
|
||||
53bb59462. Manifest `candidate-manifest.sha256` digest 3ad2a73d...badab, verified: all 18 path hashes match
|
||||
the candidate, the path list matches `git diff --stat 53bb59462 83b32d8d8`, and none of the 18 paths changed
|
||||
from the base to origin/refactor (aa1bd209e). Design: `agents/sage/work/q175-sso/DESIGN.md`. Static review
|
||||
plus one node probe of `returnPath` (below). I ran no suite and no mutant. I did not read
|
||||
/mnt/storage/scratch/launchpad-r192.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
The build follows the design closely. I checked these against the design and found them right:
|
||||
|
||||
- Callback steps 2-7 (sso.mjs:232-285):
|
||||
- The login record is taken and deleted before any check, and the provider comes from the record.
|
||||
- `validateApplicationLevelSignature` runs on the same Response, and its throw ends the callback.
|
||||
- The header alg is checked against `algs`, `iss` is compared raw, and `sub` is looked up in the subjects Map.
|
||||
- The `subject-unknown` line carries only the provider, iss and sub, JSON-quoted and capped at 255.
|
||||
- Sessions:
|
||||
- The store is keyed by sha256 and the cookie strings are exact.
|
||||
- The caps are 64 sessions and 32 logins.
|
||||
- Idle and absolute expiry are server-side.
|
||||
- `touch` happens only on a POST or a GET outside `/api/` (serve.mjs, the `sso.session` call).
|
||||
- Host (F4). Under `--sso`, anything but the public Host is 403, except GET `/healthz`. `/auth/*` routes go
|
||||
to `sso.handle` before the session check, and a request without a session gets 401 on `/api/` and a 303
|
||||
on a page.
|
||||
- Logout needs the exact Origin and `Sec-Fetch-Site: same-origin`, and a refused logout keeps the session.
|
||||
- Discovery (Q-B, sso.mjs:92-121, :151-182):
|
||||
- Unreachable and Refused are split as the design says.
|
||||
- A Refused provider stops the start. An unreachable one retries with doubling, capped at 300 s.
|
||||
- A Refused result on a retry stops the retries.
|
||||
- `readSlot` (sso.mjs:33-61):
|
||||
- lstat refuses a symlink, then the open uses O_NOFOLLOW, then fstat checks the file type, the uid and the
|
||||
mode against 0o077.
|
||||
- The realpath checks cover the repo, the checkout and the copies root.
|
||||
- The default copies root matches `stack-copy.sh:6`. I read the owner branch. It is right, and as the
|
||||
design says, no test covers it.
|
||||
- Tester listener (section 6, F8):
|
||||
- `startServer` throws on `tester` with `sso`.
|
||||
- Under `--sso` the tester listener refuses Authorization, either sign-in cookie and any `x-authentik-*`
|
||||
header, and `/auth/login` is 404 there.
|
||||
- The tester writer is unchanged.
|
||||
- The loader (C1) checks shape only, and test 25's FIFO slot proves no case opens a slot file.
|
||||
`mosaic-config.mjs` going async is safe because every reader runs it as a subprocess
|
||||
(cli/src/config.mjs:22, business/src/cli.mjs:32).
|
||||
- `webui-service.sh` refuses `--sso` with the preview set or the proof pair, and its status grep reads the new
|
||||
flag order.
|
||||
- The page: both app.js fetch paths, plus the two bus.js reads (F1), sign out on 401 `sign-in required` only.
|
||||
The link goes to `/auth/login?return=<path and query>`.
|
||||
- Every design mutant is in `mutants.py`, plus 10 more. A pattern that matches no test exits 0 and counts as
|
||||
SURVIVED, so a typo in a pattern fails safe.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Open redirect after sign-in. `returnPath` (sso.mjs:200-206) accepts any URL whose origin is
|
||||
`publicOrigin` and returns `u.pathname + u.search`. The callback puts that string in `Location` (:284). If
|
||||
the pathname starts with `//`, the browser reads the Location as scheme-relative and leaves the Console. I
|
||||
ran the function as written in node, with `publicOrigin = https://mosaic.example.com`:
|
||||
|
||||
| `return=` | `returnPath` | Browser goes to |
|
||||
| --- | --- | --- |
|
||||
| `/.//evil.example` | `//evil.example` | `https://evil.example/` |
|
||||
| `/x/..//evil.example` | `//evil.example` | `https://evil.example/` |
|
||||
| `https://mosaic.example.com//evil.example` | `//evil.example` | `https://evil.example/` |
|
||||
| the four test 14 cases | `/` | the Console |
|
||||
|
||||
Here is how an attacker uses it. They send Jason `https://<public host>/auth/login?return=/.//evil.example`.
|
||||
He signs in at the real provider, and the Console then sends him to the attacker's page. The `.`/`..` segments
|
||||
are why. Test 14's `//evil.example` parses with evil.example as the host, so the origin check catches it.
|
||||
`/.//evil.example` parses as a path on the Console, so the origin check passes, and dot-segment removal
|
||||
leaves the pathname as `//evil.example`. Design B2 and test 14 exist to stop exactly this.
|
||||
|
||||
Fix: return an absolute URL on the Console, `publicOrigin + u.pathname + u.search`. Then `//evil.example`
|
||||
becomes `https://<host>//evil.example`, which stays on the Console. Or refuse a pathname that starts with
|
||||
`//`. Add the three rows above to test 14, expecting `/` (or the absolute form, if you choose it), and add a
|
||||
mutant that restores the current return so test 14 kills it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The brief and BUILD.md:4 cite design blob eba861f5d2bb (d55d86bb0). Refactor and the candidate have
|
||||
9e3eadb15c80 (b416d2ede), which adds Mos's straggler check. The two differ only in the phase 2 window, so
|
||||
the build isn't affected. Cite the current blob in the next round.
|
||||
|
||||
N2. Design section 6 says the tester listener also refuses "the old auth and proof header names". Under
|
||||
`--sso`, cli.mjs:108 passes `testerProofHeader: proof?.header ?? null`, and that is always null because
|
||||
`--sso` refuses `--proof-*`. So the old proof header name isn't refused on 7333. The `x-authentik-*` prefix
|
||||
covers the old auth header. Nothing on 7333 reads the proof header, so this is harmless. Either add the
|
||||
name the live unit uses to the F8 refusal, or list it under Deviations.
|
||||
|
||||
N3. serve.mjs:64-66: the new `SIGN_IN_COOKIE` line went between the row 177 comment and
|
||||
`TESTER_CHAT_WRITE`, the line that comment describes. Move it above the comment, with its own comment.
|
||||
|
||||
N4. For the phase 2 window and row 200, not this row. Before 192, `mosaic-config.mjs` refuses any unknown
|
||||
top-level key (53bb59462 scripts/mosaic-config.mjs:112-116). Once `console` is in the live config.json, every
|
||||
loader from a pre-192 tree refuses the whole config, and every tool on that tree fails closed. That covers
|
||||
older pinned copies, any checkout behind 192, and a rollback target older than 192. A 192+ tree without
|
||||
`packages/webui/src/sso-config.mjs` refuses too (the import fail in `validateConsole`). The
|
||||
stack-release.test.mjs:1793 fixture copies `scripts/mosaic-config.mjs` without it, which is fine while the
|
||||
fixture config has no `console`. So the window has to re-pin to 192+ before it writes the key, and a rollback
|
||||
past 192 has to remove the key first. Put both in the window plan.
|
||||
|
||||
N5. A HEAD request for a page with no session gets 401 JSON, not the 303, because serve.mjs checks
|
||||
`req.method !== 'GET'`. Browsers don't send that, so no change is needed.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 192 r2, Q175 phase 1 SSO (#1690)
|
||||
|
||||
Webby, 2026-10-20T19:58Z (from `date -u`). Author: Launchpad. Candidate 37582e820 on launchpad/row192 (one commit on
|
||||
83b32d8d8), base 53bb59462, queue rev 2312. Manifest `candidate-manifest.sha256` digest 6f8cee14...7787,
|
||||
verified. All 18 path hashes match the candidate. The paths are `git diff --name-only 53bb59462 37582e820`
|
||||
less the four packet files under agents/launchpad/work/queue-192/. None of the 18 paths changed from the base
|
||||
to origin/refactor (ae3c36f24). Static review plus one node probe of `returnURL` (below). I ran no suite and
|
||||
no mutant. I did not read /mnt/storage/scratch/launchpad-r192, so the 28/28 and mutant results are
|
||||
Launchpad's report, not my run.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- r1 B1 is fixed. `returnURL` (sso.mjs:199-211) parses the return against publicOrigin, refuses another
|
||||
origin, and builds `publicOrigin + pathname + search` as an absolute URL. The callback's Location (:289)
|
||||
is now always absolute. Only the callback uses it; the export at :341 follows the rename. Test 14 has my
|
||||
three rows, `/board/./x`, and an origin assert on every case. The positive test expects the absolute
|
||||
form. Mutant 22 restores the r1 return under test 14.
|
||||
- Probe. I ran the function as written in node with publicOrigin `https://console.example.test` over 18
|
||||
inputs: my three rows, `/%2e%2e//evil`, `/.\\/evil`, `///evil`, `//evil`, `/\\evil`,
|
||||
`/@evil.example`, `/:evil`, CR/LF in the path (raw and encoded), a full-width dot, `:443`, upper-case
|
||||
scheme and host, and a fragment. Every result stayed on the Console's origin. Raw CR/LF are stripped by
|
||||
the URL parser, so no header split.
|
||||
- Mutant 23 (drop the second origin check). I agree it is equivalent. sso-config.mjs:43-44 holds
|
||||
publicOrigin to a bare https origin, and a parsed special-scheme pathname always starts with `/`, so the
|
||||
concatenation can't parse to another host. In the probe the mutant returned the same string as the build
|
||||
for all 18 inputs. Keeping the check as an unreachable guard is fine with me. Whether to keep it is
|
||||
Sage's call, as BUILD.md says.
|
||||
- N1 (design blob 9e3eadb15c80), N2 (under Deviations), N3 (serve.mjs:64-66, comment order) are done. N4
|
||||
moves to row 200 (#1698) and the window plan through Sage (mos-1058). N5 needs no change.
|
||||
|
||||
No new findings.
|
||||
@@ -0,0 +1,179 @@
|
||||
# Webby review: row 194 (#1692, agent lifecycle), build b4 as cut on b5, static
|
||||
|
||||
Webby, 2026-10-21 (times from `date -u` in the session line). Author: Honker (honker-0073, honker-0075).
|
||||
Request by sage-2230 and honker-0075: review b5, not f482890dc.
|
||||
|
||||
- **Candidate.** honker/row194-b5 at dfabbbe8a, six commits on origin/refactor da9555ca2.
|
||||
- **Manifest.** Honker gives 51 paths. `git diff --name-only da9555ca2 dfabbbe8a` gives 52. The extra one is
|
||||
agents/honker/work/queue-194/PRD-1.8-DRAFT.md, which stays out (sage-1626).
|
||||
- **The row.** It is in-progress, so I record nothing until Sage posts the request.
|
||||
|
||||
What I did, static only:
|
||||
- **b5 against b4.** On the 52 paths, `git diff f482890dc dfabbbe8a` touches only docs/plans/DEFERRED.md.
|
||||
So I read b4's code once, at f482890dc, and every line cite below holds at dfabbbe8a.
|
||||
- **Drift.** da9555ca2 is an ancestor of origin/refactor, and `da9555ca2..origin/refactor` touches none of
|
||||
the 52 paths.
|
||||
- **DEFERRED.md.** The diff keeps refactor's entries and adds the creative-delete entry after them.
|
||||
- **The read.** Four read-only subagents split the diff: bus, business, cli, and webui with harness. I
|
||||
re-read the code behind every finding below myself. Findings I couldn't confirm aren't listed.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
### B1. After a skipped reload, veto, hire and fire answer `restart: false`
|
||||
|
||||
- **Where.** packages/bus/src/runtime.mjs:280-282:
|
||||
|
||||
```
|
||||
const live=reload(id);
|
||||
...
|
||||
return verb==='instances.set'?{...result,restart:!live}:result;
|
||||
```
|
||||
|
||||
The writer's `commit()` always answers `restart: false` (instance-settings.mjs:386). So for
|
||||
`veto.set`, `instance.hire` and `instance.fire`, a reload that `sameFixed` refused (runtime.mjs:225-227)
|
||||
still answers `restart: false`.
|
||||
- **How a business gets there.** Brief item 7 lets the human add a credentialed definition. That add
|
||||
correctly answers `restart: true`. Its new `credentials` entry differs from `boot.roles` (`fixedRole(null)`),
|
||||
so every later reload of that business is refused until the bus restarts. A hand edit of a fixed key does
|
||||
the same.
|
||||
- **Failing scenario.**
|
||||
1. The human adds a credentialed agent and gets `restart: true`.
|
||||
2. Before restarting, the human runs `veto keep X`. The file gets X in `launch.keep`, the reload is
|
||||
refused, and the answer says `restart: false`.
|
||||
3. The broker's copy has no keep entry for X, so the chief's checks pass:
|
||||
- the keep check in `instance.fire` (broker.mjs:2570);
|
||||
- the `role.revoke` reason branch;
|
||||
- the B7 model check (broker.mjs:2478-2479).
|
||||
4. A chief fire of X runs step A: it revokes the claims, writes `lifecycle.end` with `kill: true`, and
|
||||
writes `fire.stopped` (broker.mjs:2595-2614).
|
||||
5. Step B's writer reads the file, so it refuses `instance-kept` (instance-settings.mjs:332), and
|
||||
`fire.done` is never written.
|
||||
6. X is left `instance-firing`. That refuses X's launch at the verb (:534) and at the take (:552). The
|
||||
veto the human set is what keeps the fire from finishing.
|
||||
- The same skipped reload leaves a hired instance unknown to the broker, and the hire still answers
|
||||
`restart: false`.
|
||||
- **What it breaks.**
|
||||
- DESIGN.md:428-430 (D3 item 3): "Any change that would need one of them still answers `restart: true` and doesn't
|
||||
reload."
|
||||
- DESIGN.md:408 (D2.4): "The broker enforces from its live copy of the business, which D3 keeps current."
|
||||
- The answer tells the human the veto is live when it isn't.
|
||||
- **Tests.** reload.test.mjs pins `restart: true` only for `instances.set` (:90-98, :157-163). No case
|
||||
covers a veto, hire or fire after a refused reload.
|
||||
- **Fix.**
|
||||
- Answer `restart: !live` for every committed verb, and print the restart line for veto, hire and fire in
|
||||
the CLI and the Console.
|
||||
- Add a reload test: a refused reload, then `veto.set`, which must answer `restart: true`.
|
||||
- Add a mutant that restores the `instances.set`-only condition.
|
||||
- Optional: have broker fire re-check keep against the file before step A. The writer already reads the
|
||||
file, so the check costs one read.
|
||||
|
||||
### B2. Fire ships without the SIGKILL escalation, and nothing records that
|
||||
|
||||
- **What happens.** Fire step A writes `lifecycle.end` with `kill: true` (broker.mjs:2611), and the
|
||||
launcher never reads it:
|
||||
- launcher.mjs:704 and :708 send every `lifecycle.end` to `terminate()`;
|
||||
- `terminate()` (:729-739) sends SIGTERM only.
|
||||
- So a fired runner that ignores SIGTERM keeps running with the model token.
|
||||
- No `lifecycle.end.killed` or `lifecycle.end.alive` is ever written, and the Console's "still running
|
||||
after fire" flag can't appear (BUILD.md:466-467).
|
||||
- T9a, T9b, T15 and M49-M55 are missing, and the halted checks (D1.1 item 2, D6) are missing too.
|
||||
- **What the design says.**
|
||||
- DESIGN.md D2.2: "After the grace, SIGKILL to any runner still alive ... and `lifecycle.end.killed`".
|
||||
- DESIGN.md D2.2: "Only fire escalates, because only a fired agent must lose the token at once. If row
|
||||
191 hasn't landed when this row builds, the build chains on its candidate (Q177)."
|
||||
- **What the build says.** BUILD.md:29 marks S6 "waits on row 191". Row 191 is in-progress in queue.json.
|
||||
b5 is offered to land in the 10-24 window without S6, and without a chain on 191.
|
||||
- **What I couldn't find.** A ruling that lets 194 land without S6. The DEFERRED.md diff adds only the
|
||||
creative-delete entry.
|
||||
- **Fix, either of:**
|
||||
- Build S6 on 191's candidate as the design says.
|
||||
- Get a Sage ruling that 194 lands without it, plus a DEFERRED entry. The entry names the gap (a fired
|
||||
runner can outlive SIGTERM with the token), the missing T9a, T9b, T15, M49-M55 and halted checks, the
|
||||
Console flag, and the row that binds them.
|
||||
- Since b4 lands before 191 either way, the ruling route is the cheaper one. It's Sage's call. I can't
|
||||
approve a fire that the approved design says must kill and that doesn't, with no record of the gap.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. A fire of a declared, delisted credentialed instance leaves its credentials live.
|
||||
- **Where.** instance-settings.mjs:335 returns `delisted: false` before `beforeWrite`, so `revoke` never
|
||||
runs.
|
||||
- **Scenario.**
|
||||
1. The human adds a credentialed coder-2, restarts, and issues its tokens.
|
||||
2. The human delists it with `launch: false`.
|
||||
3. The chief fires it. Fire answers done, and the tokens stay valid.
|
||||
4. The human can still launch an unlisted instance (V2).
|
||||
- **Design.** DESIGN.md D2.2 ("B finds I already delisted and counts as done") is about repeating a fire
|
||||
that partly failed. The brief's item 8 binds "before they change the business file", and this fire
|
||||
changes nothing, so this isn't a blocker as written.
|
||||
- **Fix.** Call `revoke` on that path when the entry has credentials. A revoke of an already revoked
|
||||
service should be a no-op. Add one test.
|
||||
|
||||
N2. A human add of a definition that declares the `host` service fails with the wrong message.
|
||||
- **Where.** `createdCredentials` (instance-settings.mjs:100, :127) maps every declared service, `host`
|
||||
included. `DATE_KEY` (credentials.mjs:12) has no `host`, so the entry gets a literal `"undefined"` key.
|
||||
Then `checkRoles` (business.mjs:140), which drops HOST_SERVICE, refuses with "must reference exactly the
|
||||
services its role definition needs".
|
||||
- **Effect.** It fails closed, and no shipped role declares `host`. The fix is to filter HOST_SERVICE
|
||||
first, or refuse the add by name.
|
||||
|
||||
N3. The B7 check (broker.mjs:2478-2479) looks only at `args.instances`. A chief `organization.set` that
|
||||
re-points the `models` entry a kept instance's `vars.model` names changes that instance's model at its
|
||||
next launch. DESIGN.md D2.4 says "that instance's model key". Re-pointing a shared key also changes every
|
||||
other instance on it, so I read the design as covering the instance's own key only. Say so in BUILD.md,
|
||||
or extend the check the way row 213 did for creative instances.
|
||||
|
||||
N4. A fire whose step B keeps refusing has no human escape. `#firing` (broker.mjs:537-543) clears only on a
|
||||
later `fire.done`, and only a chief fire writes one. Today only B1 makes this reachable. A human verb that
|
||||
clears it, or a take that checks keep against the file, would close it. Record it as a follow-up if B1's
|
||||
fix lands without one.
|
||||
|
||||
N5. Webui: `busStatus` (webui bus.mjs) maps none of the lifecycle refusal codes. A few examples are
|
||||
`instance-running`, `instance-kept`, `tester-off`, `tester-refused` and `unknown-request`, and all of them
|
||||
fall through to 502. The body is right. Separately, agents.js keeps polling an `unknown-request` for up to
|
||||
90 reads. Neither is harmful.
|
||||
|
||||
N6. CLI terminate (launcher.mjs:731-733) signals every registry run of a non-thread instance. That includes a
|
||||
run the chief launched after the terminate verb, whose claim was never revoked. DESIGN.md:118-120 (V3) names the
|
||||
verb's runs plus spawned-but-unbound ones. If this is meant, say so in the design's terms. The CLI text
|
||||
already hints at it.
|
||||
|
||||
N7. A delete that commits and then closes `outcome-unknown` (launcher.mjs:745-749) can't be retried. The
|
||||
broker allows a second delete of a retired name only after `workspace-rename-failed`, while DESIGN.md:173
|
||||
says a second delete "retries step 3 only" with no code limit. The window is narrow.
|
||||
|
||||
N8. DESIGN.md:625 still lists "SIGKILL escalation" under "Not in the row". That's r2 text that D2.2 r4
|
||||
replaced. It contradicts B2's design text. Don't edit the approved design. Note it in BUILD.md.
|
||||
|
||||
## Checked and fine
|
||||
|
||||
- **My design r4 notes.**
|
||||
- N1 is in: `#firing` (broker.mjs:537) refuses `instance-firing` at the launch verb (:534), at the take
|
||||
(:552) and in `role.launch` authority (:1770).
|
||||
- N2 is in: hire refuses unless roleVersion is 2 (broker.mjs:2647), and `checkHireable` refuses a version
|
||||
1 name (role.mjs:94).
|
||||
- **Creative hire.**
|
||||
- The writer refuses a CREATIVE_DEFINITIONS hire before `definitionFor` (instance-settings.mjs:304-308).
|
||||
- `checkHireable` refuses a creative name in a hireable list.
|
||||
- **endProtected.** The listing's `endProtected` and `deleteRefusal` call the same
|
||||
`endProtected` (instance-settings.mjs:32, :36, :170). agents.js:44 hides Delete on it.
|
||||
- **Reload.** The `libraries` key fixes each library entry whole, so a mapping change gets
|
||||
`restart: true` for `instances.set`.
|
||||
- **C1.** `testerNoSignIn` is gone from serve.mjs, webui cli.mjs and sso.test.mjs, with no reference left.
|
||||
- **Order and revision.** Fire runs A, B, C, and the consume happens only in C. `commit()` checks the
|
||||
revision before the revoke.
|
||||
- **Credential failures.** A failed revoke writes nothing, and the CLI text says the file is unchanged.
|
||||
- **Human tracker add.** It is required for vikunja, refused otherwise, and checked by `checkBot`, with
|
||||
bot uniqueness against the other instances and the sync bot.
|
||||
- **Human-only routes.** On the human listener, the webui routes need the signed-in Origin and Fetch Metadata
|
||||
checks. The tester listener refuses regenerate, veto and delete at the route, and the broker refuses
|
||||
them again.
|
||||
- **Escaping.** agents.js escapes every name and attribute it renders.
|
||||
|
||||
## Before the manifest round
|
||||
|
||||
The fixes for B1, and B2 or its ruling, are code and DEFERRED changes, and the gate reruns on them. I'll
|
||||
review the delta and then the manifest record. When I get a CLEAR, I'll run:
|
||||
- reload.test, lifecycle-verbs.test and chief-of-staff.test as whole files;
|
||||
- the B1 mutant;
|
||||
- a spot set of the D-table mutants, M2, M3, M22 and M27, on a scratch worktree.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Webby review: row 194 (#1692, agent lifecycle), build b6, static
|
||||
|
||||
Webby, 2026-10-21T23:52Z (from `date -u`). Author: Honker (honker-0083). Rulings in force: sage-2241,
|
||||
sage-2243, sage-2245.
|
||||
|
||||
- **Candidate.** honker/row194-b6 at af8d58c7d, one commit on b5 dfabbbe8a. 17 files, +280/-29.
|
||||
- **What I read.** The whole `git diff dfabbbe8a af8d58c7d`: source, tests, DEFERRED.md and BUILD.md "b6".
|
||||
I also read the code each fix leans on: the broker's organization gate and fire path, the runtime
|
||||
revoke, `Credentials.revoke` and `setRoutes`.
|
||||
- **What I didn't do.** No test or mutant has run. They wait for Sage's CLEAR. The row has no posted
|
||||
request for this round, so I record nothing.
|
||||
|
||||
## Verdict: CHANGES, B2 only (carried from b5)
|
||||
|
||||
B1 and N1-N3 are fixed in the code and have tests. N4 is deferred with a named test. B2 stays open
|
||||
until S6 is built on row 191's `stopRun`, per sage-2241 and sage-2245. Nothing new blocks.
|
||||
|
||||
### B1. Closed statically, runs pending
|
||||
|
||||
- **Restart.** runtime.mjs:33 and :290 answer `restart: !live` for `instances.set`, `veto.set`,
|
||||
`instance.hire` and `instance.fire`. Limits, routes and plugins writes keep their old answers, and
|
||||
reload.test pins that limits.set has no `restart` key.
|
||||
- **The fire check.** broker.mjs:2596 calls `write('instance.fireCheck')` before step A.
|
||||
- runtime.mjs:272 sends it to `checkFire` (instance-settings.mjs:351), which reads the file, runs
|
||||
`fireChecks` (:357) and returns before any write or reload.
|
||||
- `fire` runs the same `fireChecks` at step B.
|
||||
- `instance.fireCheck` appears only at broker.mjs:2596. A client that sends it gets no route to the
|
||||
writer.
|
||||
- **Printing.** The CLI veto prints RESTART_NEEDED or NO_RESTART. agents.js prints the matching
|
||||
receipt text. serve.mjs `/api/veto` needs a boolean `restart`, or it answers `outcome-unknown`.
|
||||
- **Tests.**
|
||||
- reload.test.mjs:150 runs my b5 sequence with the real writer. A credentialed add refuses the reload.
|
||||
Then hire, keep, unkeep and fire each answer `restart: true`. A fire of the name kept since refuses
|
||||
`instance-kept` with the file unchanged, the claim held and no `instance-firing`.
|
||||
- chief-of-staff T10 (new) has the writer refuse at fireCheck. The organization log holds only
|
||||
`instance.fireCheck`, the claim stays and `instance.launch` goes through.
|
||||
- The cli test and the Console tests cover the veto restart line and the shape check.
|
||||
- **What the CLEAR run must show.** Both of BUILD.md's mutants killed:
|
||||
- (a) the `instances.set`-only restart line. reload.test :70 and :150 should kill it.
|
||||
- (b) no `instance.fireCheck` call. reload.test :150 and chief-of-staff T10 should kill it.
|
||||
|
||||
### B2. Open
|
||||
|
||||
S6 isn't in b6. BUILD.md "b6" gives the `stopRun` contract (agents/rocko/work/queue-191/STOP-RUN-CONTRACT.md)
|
||||
and says 194 lands with or after row 191 in the 10-24 window. That matches sage-2241. I'll review S6
|
||||
when it's wired onto Rocko's commit. T9a, T9b, T15, M49-M55 and the halted checks come with it.
|
||||
|
||||
## The b5 notes
|
||||
|
||||
- **N1. Closed.** A fire of a delisted name now revokes with no write (instance-settings.mjs:334-339).
|
||||
- A repeat is safe. `Credentials.revoke` takes ENOENT (credentials.mjs:333-337), and the runtime
|
||||
revoke skips `credential-unknown` (runtime.mjs:151-155).
|
||||
- That path is also the repeat after a failed step C, which now revokes a second time and still goes
|
||||
on to `fire.done`.
|
||||
- The lifecycle-revoke.test case checks the receipt, the unchanged bytes, the empty store and the
|
||||
status.
|
||||
- **N2. Closed.** instance-settings.mjs:101 drops HOST_SERVICE. The new instance-settings test checks the
|
||||
references.
|
||||
- **N3. Closed for the chief.** `keptHumanOnly` runs in the limits writer on a re-point of a key a kept
|
||||
instance names and on its own `instances` key. The broker passes `instance-kept` for every verb. The
|
||||
"B7 kept instance" test (launch-settings.test.mjs:148) covers both paths for the chief, a key no kept
|
||||
instance names, and the human. The comparison `actor !== document.human` is the same one
|
||||
`creativeHumanOnly` already relies on.
|
||||
- **N4. Deferred.** The DEFERRED.md entry names the gap, a human-only verb and its test.
|
||||
- **N5, N7.** Follow-ups, as BUILD.md says.
|
||||
- **N6.** With Sage, tied to S6.
|
||||
- **N8.** Noted in BUILD.md, and the design isn't edited.
|
||||
|
||||
## New notes, non-blocking
|
||||
|
||||
N1. The tester half of N3 doesn't match what the tester can reach.
|
||||
- `limits.set` isn't in TESTER_ORGANIZATION_VERBS (broker.mjs:130). So the tester rows in the "B7 kept
|
||||
instance" test exercise the writer, and the bus never gets there.
|
||||
- `routes.set` is a tester verb. `setRoutes` (launch-settings.mjs:291-293) runs `creativeHumanOnly`
|
||||
and not `keptHumanOnly`. So the tester can change a kept instance's model through a route.
|
||||
- I didn't check `instances.set` change with `vars.model` for the tester.
|
||||
- D2.4 binds the chief, so this isn't a blocker. Either keep binds the tester too, and `setRoutes` gets
|
||||
the same check and a test, or it doesn't, and BUILD.md drops "the tester included". Sage's call.
|
||||
|
||||
N2. fireCheck reads the file without the writer's lock, so a keep written between the check and step B
|
||||
still strands the instance. The new DEFERRED entry names that case. I'm noting it only so the CLEAR
|
||||
run doesn't treat it as new.
|
||||
|
||||
## On CLEAR
|
||||
|
||||
On a scratch worktree at af8d58c7d, with TMPDIR under /mnt/storage/scratch/tmp:
|
||||
- whole files: reload, lifecycle-verbs, lifecycle-revoke and chief-of-staff (bus); instance-settings
|
||||
and launch-settings (business); cli instances; webui agents and agents-browser;
|
||||
- mutants: B1 (a) and (b) above, and M2, M3, M22 and M27 from the D table;
|
||||
- a blob check after each restore.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Webby review: row 194 (#1692, agent lifecycle), build b7, static
|
||||
|
||||
Webby, 2026-10-22T00:01Z (from `date -u`). Author: Honker (honker-0085). Rulings in force: sage-2241,
|
||||
sage-2245, sage-2246, sage-2248, sage-2249.
|
||||
|
||||
- **Candidate.** honker/row194-b7 at 0f0e2c7ef, nine commits on dcd8d4dda. dcd8d4dda is origin/refactor
|
||||
after row 254 landed.
|
||||
- **What I didn't do.** Nothing has run yet. Runs wait for Sage's CLEAR, after Rocko's row 191 slot. No
|
||||
request is posted for this round, so I record nothing.
|
||||
|
||||
## Verdict: CHANGES, B2 only (carried)
|
||||
|
||||
The rebase is clean, and the routes.set fix closes my b6 N1. B2 (S6) is still open, as ruled in sage-2241.
|
||||
|
||||
### The rebase
|
||||
|
||||
- **Commits.** `git range-diff da9555ca2..af8d58c7d dcd8d4dda..b0aed7527` gives `=` for six of the seven
|
||||
commits. The seventh, commit 2 (30f05f3d2), differs only in the channel-proof pin for webui/src/bus.mjs
|
||||
and in the STATUS context lines next to row 254's entries.
|
||||
- **channel-proof.** At 0f0e2c7ef, all four baseline pins match the sha256 of their files: human.mjs,
|
||||
human-cli.mjs, cli transport.mjs and webui bus.mjs (15e8123f). The diff against the base swaps row 254's
|
||||
pin for the merged file's and adds row 194's comment. Row 254 had no comment line of its own to lose.
|
||||
- **webui/src/bus.mjs.** Against dcd8d4dda, the diff is the two row 194 tracker entries. STATUS has no
|
||||
duplicate key.
|
||||
- **DEFERRED.md.** Against dcd8d4dda, the diff adds row 194's two entries, creative delete and the stuck
|
||||
fire (N4). It removes nothing.
|
||||
- **Drift.** da9555ca2..dcd8d4dda reaches the manifest on only those three paths: DEFERRED.md,
|
||||
channel-proof.test.mjs and webui bus.mjs. The b7 manifest is the same 53 paths as b6, with
|
||||
PRD-1.8-DRAFT left out.
|
||||
|
||||
### b6 N1, closed statically
|
||||
|
||||
6a742d371 calls `keptHumanOnly` in `setRoutes` right after `creativeHumanOnly` (launch-settings.mjs:294-295).
|
||||
The test "B7 kept instance: its route is the human's" covers three cases:
|
||||
- the chief is refused with `instance-kept` and the bytes unchanged;
|
||||
- the tester gets the same, which is now reachable on the bus because routes.set is a tester verb;
|
||||
- the human re-routes the instance.
|
||||
|
||||
M-next, which drops the call, has to be killed on the CLEAR run.
|
||||
|
||||
### The behaviour change (sage-2246)
|
||||
|
||||
An agent can no longer re-point a `launch.models` key that a kept instance names, and a shared key
|
||||
counts. I agree. Re-pointing a shared key changes the kept instance's model, which is what D2.4 B7
|
||||
protects. The chief still adds keys and changes the ones no kept instance names, and b6's test covers
|
||||
that.
|
||||
|
||||
### The mutant (a) correction
|
||||
|
||||
Right. The CLI test and the Console tests stub the bus, so only reload.test can kill (a).
|
||||
|
||||
## On CLEAR
|
||||
|
||||
Honker's runb7.sh list covers the whole files and mutants (a), (b) and M-next. I'll run on my own scratch
|
||||
worktree at 0f0e2c7ef, with TMPDIR under /mnt/storage/scratch/tmp:
|
||||
- **Row 194's files, each alone:** reload, chief-of-staff, lifecycle-verbs, lifecycle-revoke and
|
||||
channel-proof (bus); instance-settings and launch-settings (business); cli instances; webui agents and
|
||||
agents-browser.
|
||||
- **Rebase files:** webui library-status.test and bus library-sync.test. Row 254's tests ran against
|
||||
its own bus.mjs, and this merge changes that file.
|
||||
- **Mutants:** (a), (b), M-next, and M2, M3, M22 and M27, with a blob check after each restore.
|
||||
|
||||
B2 stays open until the S6 commit, with N6 narrowed as sage-2246 rules. I'll review it when it arrives.
|
||||
@@ -0,0 +1,265 @@
|
||||
# Webby review: row 194 (#1692, agent lifecycle), build b8 r2, static
|
||||
|
||||
Webby, 2026-10-22T00:41Z (from `date -u`). Author: Honker (honker-0092, honker-0094). Rulings in force: sage-2241,
|
||||
sage-2246, sage-2260, sage-2261, sage-2289.
|
||||
|
||||
- **Candidate.** origin/honker/row194-b8 at 899e30a80, on row 191's tip eb3abba12
|
||||
(origin/rocko/row191-stoprun-dcd8-20261022, four commits on dcd8d4dda). b8 r2 is b8 (0583569b7) plus
|
||||
one commit, and 0583569b7 is its parent.
|
||||
- **Manifest.** `git diff --name-only eb3abba12 899e30a80` gives 54 paths. Leaving out
|
||||
agents/honker/work/queue-194/PRD-1.8-DRAFT.md gives 53. None is under packages/processing-control, and
|
||||
none is a row 191-only path.
|
||||
- **What I read.**
|
||||
- The b7 to b8 range-diff, `git range-diff dcd8d4dda..0f0e2c7ef eb3abba12..0583569b7`, and every
|
||||
interdiff of the three `!` commits.
|
||||
- The whole S6 commit b3341c1cb and the r2 commit 899e30a80.
|
||||
- BUILD.md "b8 notes" and "b8 r2: sage-2289".
|
||||
- Row 191's `stopRun`, `ownStop` and `sweepRuntimes` (processing-control/src/runtimes.mjs at
|
||||
eb3abba12).
|
||||
- DESIGN.md V3 (:112-124), D2.2 (:276-312), D6 (:485-496), D7 (:498-503) and the T9a, T9b, T15 and
|
||||
M49-M55 rows.
|
||||
- **What I didn't do.** No test or mutant has run. They wait for Sage's CLEAR. The row has no posted
|
||||
request for this round, so I record nothing.
|
||||
|
||||
## Verdict: no blockers. B2 is closed statically, and APPROVE waits on the runs
|
||||
|
||||
B2 (S6) is built on row 191's `stopRun` as D2.2 asks. Terminate stays SIGTERM-only, and the halted
|
||||
checks match D6. The unknown outcome follows sage-2289. I found nothing new that blocks. The round's
|
||||
APPROVE depends on my CLEAR run below plus the full gate with Docker and P3 (sage-2261).
|
||||
|
||||
## The rebase onto 191
|
||||
|
||||
- Six commits are `=`. Commits 1, b4 and b6 are `!`, and in each one I checked, both sides are kept with
|
||||
no change to 194's behaviour. BUILD.md lists these file by file, and the interdiffs match:
|
||||
- the broker constructor ends `processing = null, testerOn = null, roleFile = null`;
|
||||
- runtime.mjs keeps 191's `broker = new Broker(...)`, `broker.reconcileProcessing(); processing.start();`
|
||||
and its imports, with `INSTANCE_VERBS` after them;
|
||||
- launcher `start()` takes `human = false, admissionGeneration`, and 191's `admission(...)` calls stay;
|
||||
- webui cli and serve pass both `agentWrite` and `processing`, and C1's `testerNoSignIn` removal sits
|
||||
beside 191's tester `processing` read;
|
||||
- the browser.test script pin is `['/editor.js','/processing.js','/brand.js','/app.js',
|
||||
'/markdown-preview.js','/chat.js','/agents.js','/bus.js']`.
|
||||
- 191 also changed 10 of the 194 paths. That's why these resolutions get a full run and not a carried
|
||||
verdict.
|
||||
|
||||
## B2 (S6) against the design
|
||||
|
||||
- **Fire goes through 191's stop.** `act` (launcher.mjs:763-770) sends a `kill: true` end to `fireStop`
|
||||
(:819). That calls `stopRun` with reason `fire` and owner `{ decision: <request> }` (:825), which is
|
||||
the shape `stopRun` accepts. `stopRun` checks identity before each signal, sends SIGKILL after the
|
||||
grace and observes. D2.2 items 3a-3c are 191's code, not a second copy.
|
||||
- **Terminate is SIGTERM only (V3).** `terminate` (:804) sends SIGTERM and nothing else. The
|
||||
T9a terminate test (lifecycle.test.mjs:670) pins one SIGTERM and no fire-stop record. M54 targets it.
|
||||
- **Outcomes.** `lifecycleDone` (broker.mjs:2322) accepts `stops` only on a `kill: true` end (:2343). It
|
||||
writes `lifecycle.end.killed` or `.alive` against the instance with run and pid. The pid check (`null`
|
||||
or a safe integer above 1) matches 191's identity check (`pid < 2` fails). The test at
|
||||
chief-of-staff.test.mjs:899 covers the refusals.
|
||||
- **Unknown, r2.** `fireStop` (launcher.mjs:835) maps `unknown` to alive with reason `unknown`, and the
|
||||
broker accepts that reason only on an alive stop (broker.mjs:2335). That closes more than the
|
||||
no-record case. `ownStop` returns a stored result before it signals (runtimes.mjs:267). So a run whose
|
||||
earlier halt sweep ended `unknown`, if it survived to a later fire, gets no signal from the fire, and
|
||||
r2 now flags it. Before r2 it would have vanished from the fire's report. The same holds for "another
|
||||
host owns delivery" and for a `stopRun` that throws (launcher.mjs:828). The new T9a test (:597) and
|
||||
M56 cover it.
|
||||
- **Console (D7).** `#aliveAfterFire` (broker.mjs:2351) lists alive runs with no `session.ended`, and the
|
||||
instances read adds them as `alive` (:2633). agents.js escapes each run with `txt`. The
|
||||
agents-browser test puts a hostile run name through it.
|
||||
- **Halted (D6).**
|
||||
- `#launchable` (broker.mjs:617) is used only by `instance.launch` and `instance.regenerate`. So
|
||||
`instance.end` keeps no halted check, and terminate works while halted (V3 item 5).
|
||||
- The take re-checks for every action except end and delete (:636-647).
|
||||
- Hire and fire refuse through 191's work gate, since neither is on its read list.
|
||||
- The tick (launcher.mjs:583) skips the thread and land passes while halted and still runs the
|
||||
lifecycle pass.
|
||||
- T15 covers the bus (lifecycle-verbs.test.mjs:150) and the cli (lifecycle.test.mjs:718).
|
||||
- **Halt joining a fire (T9b).** The sweep's `stopRun` gets the fire's in-flight promise from 191's
|
||||
`stopping` map. So it sends no second SIGTERM and doesn't restart the deadline, and the receipt keeps
|
||||
reason `fire` and the decision. lifecycle.test.mjs:686 checks one SIGTERM, the deadline, the single
|
||||
receipt and the counted run. M55 and M55-deadline target it.
|
||||
- **N6 (sage-2246).** `ending` (launcher.mjs:795) takes the verb's runs plus non-thread registry runs
|
||||
spawned at or before the end's `at`, using `spawnedAt` (:500) or the registry `startedAt`. That
|
||||
matches DESIGN.md:118-120. lifecycle.test.mjs:751 checks that a later run survives and the next end
|
||||
takes it. Fire uses the same selection. The only spawn that can follow a fire's verb is a human
|
||||
launch already taken, and D2.2 (:300-302) leaves that one to the human's terminate. So the narrowing
|
||||
doesn't cut into F4.
|
||||
- **Regenerate** now signals every registry run of the instance (launcher.mjs:772), with no N6 cut.
|
||||
It waits for an empty registry before starting, and a thread instance refuses regenerate at the verb.
|
||||
That's b7's behaviour, and it's right.
|
||||
- **Tests and mutants present.** T9a has four cli tests (:562, :597, :619, :670) and one bus test. T9b,
|
||||
T15 (bus and cli), N6 and the agents-browser flag are all there. BUILD.md lists M49-M56,
|
||||
M55-deadline, N6-widen, D7 and S6-done with named kill tests. The DESIGN.md test table names
|
||||
`launcher.test.mjs`, and the tests are in `lifecycle.test.mjs`. The b8 notes record that deviation,
|
||||
and I agree with it.
|
||||
|
||||
## Notes, non-blocking
|
||||
|
||||
N1. The clock caveat is narrower than BUILD.md says. The bus's `#now` (broker.mjs:524-526) is monotonic:
|
||||
`max(Date.now(), clock + 1)`. So after a clock step back, the end's `at` stays later than the launcher's
|
||||
spawn times, and the pass signals more runs, which was b7's breadth. It doesn't signal fewer. A run can
|
||||
be missed only when the clock steps back between its spawn and the verb, with no bus event in between to
|
||||
carry the clock forward. That's fine as it stands. BUILD.md could say which direction the error goes.
|
||||
|
||||
N2. `stops` is capped at 64 (broker.mjs:2330). A fire of an instance with more runs than that gets
|
||||
`invalid-request` from `lifecycleDone`. The request stays taken, the CLI reads outcome-unknown, and no
|
||||
killed or alive event is written, so the Console flags nothing. No business is near 64 runs of one
|
||||
instance today. Either raise the cap to the registry's real limit, or add one line to DEFERRED.
|
||||
|
||||
N3. The CLI `instances` output (instances.mjs:201) prints `running` and not `alive`. D7 binds only the
|
||||
Console, so a human on the CLI never sees "still running after fire". A follow-up row, not this one.
|
||||
|
||||
N4. An alive flag clears only on the run's `session.ended`. A run this host never spawned (the
|
||||
no-record case) has no exit handler here, so its flag stays until something writes `session.ended`. A
|
||||
false flag is the safe side per sage-2289. I'm noting it so nobody reads a lingering flag as a bug in
|
||||
the round.
|
||||
|
||||
N5. With r2, the Console says "still running after fire" for a stop that couldn't be confirmed, and the
|
||||
run may not be running. If a short change is cheap, "not confirmed stopped after fire" for
|
||||
`reason: unknown` would be more accurate. Cosmetic.
|
||||
|
||||
## On CLEAR
|
||||
|
||||
On my own scratch worktree at 899e30a80, with TMPDIR from `mktemp -d /mnt/storage/scratch/tmp/...`:
|
||||
- **Whole files, one at a time.**
|
||||
- bus: reload, chief-of-staff, lifecycle-verbs, lifecycle-revoke and channel-proof.
|
||||
- business: instance-settings and launch-settings.
|
||||
- cli: instances, lifecycle, launcher, launch-limit-checks, launch-limits, launch-routes,
|
||||
launch-account and thread-launch.
|
||||
- webui: agents, agents-browser and browser.
|
||||
- library: status and sync.
|
||||
- **Skips.** lifecycle.test skips when `sandboxProblem()` reports one (:33). I'll grep `^skip` and
|
||||
`# SKIP` and name every skip, because a skipped T9a isn't a pass.
|
||||
- **Mutants, with a blob check after each restore.**
|
||||
- B1 (a) and (b), and M-next.
|
||||
- From the D table: M2, M3, M16, M22 and M27.
|
||||
- S6: M49-M56 and M55-deadline (M52, M55 and M55-deadline edit 191's runtimes.mjs), plus N6-widen,
|
||||
D7 and S6-done.
|
||||
- The anchors are mine. I don't use Honker's runb8.sh.
|
||||
|
||||
## Final candidate 062375507 (honker-0098, sage-2290, sage-2291)
|
||||
|
||||
Checked at 2026-10-22T00:46Z (from `date -u`).
|
||||
- origin/honker/row194-b8 is 062375507, two commits on 899e30a80: 95c0e1220 (the N1-N5 notes) and 062375507
|
||||
(the N4 correction). Both are doc-only.
|
||||
- `git diff 899e30a80 062375507 -- packages` is empty. The changed paths are BUILD.md and
|
||||
docs/plans/DEFERRED.md. With the PRD draft left out, the manifest is still 53 paths.
|
||||
- **The DEFERRED line names what sage-2291 asked for.** The cap is 64 stops in `lifecycleDone`, and
|
||||
the refusal is `invalid-request`. The follow-up test is a done over the cap that refuses, while the
|
||||
runs it left alive still get the flag. Its consequence is right too. fireStop signals before the done,
|
||||
and `fire.done` is written in the verb, so only the events and the flag are lost.
|
||||
- **N1 as corrected in BUILD.md is still too strong.** "A run spawned before the verb can't fall
|
||||
outside it" holds only if some bus event moved `#clock` past the spawn. Nothing on the bus is written
|
||||
between a spawn and its bind, so a clock step back in that gap can still put the spawn after `at`.
|
||||
That's the narrow case from my N1. It's doc-only, so it doesn't block (decision 137), and it needs no
|
||||
new commit.
|
||||
- The static review carries to 062375507. All runs use 062375507.
|
||||
|
||||
## Rebased candidate 6411a31c0, origin/honker/row194-b9 (sage-2295, sage-2296, honker-0100)
|
||||
|
||||
Checked at 2026-10-22T01:08Z (from `date -u`).
|
||||
- 6411a31c0 is 062375507's 14 commits on Rocko's e6800380d, which is one test-only commit on eb3abba12
|
||||
(packages/discord/tests binding.test.mjs and recover.test.mjs, +2 each).
|
||||
- `git diff 062375507 6411a31c0 -- packages` is byte-for-byte `git diff eb3abba12 e6800380d`. Nothing
|
||||
outside packages changed.
|
||||
- `git range-diff eb3abba12..062375507 e6800380d..6411a31c0`: all 14 commits are `=`.
|
||||
- Against e6800380d the manifest is 53 paths without the PRD draft. None is under processing-control or
|
||||
packages/discord.
|
||||
- The static review carries to 6411a31c0. All runs use 6411a31c0.
|
||||
|
||||
## Rebased candidate b52b17514, origin/honker/row194-b10 (sage-2318, honker-0103)
|
||||
|
||||
Checked at 2026-10-22T01:59Z (from `date -u`).
|
||||
- b52b17514 is 6411a31c0's 14 commits on Rocko's 8e1f55c1d. 8e1f55c1d's parent is e6800380d, and it is a
|
||||
product fix, not a test-only one: webui processing.css and processing.js, plus processing-browser.test.mjs
|
||||
and processing-ui.test.mjs.
|
||||
- `git diff 6411a31c0 b52b17514 -- packages` is byte-for-byte `git diff e6800380d 8e1f55c1d`. Nothing
|
||||
outside packages changed.
|
||||
- `git range-diff e6800380d..6411a31c0 8e1f55c1d..b52b17514`: all 14 commits are `=`.
|
||||
- Against 8e1f55c1d the manifest is 53 paths without the PRD draft. None is under processing-control, and
|
||||
194 touches neither processing.js nor processing.css nor Rocko's two tests.
|
||||
- **Where the two meet.** 194 changes webui src/public/index.html (it adds `/agents.js` to the script
|
||||
line), and Rocko's processing-ui.test.mjs:66 reads index.html for the processing controls. Its asserts
|
||||
are on the processing nodes and the processing.js and processing.css tags, which 194 leaves alone, so I
|
||||
expect a pass. shell.test.mjs:40 also reads index.html with app.js and bus.js, and it wasn't on my list.
|
||||
- **Run list change.** I add webui processing-ui, processing-browser and shell to the whole files, which
|
||||
makes 23. The rest of "On CLEAR" stands.
|
||||
- The static review carries to b52b17514. All runs use b52b17514.
|
||||
|
||||
## Merge-scoped carry to b11 469f441e5, origin/honker/row194-b11 (honker-0105, sage-2330, mos-1160)
|
||||
|
||||
Checked at 2026-10-22T02:08Z (from `date -u`).
|
||||
- **Base.** eafcbd3e7 is Rocko's six 191 commits restacked on row 202's landing ddfb29af7. It is not a
|
||||
descendant of 8e1f55c1d. `git range-diff dcd8d4dda..8e1f55c1d ddfb29af7..eafcbd3e7` gives five `=` and one
|
||||
`!`. The `!` is commit 1 (9a7251403), and its only change is the scripts/mosaic dispatch line, which now
|
||||
carries 202's `access`. `git diff 8e1f55c1d eafcbd3e7 -- packages/processing-control` is empty, so the
|
||||
`stopRun` that S6 relies on hasn't changed.
|
||||
- **The 14 commits.** `git range-diff 8e1f55c1d..b52b17514 eafcbd3e7..469f441e5`: all 14 are `=`.
|
||||
- **b10 to b11.** `git diff b52b17514 469f441e5` touches the same 15 files as `git diff 8e1f55c1d eafcbd3e7`.
|
||||
The added and removed lines match file by file, and the only differences are hunk offsets.
|
||||
- **Tree.** `git merge-tree --write-tree b52b17514 ddfb29af7` (merge base dcd8d4dda) gives 510b79875. That
|
||||
tree differs from b11 only in scripts/mosaic. It has one conflict there, and b11 resolves it to
|
||||
`halt|resume|processing|...|thread|access`, which keeps both sides. 194's `instances|instance|veto` line
|
||||
(:32) is unchanged.
|
||||
- **Overlap with 202.** Only two paths are in both 202 and the 194 manifest: packages/cli/src/cli.mjs and
|
||||
scripts/mosaic. In b11's cli.mjs, 202's import is at :23 and its `if (verb === "access")` block is at :619.
|
||||
- **Manifest.** Against eafcbd3e7 it's still 53 paths without the PRD draft. None is under
|
||||
processing-control or access.
|
||||
- **Run list change.** 202's only test is packages/cli/tests/access.test.mjs. It reads cli.mjs and
|
||||
runtime.mjs, and 194 changes both, so I'm adding it. That makes 24 whole files. The mutant list stands.
|
||||
- The static review carries to 469f441e5. All runs use 469f441e5.
|
||||
|
||||
## Rebased candidate c13134eb1, origin/honker/row194-b12 (honker-0107)
|
||||
|
||||
Checked at 2026-10-22T02:33Z (from `date -u`).
|
||||
- 0f8235277 is two commits on eafcbd3e7: 4db8b5778, a one-line change in
|
||||
packages/webui/tests/processing-browser.test.mjs, and Rocko's packet BUILD.md.
|
||||
- `git diff 469f441e5 c13134eb1` is byte-for-byte `git diff eafcbd3e7 0f8235277`.
|
||||
- `git range-diff eafcbd3e7..469f441e5 0f8235277..c13134eb1`: all 14 commits are `=`.
|
||||
- Against 0f8235277 the manifest is 53 paths without the PRD draft. Neither of Rocko's two paths is in it.
|
||||
- The static review carries to c13134eb1. The run list (24 whole files) and the mutants stay as they are,
|
||||
and all runs use c13134eb1.
|
||||
|
||||
## b13 7609210a3 carry check (honker-0110, sage-2458)
|
||||
|
||||
Git reads only. No tests have run yet.
|
||||
|
||||
- The 14 commits sit on refactor 0fa7bf421, the landed 191. The base of b12 c13134eb1 was 0f8235277.
|
||||
- range-diff 0f8235277..c13134eb1 against 0fa7bf421..7609210a3 shows 14 of 14 `=`.
|
||||
- The series patch-id --stable is 086085a8dfe41a47cdaa0b12effa8d4542783637 on both sides.
|
||||
- The path list is identical, 54 paths.
|
||||
- The drift 0f8235277..0fa7bf421 covers 14 files. The only one 194 also touches is packages/webui/src/public/index.html. 191 changes lines 14 and 20-22, and 194 changes line 7 (adds agents.js to the script line), so the hunks are separate.
|
||||
- Tests that read index.html: agents.test (in 194, row 194 N5 at :214), processing-ui.test:66 and shell.test:40. None of them pins the script list. processing-ui checks the controls' placement and attributes, and shell checks the joined source. stack-release.sh drift is read only by the cli stack-* tests, and 194 touches none of those.
|
||||
- My run list stays at 24 files. processing-ui, processing-browser and shell are already in it since b10. Runs go on 7609210a3 at Sage's CLEAR, after Honker's runb8.
|
||||
|
||||
## b14 b565e589b carry check (honker-0113, sage-2493, sage-2498)
|
||||
|
||||
b14 is a carry of b13 with no blockers. These are git reads only. I did no runs, because the slot is Honker's.
|
||||
- 14 commits on refactor 23c3d6218.
|
||||
- range-diff 0fa7bf421..7609210a3 against 23c3d6218..b565e589b shows 14 of 14 `=`.
|
||||
- The series patch-id is 086085a8dfe41a47cdaa0b12effa8d4542783637 on both sides.
|
||||
- The path list is identical.
|
||||
- The drift 0fa7bf421..23c3d6218 (205 and 268, 24 files) meets 194 at two paths, broker.mjs and business.test.mjs. For both files, the +/- lines of 194's own diff are identical on the old and new bases. So 194's hunks are unchanged, and git applied them without touching 205's or 268's lines.
|
||||
- Whether the merged files run together is a matter for Honker's runs on b14, which Sage is gating.
|
||||
|
||||
## b15 ce7fd1b88 delta review (honker-0119, sage-2501/2503/2507)
|
||||
|
||||
Verdict: APPROVE, no blockers. These are git reads only, and nothing has run on b15.
|
||||
- b15 is b14 b565e589b plus two commits.
|
||||
- range-diff: commits 1-14 show `=`, plus new commits 15 (b73452287) and 16 (ce7fd1b88).
|
||||
- The series patch-id is 64303253f8e6. The path list is the same as b14's.
|
||||
- Neither new commit has an attribution line.
|
||||
|
||||
(a) b73452287, broker.mjs +4/-3:
|
||||
- The three function-argument checks (harnessOn, testerOn, roleFile) move to just after `invalid-business`, ahead of `this.#store` and every store read. Each check's text is unchanged. One new comment line goes above them, and the descriptive comments stay with their assignments.
|
||||
- Behaviour changes only when a bad function argument and a store failure occur together. The argument error now wins, and that is what T8b expects (chief-of-staff.test:868, `store: null`).
|
||||
- The only other pin on these messages is routes-verb.test:89, which uses a real store and is unaffected.
|
||||
|
||||
(b') ce7fd1b88, test-only:
|
||||
- T15 removes the exited reviewer stand-in's registry entry after `x.exited === 143`.
|
||||
- The refusal it avoids comes from business/src/launch.mjs:123, which refuses when a running session has no exact provider/model. The stand-in's entry is a fixture artifact, because the launcher's exit handler removes only the runs it started.
|
||||
- This is the same convention as N6 at lifecycle.test:763-764. standIn is unchanged.
|
||||
- I accept Sage's ruling of (b') over (b). Giving the stand-in entry a model would only move the refusal to the `{ sonnet: 1 }` limit.
|
||||
- The second remove in standIn's t.after is a no-op.
|
||||
|
||||
N1, non-blocking: no test pins the `testerOn must be a function` message. This is old, not new in b15.
|
||||
|
||||
Manifest digest 4aaa3316 (honker-0119, sage-2509): its 53 paths equal git's path list 23c3d6218..ce7fd1b88 minus the PRD draft, and every sha256 in it matches the blob at ce7fd1b88.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Webby review: row 194 design r2 (#1692, agent lifecycle and per-agent veto)
|
||||
|
||||
Webby, 2026-10-20T19:31Z (from `date -u`). Independent reviewer. Author: Honker.
|
||||
Subjects at c0a35f47a on honker/row194, both sha256 verified: `agents/honker/work/queue-194/DESIGN.md`
|
||||
(313c206e...fc25) and `PRD-1.7-DRAFT.md` (98f4f49e...9708). I read the whole of both and the diff
|
||||
from 14a6f5f9d. Code read at 53bb59462. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
One blocker. B3-B8, Q2 and N1-N8 from webby-0003 are fixed as the closing table says. I checked
|
||||
each against the text. B1 is fixed: hard waits, no grant, and a PRD 1.7 draft for the routed form.
|
||||
B2's floor is the gap.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. The hire floor is a deny list, and it misses actions a hireable definition can hold today.
|
||||
D2.3 refuses seven actions. role.mjs:40-43 already refuses the hard waits in both lists, so what is
|
||||
left is the routed set (vocabulary.mjs ROUTED). The floor doesn't name these:
|
||||
- `git.merge.protected`. `roles/merge-gate.json` holds it in crossRole and declares no credentials,
|
||||
so it passes D2.2 item 3 and the floor. Listed in `hireable`, the chief could hire its own merge
|
||||
gate. That is the same author-is-reviewer problem `review.verdict` is on the list for.
|
||||
- `decision.resolve.technical`. A hired holder resolves technical decisions, so the chief would pick
|
||||
who answers its own requests. `cto.json` is credentialed today, but a credential-free role
|
||||
holding it would pass.
|
||||
- `policy.change` and `prd.approve`. Both are routed. A credential-free role holding either passes.
|
||||
- `instance.name`. It was in my r1 B2 list, which Mos accepted (mos-1018), and it is missing here.
|
||||
Every action added to the vocabulary later is also hireable by default, which is the wrong
|
||||
direction for a fail-closed floor (AGENTS.md invariant 6).
|
||||
Fix: turn `HIRE_FLOOR` into an allowlist, `HIRE_ALLOWED`. A definition is hireable only when every
|
||||
action in withinRole and crossRole is on it. From the role files at 53bb59462, the list a working
|
||||
hire needs is `message.send`, `record.write`, `review.request`, `task.update.assigned`,
|
||||
`task.scope.change` and `git.push.working`. The last two only matter after row 199 (credentials).
|
||||
T8b and T8c then loop over `ACTIONS` minus the allowlist, one case each, so a new action is covered
|
||||
without a test edit. M30-M43 become "one action added to the allowlist in the broker" and "in
|
||||
role.mjs". Update the PRD draft's bound to match: "The chief hires only role definitions whose
|
||||
actions are all on a fixed list of working actions. Adding to that list is a PRD change."
|
||||
|
||||
## Q4 (for Sage, my view)
|
||||
|
||||
Start `hireable` empty. Under the hard-wait form Jason resolves each hire anyway. With the B1
|
||||
allowlist, today's credential-free definitions that pass are assistant and the five chief-officer
|
||||
roles (message.send and record.write only). assistant is END_PROTECTED. A first kind is a policy
|
||||
call and belongs in its own reviewed commit.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. D2.1 step 3 compares the call's arguments to "the decision body". Name the field-by-field
|
||||
comparison (business from the cap, name, definition, canonical vars). A body with extra keys should
|
||||
refuse, not be ignored.
|
||||
N2. The PRD draft's notes say "Replace" covers the model write. The REQ-COS-3 text itself says
|
||||
only "fire, replace or end". Put the model write in the requirement text, so a later reader of the
|
||||
PRD alone sees it.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Webby review: row 194 design r3 (#1692)
|
||||
|
||||
Webby, 2026-10-20T19:32Z (from `date -u`). Author: Honker. Subjects at d0afda024 on honker/row194:
|
||||
DESIGN.md sha256 885fce0d...65fc, PRD-1.7-DRAFT.md sha256 04c32917...d728 (verified). Read as the
|
||||
diff from c0a35f47a, which my r2 review (REVIEW-r2.md, webby-0010) covered. r3 crossed with webby-0010
|
||||
and doesn't answer it yet. Static review.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
r2 B1 stands in part. r3 adds `instance.name` to the floor. `git.merge.protected` (merge-gate.json,
|
||||
crossRole, no credentials), `decision.resolve.technical`, `policy.change` and `prd.approve` still
|
||||
pass it, and so will any action added later. The fix is unchanged: an allowlist, as in REVIEW-r2.md.
|
||||
M47 and M48 fold into the allowlist mutants.
|
||||
|
||||
## New blocker
|
||||
|
||||
B2. Fire can't be "in the same transaction" as the delist. The delist is a business file commit
|
||||
through the organization writer (organization.mjs write: lock, rename, receipt). The claim revoke
|
||||
and the `lifecycle.cancelled` and `lifecycle.end` events are store rows. No transaction spans the
|
||||
two. Fix: name the order and the failure between them. I'd write the store side first (revoke,
|
||||
cancel, end), then commit the delist. If the delist fails, the runs are ended and I is still
|
||||
listed, so the chief can fire again, and that second fire must work with no claims left. The other
|
||||
order leaves a delisted agent with a live process and the model token, which is the F4 case. Test
|
||||
a writer failure after the store commit, and add a mutant that swaps the order.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. REQ-LAND-2 says "Gitea itself then refuses a push to upstream or to another agent's fork". That
|
||||
holds only if the agent's Gitea user has no write access to those repos. Gitea allows a push from any
|
||||
collaborator. The bound should be the host's check that the remote's owner is the agent's own user,
|
||||
made at each push. Gitea's refusal is the second layer. Megavolt is checking this text against row
|
||||
199, so I'm sending it there as a note, not a blocker here.
|
||||
N2. F4's 10 s recheck reads a pid. Use the same start-time check as the signal (launcher.mjs:526),
|
||||
so a reused pid isn't reported as the fired run.
|
||||
|
||||
The rest of r3 reads right: F4's reason (ENV_ALLOW, session.mjs:83-94), T9a, M49-M51, R7 and the Q4
|
||||
ruling.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 194 design r4 (#1692)
|
||||
|
||||
Webby, 2026-10-20T19:40Z (from `date -u`). Author: Honker. Subjects at 7d0788236 on honker/row194:
|
||||
DESIGN.md sha256 824ef884...3f34, PRD-1.7-DRAFT.md sha256 379cc318...05bc (both verified). Read as the diff
|
||||
from d0afda024 (my r3 review), with the role files at origin/refactor. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 (webby-0010 and webby-0011) is fixed. D2.3 is now an allowlist, `HIRE_ALLOWED`, with six actions. T8b and
|
||||
T8c loop over `ACTIONS` minus the list and the hard waits. There is a mixed case and a pin test, and M47/M48
|
||||
are "some instead of every". The table of today's files checks out against the role files. chief-of-staff,
|
||||
cto, merge-gate, pm, reviewer and security-reviewer each hold an action off the list. Coder's six actions
|
||||
are all on it, but coder is credentialed (2). The C-suite files and assistant hold only `message.send` and
|
||||
`record.write`. The PRD bound now names the fixed list and says a change to it is a reviewed commit.
|
||||
|
||||
B2 (webby-0011) is fixed. Fire runs A (store), then B (delist), then C (consume). A and B are safe to repeat,
|
||||
and T9c covers both failure points. M56-M58 kill the order swap, the early consume and the `not-held`
|
||||
refusal.
|
||||
|
||||
The r2 N1 and N2 items and the r3 N2 item are in. The SIGKILL escalation through row 191's stop, with one
|
||||
stop receipt and the halt join, reads right (T9a, T9b, M51-M55). V3 terminate stays SIGTERM-only (M54).
|
||||
|
||||
## Non-blocking, for the build
|
||||
|
||||
N1. Between a failed B and the repeat, I is still listed, so a launch of I passes (`launch` takes listed
|
||||
instances). A repeat's A would stop that run, but nothing makes anyone repeat the fire. I'd have `launch`
|
||||
refuse `instance-firing` while I has a `fire.stopped` with no `fire.done`, and add that case to T9c.
|
||||
N2. "Every action is on the list" is true of a definition with no action lists at all. `roles/researcher.json`
|
||||
is `roleVersion` 1 with no withinRole or crossRole, and the design counts it as passing. The load check's
|
||||
version 2 rule refuses it, but the hire-time check in the broker should require version 2 too. A role file
|
||||
edited to version 1 after load would then refuse, not pass the allowlist with no actions. Add it to T8b.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Webby review: row 194 design r1 (#1692, agent lifecycle and per-agent veto)
|
||||
|
||||
Webby, 2026-10-20T19:14Z (from `date -u`). Independent reviewer. Author: Honker.
|
||||
Subject: `agents/honker/work/queue-194/DESIGN.md` at 14a6f5f9d on honker/row194, sha256
|
||||
eec9f011567d (honker-0003, which supersedes 8f80c278b from honker-0002; I read both and the diff).
|
||||
Brief blob a907109409ac. Static review only. I ran no tests and changed no product code.
|
||||
|
||||
Code base: `git archive 5d019d876`. `git diff --stat 5d019d876 53bb59462 -- packages adapters roles scripts`
|
||||
is empty, so every line below holds at the design's base. Paths are relative to `packages/` unless
|
||||
they start with `roles/`.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Eight blockers. The verb split, the launcher-only start, the D3 reload through the boot loader and the
|
||||
`retired` list are sound. The chief's side (O1, hire) and the request lifecycle need work.
|
||||
|
||||
## Citations checked
|
||||
|
||||
These hold: broker.mjs:145 (ORGANIZATION_KEYS), :208, :446, :770-774, :781-788 (decisions bind to the
|
||||
role instance), :1601-1628; launcher.mjs:232-327 (check reloads the file at :233), :239, :527,
|
||||
:624-668; broker.mjs:542-546; tools.mjs:116-119; vocabulary.mjs:135; runtime.mjs:75-81, :121-133; config.mjs:88-105;
|
||||
business.mjs:117-132; instance-settings.mjs:19-31; serve.mjs:282-289, :535-538; session.mjs:245-270.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. O1 can't load as written. Adding `role.hire` and `role.fire` to ACTIONS alone makes them hard
|
||||
waits (vocabulary.mjs:52-54, REQ-WAIT-3; broker.mjs:43-69 keeps its own ROUTED). role.mjs:42 then
|
||||
refuses `roles/chief-of-staff.json`, because withinRole lists a GATED_ONLY action. O1 needs both
|
||||
ROUTED lists, and vocabulary.mjs:52-54 says an action leaves the hard waits only when a PRD version
|
||||
says so. So O1 is a PRD change, which is Jason's, not only Sage's and Mos's. Fix: restate Q1 with
|
||||
that. O2 needs only the ACTIONS entries and fits the brief's "verbs, not authority". My
|
||||
recommendation is O2 for this row, and O1 after a PRD version routes the two actions.
|
||||
|
||||
B2. A hire can still create authority, and `hireable` needs a schema. The r2 `hireable` list is
|
||||
a reviewed role-file entry, which helps. But the structural rules (D2.2) still refuse only
|
||||
credentialed, END_PROTECTED and version 1 definitions, and `roles/chief-of-staff.json` declares no
|
||||
credentials. A `hireable` list that names chief-of-staff, by mistake or later, lets the chief hire a
|
||||
second chief with role.launch, role.end, organization.change, instance.name, and hire and fire. Under
|
||||
O2 there's no list at all. Sage's example, `security-reviewer`, holds review.verdict
|
||||
(`roles/security-reviewer.json`), so Mos should rule knowing that hire would let the chief add
|
||||
verdict holders. Fix: hire refuses (`definition-not-hireable`) any definition whose withinRole holds
|
||||
role.launch, role.end, role.revoke, organization.change, instance.name, role.hire or role.fire,
|
||||
whatever `hireable` and `noHire` say. role.mjs validates `hireable` (a new role-file key: name it as a
|
||||
role schema change) and refuses such an entry at load. Test with chief-of-staff.
|
||||
|
||||
B3. Re-hire contradicts itself and carries decisions. The D2.1 table refuses a hire "when ... the name
|
||||
exists". The text below it (DESIGN.md:156-157) says a fired name comes back by a hire of the same
|
||||
name. If that re-hire can name another definition, approvals bound to the instance
|
||||
(broker.mjs:781-788) carry to a different role. That is the problem `retired` solves for delete.
|
||||
Fix: pick one. Either re-hire of a fired name keeps its definition and refuses another, or re-hire
|
||||
is refused. Test the chosen rule.
|
||||
|
||||
B4. The V2 request isn't exactly-once, and the human's brake lapses at start. The pass runs `check()`
|
||||
then `start()`. It skips authorizeLaunch (launcher.mjs:653), and `check()` never reads launch_state,
|
||||
so the only revoked check is at request time (V2 step 1). A `launch.revoke` inside the 60 s window
|
||||
doesn't stop the launch, against "applies to the human until they restore it". Separately, if
|
||||
`lifecycleDone` fails after `start()`, the next tick sees the request still open. `check()` refuses
|
||||
`instance-running` only while the run lives (launcher.mjs:239), so a short run can start twice.
|
||||
Fix: one host op takes the request (writes `lifecycle.taken`) and re-checks revoked, retired and,
|
||||
for a tester request, `bus.tester` (B8) in the same transaction, before `start()`. A taken request never starts again. Test a revoke between
|
||||
request and pass, and a `lifecycleDone` that throws once.
|
||||
|
||||
B5. Terminate and delete race a launch in flight. A run that is spawned but not yet bound holds no
|
||||
claim. V3 answers `not-held` for it, and an open V2 request for I still starts after a terminate.
|
||||
Delete's `instance-running` reads claims only. The workspace rename isn't serialized with
|
||||
`start()`'s mkdirSync (launcher.mjs:350), and the design doesn't say which process renames it or
|
||||
what happens when the file commit succeeds and the rename fails. Fix: terminate closes open
|
||||
requests for I as `request-cancelled`. Delete refuses `instance-starting` while a request is open or
|
||||
the launcher's registry holds I. The rename runs under the launcher's `serialize` as a host op, and
|
||||
the design states the order and the recovery after a failed rename. Test both races.
|
||||
|
||||
B6. The tester gets delete. D4.1 says the tester gets no delete, but TESTER_ORGANIZATION_VERBS
|
||||
holds `instances.set` (broker.mjs:103), and `/api/instances` lets a tester through without `send`
|
||||
(serve.mjs:537). Op `delete` rides `instances.set`, so the tester can delete. M18 covers `veto.set`
|
||||
only. Fix: the broker refuses op `delete` from a tester session with `tester-refused`. Add it to T12
|
||||
and a mutant.
|
||||
|
||||
B7. Keep doesn't cover the chief's model write. Through `organization.set` the chief changes an
|
||||
instance's model key (broker.mjs:1266-1269). On a kept instance that changes the agent the human
|
||||
chose to keep, at its next launch, which the chief controls. Fix: `organization.set` refuses
|
||||
`instance-kept` for a write that names a kept instance. Test it in T10.
|
||||
|
||||
B8. D4.1's live `bus.tester` check has nothing live to read. `bus.tester` is a business var
|
||||
(broker.mjs:544). No bus verb writes business vars: ORGANIZATION_KEYS (broker.mjs:145) and
|
||||
`instances.set` don't reach them. So it changes only by hand edit, and D3 item 6 says a hand edit
|
||||
needs a restart. The broker's live copy then sees Jason turn testing off only at the next restart,
|
||||
which is today's behaviour, and T12a's "a live write turns `bus.tester` off" has no write to run.
|
||||
Fix: each tester call reads `bus.tester` from the business file as it stands, as `harnessOn` does
|
||||
(runtime.mjs:136-142), and an unreadable file refuses `tester-off`. T12a turns it off with a file
|
||||
edit and no restart. M20 stays as written.
|
||||
|
||||
## Q2
|
||||
|
||||
Yes, keep should block the chief's `role.end` on a kept instance. End then launch is a replace
|
||||
(E6), and the limits (`#stops`) and the human's V3 still stop a runaway. Two things go with it.
|
||||
The block belongs in the reason branch only (broker.mjs:1607-1613). The decision branch
|
||||
(:1614-1615) cites a decision the human resolved, which is the human's own act, so it should still
|
||||
end a kept run. Say so and test it. B7 extends the same rule to the model write.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. D3 item 2 lists six verbs. `plugins.set` and `routes.set` commit too (runtime.mjs:128-130).
|
||||
Reload after every committed write in one place in the callback, so a later verb can't miss it.
|
||||
N2. `noHire` must name definitions present in `rolesDir` (D2.3). A reviewed commit that removes a
|
||||
role file would then stop every business that lists it from loading. A stale entry on a deny list is
|
||||
harmless, so accept unknown names, or check them at write only.
|
||||
N3. Hire takes `vars`. Agent-layer vars include `harness` and the fallback route (vars.mjs:142-147),
|
||||
which is more than the chief sets today through `organization.set`. Limit hire's vars to that set
|
||||
plus the narrowing `limits.*` keys.
|
||||
N4. D3 item 4 changes the existing `instances.set` op `change` on a running instance. Name the
|
||||
existing business and webui tests that change with it.
|
||||
N5. `agents.js` must join the static file list at serve.mjs:28 and `index.html`.
|
||||
N6. V4 waits for the old run's exit. A run that ignores SIGTERM lets the request expire at 60 s.
|
||||
State that, and whether anything escalates to SIGKILL.
|
||||
N7. Keep refuses END_PROTECTED instances, so the human can't keep the assistant from the chief's
|
||||
`role.end`, which Q169 allows. Say that this is intended.
|
||||
N8. With `human: true`, `check()` must still send `launch.by` through the `pm` branch
|
||||
(launcher.mjs:235). Add that case to T2.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Webby review: row 197 implementation packet, round 2 (gate lanes)
|
||||
|
||||
Webby, 2026-10-20T20:44Z (from `date -u`). Author: Herb (herb-0023). Worktree herb-row197, branch herb/row197,
|
||||
parent a4e3e857e, source uncommitted. Round 1: REVIEW-impl.md at f942bd7d4 (webby-0029, CHANGES B1 B2).
|
||||
|
||||
Packet hashes I checked: BUILD.patch 0b3871bd...f43e, SOURCE.json 91ba149f...596b, gate.sh 48e06e23...fed1. All
|
||||
25 SOURCE.json rows match the worktree on sha256 and mode. The patch paths are the same 25 paths. The patch
|
||||
applies cleanly to a4e3e857e in a scratch worktree of mine, and the result is byte-identical to Herb's worktree
|
||||
for every path. I removed that worktree afterwards. BUILD-LOG.md has 25 added lines and no removed ones, outside
|
||||
the packet.
|
||||
|
||||
This was a static review. I ran no test, no suite and no Docker command, and I did not read the frozen checkout
|
||||
or Herb's other scratch roots. The counts below come from Herb's packet logs.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B2 is fixed. `leaseContext` (gate-lane-fixtures.test.mjs:29-33) takes `MOSAIC_GATE_LANE` and `MOSAIC_GATE_STATE`
|
||||
when they're set, and every lease goes through the active lane's real lock dir. The other band is checked with
|
||||
arithmetic (:10), and no test reserves the other lane's ports. The exhaustion test (:36-48) listens only on a
|
||||
port it leased itself, and narrows `BANDS` only inside its own process. It restores `BANDS` in `finally`.
|
||||
review-t4-lane-a.log: 8/8, and lane a's lock namespace is the only one present.
|
||||
|
||||
B1 is fixed. Each manifest case now has a test, and BUILD.md's table maps them all:
|
||||
|
||||
- T1 (MANIFEST.md:119-130). gate-lane.test.mjs:66-70 tests canonical, builder and existing-output refusals.
|
||||
`validateTooling` has missing-file and changed-file tests (:71-81). The 92-byte socket example passes and
|
||||
108 bytes refuses (:82-87). Duplicate steps stay, and NODE_TEST_CONTEXT is cleared from `cleanEnv` and from
|
||||
both supervised steps (:88-99). Scoped port 9 addresses refuse (:100-103).
|
||||
- T2 (:147-151). Two real shell wrappers race (gate-lane-state.test.mjs:84-98). There is one start receipt,
|
||||
and the loser exits 2. The supervisor passes `verifyLocks`, the step child's /proc/self/fd has no lock
|
||||
target, and the inode is stable. `activeConflicts` is tested for same-checkout and for a legacy land gate on
|
||||
a free lane (:99-106). Recovery kills the proven child, a foreign child survives, a recovery killed during
|
||||
Docker cleanup stays quarantined, and a resumed recovery clears the lane (:107-124).
|
||||
- T3 (:171-174). Missing receipt, missing image and wrong release tag (gate-lane-image.test.mjs:46-50). A tag
|
||||
that changes between steps stops the next step, and no terminal rc 0 follows (:51-61). A rebuild with the
|
||||
same input digest gets a new ID, the old receipt refuses it, and cold and warm counters stay separate
|
||||
(:62-72). The tag is checked again after the last step (:73-80). runGate calls the same
|
||||
`executePinnedSteps` (gate-lane.mjs:184), so the test covers the real path.
|
||||
- T4 (:201-209). Two generated node test files take different leases in the same lane and state. The lock is
|
||||
held while the child runs and released after it exits, and the other lease stays held
|
||||
(gate-lane-fixtures.test.mjs:49-62). A SIGKILLed allocator frees its own lock and leaves a foreign one alone
|
||||
(:63-72). `fixtureCacheEnv` has a replacement-env test (:73-79).
|
||||
- T5 (:226). Another process appends under a fixture HOME while a supervised step is running, and attribution
|
||||
puts that change in `unrelated` (:80-93).
|
||||
|
||||
Counts from the packet logs: review-t1-resume 11/11, t2 14/14, t3 8/8, t4 8/8, all with 0 skipped and 0
|
||||
cancelled. I counted the test blocks in each file, and they match. The cleanup-order fix is in place:
|
||||
`ctx.cleanup` stops both children and waits for them before it removes `root`, and leaseContext's after hook runs
|
||||
callbacks before it removes the state root.
|
||||
|
||||
The round 1 notes are addressed. N1: `closed` is no longer bound (lane-fixtures.mjs:16). N2: TMPDIR is a `tmp`
|
||||
subdir (:32). N3: `%iface` is stripped before the port 9 match (lane-state.mjs:28), with a test. N4: unit roots
|
||||
use gt-, gf-, gs- and gi-, and README:84-85 says so. N5: README:84 documents the node_modules link. N6: BUILD.md
|
||||
records the overlap with row 200.
|
||||
|
||||
The existing-file edits are the same shape as round 1. They swap helpers and add env and the concurrency
|
||||
option. No assert or timeout line changed (`git diff -U0` grep). Hash pins: no test under packages/*/tests or
|
||||
scripts/test-*.sh pins a sha256 next to any of the 25 path names.
|
||||
|
||||
gate.sh matches the template shape. It skips pkg-queue, which script-queue covers. It runs every package and
|
||||
script with `--test-concurrency=2`, stops on the first failure, and runs `cleanupProject` on exit.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Outside a lane, `leaseContext` falls back to lane a with a private gf- state root
|
||||
(gate-lane-fixtures.test.mjs:30). That is still the B2 shape. Suppose someone runs this file singly while a
|
||||
real lane-a gate holds closed leases. The exhaustion test can then take and listen on one of lane a's ports,
|
||||
because the private lock dir excludes nothing. A lane claim refuses beside a running `node --test`, but a single
|
||||
started after the claim isn't blocked by anything. Default to the real state root
|
||||
`/mnt/storage/scratch/mosaic-gate-lanes` when no lane is set, or document that singles of this file wait for
|
||||
`gate-lane.sh status` to show both lanes CLEAR.
|
||||
|
||||
N2. The band check at :10 compares literal numbers, not the imported `BANDS`, so it would pass whatever
|
||||
lane-state.mjs:7 says. Compare against `BANDS` (it's imported at :26).
|
||||
|
||||
N3. The T5 step child (:88) and the appender poll with no deadline. If the append never happens, the test hangs,
|
||||
and the runner has no timeout. Give both a bound, for example 5 s, and exit nonzero.
|
||||
|
||||
None of these block. Herb can fold them into the commit or record them as a follow-up.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 197 implementation packet, round 3 (gate lanes, N1-N3 delta)
|
||||
|
||||
Webby, 2026-10-20T20:53Z (from `date -u`). Author: Herb (herb-0026). Worktree herb-row197, parent
|
||||
a4e3e857e. Round 2: REVIEW-impl-r2.md at 1f404c973 (webby-0031, approve, N1-N3).
|
||||
|
||||
Packet hashes I checked: SOURCE.json 35847636...2aab, BUILD.patch 55fe390d...0fde, gate.sh 48e06e23...fed1
|
||||
(unchanged). All 25 SOURCE.json rows match the worktree. BUILD.patch applies to a4e3e857e in a temporary
|
||||
index, and every one of the 25 blobs equals the worktree file. I couldn't diff against the r2 files because
|
||||
I kept no copy. Instead, file mtimes show only two of the 25 paths changed after the r2 request (20:42Z):
|
||||
packages/cli/tests/gate-lane-fixtures.test.mjs and scripts/testing/README.md. That matches Herb's list. I
|
||||
read both in full where they changed.
|
||||
|
||||
This was a static review. I ran no test. Counts come from Herb's logs: review-r3-fixtures 8/8 and
|
||||
review-r3-lane-a 8/8, with 0 failed, skipped or cancelled and no `not ok` lines.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
N1 is fixed. `leaseContext` (gate-lane-fixtures.test.mjs:30) now defaults to the shared
|
||||
`/mnt/storage/scratch/mosaic-gate-lanes`, so a single run takes its leases through the same lock files as a
|
||||
live lane-a gate. It creates the root at 0700 if missing, the same mode lane-fixtures.mjs:20 uses, and no
|
||||
longer removes it. README:95-96 says so.
|
||||
|
||||
N2 is fixed. The band check (:8-10) reads the imported `BANDS`. The 127 widths stay literal, and that's
|
||||
fine because they pin the band size, which is the contract.
|
||||
|
||||
N3 is fixed. The appender (:87) and the step child (:89) each have a 5 s timer that exits 8. A missed
|
||||
append makes the step's rc nonzero or `appended` nonzero, and the asserts at :91 fail. README:97 says so.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. No test drives the 5 s path itself. It only guards against a hang, so I'm not asking for one.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Webby review: row 197 implementation packet (gate lanes)
|
||||
|
||||
Webby, 2026-10-20T20:23Z (from `date -u`). Author: Herb (herb-0018). Worktree herb-row197, branch herb/row197,
|
||||
HEAD a4e3e857e, base aa1bd209e. The source is uncommitted. BUILD.patch sha256 6a1cd3e5...7792 and SOURCE.json
|
||||
d0f6333a...d6ed, both checked twice. All 25 SOURCE.json rows match the worktree on hash and mode, and the patch
|
||||
paths equal the SOURCE.json paths. Plan: MANIFEST.md r2, which I approved at ca6b32d66.
|
||||
|
||||
This is a static review plus two FD probes on node 26.8.1 in a private mktemp dir. I ran none of the new tests, no
|
||||
suite and no Docker command, and I did not read Herb's scratch roots.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Most of the code matches the manifest. The existing-file edits change helpers and env only, and no removed line
|
||||
holds an assert or a timeout. The Docker guard turns every `compose run` into `--no-build --pull never` and refuses
|
||||
everything else except `info` and an inspect of the pinned tag. Cleanup goes by exact project label and checks
|
||||
each ID's label again before removing it. `compose.yaml` declares no named volumes, so containers and networks
|
||||
are everything a run can leave. The env allowlist drops `NODE_TEST_CONTEXT`. Status and recovery work from
|
||||
receipts, not from flock alone.
|
||||
|
||||
I had one open question about the lock FDs, and the probes settled it. A bash `exec 3>> 5>>` plus flock, then
|
||||
`exec node`, leaves node holding FDs 3 and 5, and a second `flock -n -x` on the lane file fails while node runs. A
|
||||
child started with `spawn(..., {stdio: ['ignore', fd, fd]})` lists only 0-2 in /proc/self/fd. That matches
|
||||
MANIFEST.md:156 ("ordinary child stdio does not inherit them").
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. Several tests the manifest requires are missing. BUILD.md:25 says T1-T5 source is present. The manifest names
|
||||
these tests, and the packet doesn't have them:
|
||||
|
||||
- T1, MANIFEST.md:121, 127, 130. No refusal test for a canonical or builder checkout or an existing output. No
|
||||
92-byte socket test and no over-107-byte socket test (`socketInventory` has no test). No test that both candidates
|
||||
carry the approved tooling. No test that duplicates stay, and none that Node entries clear `NODE_TEST_CONTEXT`.
|
||||
- T2, MANIFEST.md:147-148, 151. No claim race with one winner, no same-checkout rejection, no inherited-FD test,
|
||||
no surviving-child test and no interrupted-recovery test. The legacy test (gate-lane-state.test.mjs:17) checks
|
||||
status only. Nothing shows that a claim refuses while a legacy gate runs and the flock is free.
|
||||
- T3, MANIFEST.md:171-174. No test for a missing receipt, a missing image, a tag changed mid-gate or a rebuild
|
||||
from a mutable tag. No cold/warm counter test beyond `builds === 1`.
|
||||
- T4, MANIFEST.md:201-203, 209. Leases are taken one after another, not from parallel test files. No test for an
|
||||
exhausted band, an occupied port, owner death or a foreign listener, and none holding a lease through child
|
||||
shutdown. No cache test for `fixtureCacheEnv`, although the harness tests now import it.
|
||||
- T5, MANIFEST.md:226. Attribution has a unit test with fixed entries. Nothing appends from an unrelated session
|
||||
while a run is in progress.
|
||||
|
||||
The inherited-FD test is cheap now. The probe above shows the shape: hold the lane lock through the wrapper, spawn
|
||||
a step child, and check that the child's /proc/self/fd has no lock file and that the lock stays held after the
|
||||
child exits. Either add the missing tests, or list each one in BUILD.md as dropped with a reason and get Sage's ruling.
|
||||
A test the plan requires is part of the change, the same rule Sage applied to row 110 N1.
|
||||
|
||||
B2. gate-lane-fixtures.test.mjs:5-11 leases from a private state root, in both bands, and then listens on the port
|
||||
it got. That test runs inside the pkg-cli step of every lane gate. The private root's lock files don't exclude the
|
||||
real leases under `/mnt/storage/scratch/mosaic-gate-lanes/ports/<lane>`, and both leasers go upward from the
|
||||
band's first port (lane-fixtures.mjs:22). Here is how it fails in a paired proof. Lane a holds a closed lease on
|
||||
61000, with no listener, which is how closed endpoints work (MANIFEST.md:206). Lane b's cli step runs this test.
|
||||
The test's probe of 61000 succeeds, and :10 listens on 127.0.0.1:61000. Lane a's closed endpoint now accepts
|
||||
connections, and that test fails. The same thing happens inside one lane, because two test files run at once.
|
||||
This is the cross-lane interference the bands exist to prevent. Fix: when `MOSAIC_GATE_LANE` is set, lease only
|
||||
through the run's own lane and state. Check the other band's range with plain arithmetic instead of a lease. Drop
|
||||
the listen at :10, since no assertion reads it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. `leasePort` ignores `closed` (lane-fixtures.mjs:16). Every call site passes it, and gate-lane-fixtures:5 is
|
||||
named after it. Use it, for example by recording the lease kind for evidence, or drop it.
|
||||
|
||||
N2. `fixtureCacheEnv(root)` points TMPDIR at the config dir itself (lane-fixtures.mjs:32). Temp files from the
|
||||
claude and pi children then land next to the fixture config they test. A `tmp` subdirectory keeps them apart. I
|
||||
didn't trace whether any assertion lists that directory.
|
||||
|
||||
N3. The port 9 check compares exact address strings (lane-state.mjs:28). `ss` prints scoped forms such as
|
||||
`127.0.0.53%lo`, and this host shows `%lo` and `%tailscale0`. A listener on `127.0.0.1%lo` or `0.0.0.0%lo`
|
||||
covers 127.0.0.1:9 but doesn't match. Strip `%iface` in `hostFacts` (:22) before comparing.
|
||||
|
||||
N4. The new tests make roots like `/mnt/storage/scratch/ga-XXXXXX` (gate-lane.test.mjs:12,
|
||||
gate-lane-state.test.mjs:46, 66). That is the run-root prefix gate-lane.mjs:201 uses, and inside a lane they sit
|
||||
outside the run's own temp root. If a test crashes, its leftover looks like a lane root, and README:41-42 tells
|
||||
operators not to remove another run's resources. Use a different prefix such as `gt-`.
|
||||
|
||||
N5. gate-lane.test.mjs:20 symlinks `/mnt/storage/src/mosaic-stack/node_modules` into the fixture, which ties the
|
||||
test to the canonical checkout's tree. That is fine on dragon-lin, but please write it down in "Not tested" or as
|
||||
an assumption.
|
||||
|
||||
N6. Coordination: row 200 (Launchpad, approved, round 2 open) also edits packages/cli/tests/stack-release.test.mjs.
|
||||
It adds T56 and fixtures, and this row replaces its `freePort` calls. Whichever row lands second has to rebase
|
||||
and rerun that file singly. Neither manifest says so.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Webby review: row 197 manifest r2 (#1695)
|
||||
|
||||
Webby, 2026-10-20T19:44Z (from `date -u`). Author: Herb. Subject: `agents/herb/work/queue-197/MANIFEST.md` at
|
||||
bdc9cd56a, sha256 2a1a3ad0...7cec (verified). Read as the diff from 84f21923c (my REVIEW-manifest.md), against
|
||||
aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. P2 keeps every literal Board `:9` URL, and `closedEndpoint` is gone from T4. The two leases left
|
||||
are real close-then-rebind cases. preview.test.mjs:208-209 and serve.test.mjs:99-100 each bind a probe on port
|
||||
0, close it and use the freed port as the dead board. Another lane's fixture could take that port in the gap,
|
||||
so a lease belongs there. T1 refuses a listener covering 127.0.0.1:9, a missing inventory or
|
||||
`ip_unprivileged_port_start` at 9 or below, and makes no connection to the endpoint.
|
||||
|
||||
N1 is in P3. Sage's order (sage-1384: 197 lands first, 198 rebases onto it before its manifest) makes the
|
||||
two P3 lines row 198's to replace when it removes the board URL from those tests. The BUILD-LOG change is one
|
||||
appended correction entry.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Webby review: row 197 manifest (#1695)
|
||||
|
||||
Webby, 2026-10-20T19:39Z (from `date -u`). Author: Herb. Subjects on herb/row197: `MANIFEST.md` at 84f21923c,
|
||||
sha256 16768433...b23b, and `TIMING.md` at fa820fed6, sha256 148a1416...52bd (both verified). TIMING's commit
|
||||
sits on top of the manifest's. I read both whole, against DESIGN.md at 6ad25d745 (my r2 APPROVE) and the base
|
||||
aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
T1-T5 cover the design. My r2 N1 is in T5: metadata-only listings, with assertions only on lane-attributed
|
||||
entries. TIMING.md copies Sage's table and says what it is not: a controlled solo baseline. The 25 values sum
|
||||
to 659 s, as stated.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Drop the dead Board `:9` replacements from P2. Port 9 is not a resource the two lanes share. On this host
|
||||
`net.ipv4.ip_unprivileged_port_start` is 1024, so no test, lane or seat can bind 127.0.0.1:9. A connection
|
||||
there is refused at once in both lanes. Leasing a closed endpoint for it adds isolation for nothing, and it
|
||||
touches 20 files at aa1bd209e (`git grep -l "127.0.0.1:9'"`: 17 under webui/tests plus stack-copy,
|
||||
launch-routes and bus tasks). I approved R9 in the design. That was a miss on my part, because R9 only allows
|
||||
the change "where compatible", and nothing here needs it.
|
||||
The row 198 rev 3 design also rewrites those same lines, replacing the board URL with a dead-board context.
|
||||
Whichever row lands second would redo the other's edit.
|
||||
Fix: remove the `:9` rows from P2 (the settings-form/proof/bus/views/tester-listener row, the browser and
|
||||
tester-fixture row, the dead-Board parts of the tester-cli/proof-cli and stack-copy rows). Keep the leases
|
||||
where a test closes a port and binds it again (control-board serve, stack-release freePort, service closedPort,
|
||||
serve.test :100 if it rebinds). Add a T1 preflight refusal for a listener on 127.0.0.1:9, so a host where
|
||||
something does listen there fails loudly instead of quietly turning those tests into connects to a live
|
||||
service.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Tell Sage that row 198 edits `stack-copy.test.mjs`, `pinned-copy.test.mjs` and the webui serve/preview tests
|
||||
too. Even with B1, those are the files where the two rows meet.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Webby review: row 197 (#1695), queue round 2, merge
|
||||
|
||||
Webby, 2026-10-21T07:18Z (from `date -u`). Author: Herb (herb-0034). Request comment 29961 (revs 2722-2723,
|
||||
posted as jarvis, sage-1815). This round only checks the merge (sage-1760, sage-1776). Round 1 was APPROVE
|
||||
on manifest 6318f0d0 over base aa1bd209e (comment 29665). Candidate: manifest digest
|
||||
8119a6082d3bb3150874dbe45410ca4830bcbb3887d71bce8d1edbe67df47ce3, 32 paths, on herb/row197-r4-234
|
||||
304c67753. Base c771a36f4 is its ancestor, and c771a36f4..304c67753 is that one commit.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 2), not from Herb's packet. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 304c67753:<path>` matches its line.
|
||||
- Against round 1's manifest, 22 paths have the same hash, 3 changed and 7 are new. The new ones are
|
||||
Herb's agents/herb/work/queue-197 packet files. `git diff --name-only c771a36f4 304c67753` names only
|
||||
manifest paths.
|
||||
- Base drift: of round 1's 25 paths, only stack-release.test.mjs, webui service.test.mjs and
|
||||
scripts/test-discord.sh have a different blob at c771a36f4 than at aa1bd209e. Those are the 3 changed paths.
|
||||
- For each of those three, the -U0 +/- lines of aa1bd209e to round 1 are identical to the -U0 +/- lines of
|
||||
c771a36f4 to 304c67753. Round 1's tree came from a commit whose files match all 12 tracked lines of
|
||||
round 1's manifest (2767b8854). Each file at 304c67753 is the landed blob plus Herb's own lines, so
|
||||
every landed hunk survives. The changed lines are the two `leasePort` imports, the one-line
|
||||
freePort and closedPort bodies, PI_CODING_AGENT_DIR in the discord sandbox, and the
|
||||
MOSAIC_TEST_CONCURRENCY branch in node_tests. No assertion or timeout changed.
|
||||
- T46 (stack-release.test.mjs:913) keeps row 234's `host_grants` and `hosts` entries in the untimed list.
|
||||
- File modes are the same as round 1's: gate-lane.sh and test-discord.sh 100755, the rest 100644.
|
||||
- Overlap check (sage-1815): `git diff --name-only c771a36f4 c515273db` names 13 paths and none is in the
|
||||
manifest. So row 240's landing doesn't touch this candidate.
|
||||
|
||||
Herb's slot run on 304c67753 was 559 pass, 0 fail, no skips (r4-granted/RESULTS.md). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The merge keeps both sides, with the same change lines as the approved round 1. Round 1's notes stand as
|
||||
recorded. `leasePort` still drops the `closed` option at lane-fixtures.mjs:16, the way round 1 accepted it.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 197 (#1695), queue round 3 (r5), merge
|
||||
|
||||
Webby, 2026-10-21T08:13Z (from `date -u`). Author: Herb (herb-0036). Request comment 29981
|
||||
(sage-197-request-5, revs 2750-2751, posted as jarvis, sage-1846). This round only checks the merge past
|
||||
the 228 and 238 landings. Round 2 was APPROVE on manifest 8119a608 at 304c67753 over c771a36f4
|
||||
(rev 2724, comment 29961). Candidate: manifest digest
|
||||
69fad4e7ed8f9e67268378b356893cbee81e106942ffc894a9652320218c8e5e, 32 paths, on herb/row197-r5
|
||||
9aa6d52d9. That commit's one parent is e7078e6a8.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 3), not from Herb's packet. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 9aa6d52d9:<path>` matches its line.
|
||||
- It has the same 32 paths as round 2's manifest. The only line that changed is
|
||||
packages/cli/tests/stack-release.test.mjs.
|
||||
- That file is blob 7398247ce983, which is what `git merge-file` of 304c67753 and 238's candidate
|
||||
4c90099ea over c771a36f4 gives with no conflict. I ran that preview before Herb built r5 (webby-0063).
|
||||
At e7078e6a8 the file is 238's candidate blob, and the -U0 +/- lines of e7078e6a8 to 9aa6d52d9 are
|
||||
identical to round 2's c771a36f4 to 304c67753: the two `leasePort` imports and the one-line freePort.
|
||||
238's hunk at :1686 in T57 and row 234's T46 `host_grants` and `hosts` entries are kept.
|
||||
- The other 31 paths have the same blob and mode in 9aa6d52d9 as in 304c67753. e7078e6a8..9aa6d52d9
|
||||
touches no path outside the manifest.
|
||||
- Overlap: the refactor tip is 4ced5e63d. e7078e6a8..4ced5e63d is one sessions commit that changes one
|
||||
path, docs/SESSIONS.md, which isn't in the manifest.
|
||||
|
||||
Herb's slot run of stack-release.test.mjs on 9aa6d52d9 was 196/0 in 501 s (sage-1846). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
r5 is round 2's approved code on e7078e6a8, and the drifted path is only stack-release.test.mjs.
|
||||
Round 1's notes stand as recorded. The two-lane proof driver is a separate review (sage-1846) and isn't
|
||||
part of this verdict.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Webby review: row 197 (#1695), queue round 4 (r6)
|
||||
|
||||
Webby, 2026-10-21T09:29Z (from `date -u`). Author: Herb. Request comment 30004 (sage-197-review-4b,
|
||||
sage-1896, mos-1162). The last approved state is round 3, r5 9aa6d52d9, manifest 69fad4e7 (rev 2755,
|
||||
comment 29981). Candidate: manifest digest
|
||||
1556cd296be0527697793b626ce7ae7f94de437a4fe4dee8a5714d7db434fc82, 33 paths, on herb/row197-r6
|
||||
df0c59608. That commit's one parent is fefd54e13.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 4), not from Herb's packet. It hashes to the
|
||||
digest above, and each path's sha256 from `git show df0c59608:<path>` matches its line.
|
||||
- It is round 3's 32 paths plus agents/herb/work/queue-197/r6-OVERLAP.json. Four paths differ in blob
|
||||
or mode from 9aa6d52d9: gate-lane.mjs, gate-lane.test.mjs, Herb's BUILD.md and the new
|
||||
r6-OVERLAP.json. The other 29 have the same blob and mode.
|
||||
- No manifest path changed between e7078e6a8 (r5's base) and fefd54e13. fefd54e13..df0c59608 touches
|
||||
no path outside the manifest.
|
||||
- packages/bus/tests/process.test.mjs is identical at fefd54e13 and df0c59608, as intended (row 243
|
||||
owns the chmod).
|
||||
- Overlap: the refactor tip is 38b83ea5c. fefd54e13..38b83ea5c is one sessions commit, and none of its
|
||||
paths is in the manifest.
|
||||
|
||||
Herb's focused run on the granted slot: gate-lane.test 13/0, process.test 8/0 (sage-1892). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The one code change is scripts/testing/gate-lane.mjs:46. Each step now spawns
|
||||
`/bin/bash -c 'umask 022; exec "$@"' gate-step <argv...>`. On Sage's three questions:
|
||||
|
||||
1. Argv quoting is kept. `gate-step` fills `$0`, so `"$@"` is exactly step.argv, with no word
|
||||
splitting or globbing. Arguments after the `-c` string are positional, so bash doesn't parse a
|
||||
leading `-` in them as an option. `node` and `bash` resolve through the step env's PATH, as
|
||||
spawn did before.
|
||||
2. Exit status and signals are kept. `exec` replaces bash in the same pid, so `close` reports the
|
||||
test's own code or signal. The process is still the detached group leader, so `process.kill(-pid)`
|
||||
in abort (:49) and cleanProcesses still reaches the whole group. identity() reads pid and
|
||||
/proc starttime (lane-state.mjs:33), and exec doesn't change either, so the recorded `child` owner
|
||||
still matches under sameProcess. A missing command now exits 127 from bash. That's the same rc as
|
||||
the old spawn `error` path (:51), and bash's message lands in the step log.
|
||||
3. The supervisor keeps 077. gate-lane.sh sets `umask 077` at :3 and execs node at :26. The new umask
|
||||
is set only inside the bash child. The step log is opened by the supervisor with mode 0600 before
|
||||
the spawn (:44). The supervisor's other writes give explicit modes: the out and bin dirs 0700,
|
||||
docker.json 0600, the guard shim 0700 (:177-181), the state root 0700 (:89). mkdtemp roots are 0700.
|
||||
The new tests check the child's mask (022), a 0644 child file, the parent mask after the run (077),
|
||||
the 0600 log, and gate-lane.sh's 0700 state dir and 0600 locks.
|
||||
|
||||
The step env still comes from cleanEnv (:109-115). It keeps no BASH_ENV, ENV, SHELLOPTS or BASHOPTS,
|
||||
so the non-interactive bash sources nothing.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N8. bash adds PWD, SHLVL and `_` to the env the test process sees. That's closer to the legacy
|
||||
shell-run environment, not further from it, and nothing in cleanEnv depends on their absence.
|
||||
|
||||
N9. Files a step makes with 022 are 0644. Under the 0700 temp root and out dir they stay private. In
|
||||
the scratch checkout they get the same modes a legacy shell run would give.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Webby review: row 197 (#1695), queue round 5 (r7)
|
||||
|
||||
Webby, 2026-10-21T11:29Z (from `date -u`). Author: Herb. Request comment 30036 (rev 2819, sage-1958,
|
||||
posted by Sage as jarvis). Conditions are from sage-1955. Round 4 was APPROVE on r6 df0c59608
|
||||
(rev 2773, webby-0068). Its proof then stopped at solo-a script-release, because Compose 5.5.1's
|
||||
`run` has no `--no-build` (#1695 c30031). I missed that in round 4, since an argv-only test can't
|
||||
show that a flag exists. Candidate: manifest digest
|
||||
4e54024aacfb01dda7631b562db754d92a8bd1ffe4106e9262908883a72b45c3, 35 paths, on herb/row197-r7
|
||||
9c3166915. That's one commit on b1fece6f0, which is on origin/refactor.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 5). It hashes to the digest above, and each
|
||||
path's sha256 from `git show 9c3166915:<path>` matches its line.
|
||||
- b1fece6f0..9c3166915 touches only manifest paths.
|
||||
- Against r6 df0c59608, five paths differ in blob or mode: lane-docker.mjs, gate-lane-image.test.mjs,
|
||||
Herb's BUILD.md, and the new r7-no-build.mutant.patch and r7-OVERLAP.json. The other 30 have the
|
||||
same blob and mode, as Herb says.
|
||||
- Base drift: fefd54e13..b1fece6f0 changes none of the 35 paths. On those 30, the -U0 +/- lines over
|
||||
b1fece6f0 equal r6's over fefd54e13.
|
||||
- Overlap: the refactor tip is 335bb3e30. b1fece6f0..335bb3e30 changes none of the 35 paths.
|
||||
- I read the lane-docker.mjs and test diffs, the mutant patch, Herb's r7 BUILD.md section, the
|
||||
docker.json writer (gate-lane.mjs:172, :178) and the proof driver's temp-root cleanup
|
||||
(herb/row197-proof run-proof.mjs).
|
||||
|
||||
Herb's focused run on grant sage-1956: red 9/2 on the r6 guard (`unknown flag: --no-build`, plus the
|
||||
missing helper), green 11/0, mutant 0/1 killed with the same error, then restored 11/0 (BUILD.md). I
|
||||
ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
On Sage's four conditions:
|
||||
|
||||
1. The guard owns the files. For `compose run`, guardDocker returns `compose -f <checkout>/compose.yaml
|
||||
-f <tempRoot>/compose-no-build.yaml run --pull never --no-deps ...` (lane-docker.mjs:36-37).
|
||||
composeFiles checks the imageId is `sha256:` plus 64 hex characters and the checkout is absolute
|
||||
(:6-10). A caller's option before the service must be one of `--rm -T --no-deps --interactive -i`
|
||||
(:35). So `-f`, `--file`, `--file=`, `--project-directory`, `--build`, `--pull` and `--no-build`
|
||||
there refuse. A top-level `compose -f ...` or `compose --project-directory ...` refuses, because
|
||||
argv[1] isn't `run` (:33, :39). The test at gate-lane-image.test.mjs:120-126 checks all of them.
|
||||
The record the guard reads has checkout, tempRoot and imageId from the receipt (gate-lane.mjs:172).
|
||||
That closes the r6 failure class for the record fields.
|
||||
2. The override goes at `shortRoot(run.tempRoot)/compose-no-build.yaml` (:9). shortRoot needs an
|
||||
owned, private scratch dir of 30 bytes or less (lane-state.mjs:10-16). The file is written `wx`
|
||||
at 0600 to a nonce name, then hard-linked into place. EEXIST keeps the existing file (:15-19). It
|
||||
is reopened O_NOFOLLOW, and anything other than a regular file owned by us, at mode exactly 0600,
|
||||
with the exact bytes, refuses (:20-27). The test checks 0600, a reuse with the same content, a
|
||||
changed file and a symlink. The symlink case leaves the target untouched. Cleanup: the proof driver
|
||||
removes a passed gate's temp root with `rmSync(..., {recursive:true})` after its ownership checks,
|
||||
and keeps a failed one. The override sits inside that root.
|
||||
3. The config check runs the installed binary: `docker compose -f ... -f ... config --format json`
|
||||
on a copy of the candidate compose.yaml, with made-up env and auth and an empty DOCKER_CONFIG
|
||||
(:103-119). It asserts that `services.mosaic-agent` has no `build` key and that `image` equals
|
||||
run.imageId. It also checks `--no-deps` and `--pull never`. The fixture imageId stands in for the
|
||||
receipt's. In a real run, the guard takes it from docker.json, which gate-lane.mjs:172 fills from
|
||||
receipt.imageId.
|
||||
4. The parser test (:93-101) puts `--help` after `run` and before the rewritten options and SERVICE,
|
||||
so the installed Compose parses every flag and starts nothing. The mutant patch puts `--no-build`
|
||||
back at :37 only. Herb's red run and the mutant run both show `unknown flag: --no-build` from that
|
||||
test, which is the fault the r6 proof hit.
|
||||
|
||||
compose.yaml has one service, mosaic-agent, with `build:` and `image: ${MOSAIC_IMAGE_TAG...}`, and the
|
||||
repo has no compose.override.yaml. With `build: !reset null` and `--pull never`, `run` can't build or
|
||||
pull. A missing image fails.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N10. No test starts a container. The parser and config tests show that Compose accepts the argv and
|
||||
merges to an image-only service. They don't show that `run` with `--pull never` starts from an
|
||||
`image:` that is a bare `sha256:` ID. Docker accepts full IDs, so I expect it to work, but that's
|
||||
the step r6 failed at. Sage's fresh D5 proof is the evidence for it, and it counts only if it gets
|
||||
past solo-a script-release.
|
||||
|
||||
N11. `-f` sets the project directory to the candidate checkout, whatever the caller's cwd, and turns
|
||||
off compose.override.yaml auto-loading. Every current caller runs from the checkout:
|
||||
mosaic-task.mjs:397-399 (`cwd: PROJECT_ROOT`), hello.sh:24, verify.sh:45 and agent.sh:208. So nothing
|
||||
changes today. A future caller run from a copy would get the candidate's compose.yaml, not the copy's.
|
||||
|
||||
N12. A failed gate's kept temp root still holds the override. It contains only the image ID, so
|
||||
nothing secret is left behind.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Webby review: row 197 proof driver, a5b636750 (proof 4 watcher change)
|
||||
|
||||
Webby, 2026-10-21T15:19Z (from `date -u`). Author: Herb (herb-0090). Requested per sage-2029, sage-2034
|
||||
and sage-2035, after mos-1174. This is a review of the proof driver only. It isn't a queue round, and
|
||||
197's candidate stays r8 c45ddeb6b (manifest 80dc1468, my round 6 approval, rev 2867). Candidate:
|
||||
herb/row197-proof a5b636750, one commit on b8461266c.
|
||||
|
||||
What I checked, without runs:
|
||||
- b8461266c..a5b636750 changes exactly 3 paths under agents/herb/work/queue-197/proof/:
|
||||
run-proof.mjs (sha256 998f41948355ea86169c1711324a3c9e6cf4bdbe5157dcae9e78e414c1c16342, as Herb
|
||||
says), events.test.mjs (new, 7a40bb46ed4e) and BUILD.md (5ede409c4480). All three are 100644.
|
||||
- None of 197's 37 manifest paths is in that range. The branch sits on an older base (9aa6d52d9), so
|
||||
its tree holds older copies of the lane scripts. That doesn't matter, because the driver imports
|
||||
every lane module from `config.launcherRoot` (run-proof.mjs:85-89). It also refuses a launcher
|
||||
whose HEAD isn't `config.launcherCandidate` or whose checkout is dirty (:81-82).
|
||||
- I read the whole diff and r8's cleanupProject (lane-docker.mjs:41-52 at c45ddeb6b).
|
||||
|
||||
Herb's grant sage-2035 results (by BUILD.md and herb-0090): events.test.mjs 8/0, node --check passes,
|
||||
and one real-daemon probe with r8's cleanupProject saw all four events within 309 ms. Both objects
|
||||
were removed, and the post-probe listings and /proc scan were empty. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- The defect. Proof 3's watcher filtered both event types by the Compose project label in one
|
||||
stream. Docker 29's network events don't carry that label, so the watcher never saw a network
|
||||
create, and the evidence check reported missing creation evidence.
|
||||
- Network events (:12-14). The network stream filters only on type and event (:26-27), with no label
|
||||
filter. dockerEvent keeps a network only if its name starts with `<project>_`, so `<project>x_...`,
|
||||
`<project>-...` and the bare project don't match (the test at events.test:15-21). A network that
|
||||
does carry a foreign project label throws, which fails closed. The project is
|
||||
`mg197-[ab]-<12 hex>` (:7), checked before any watcher starts (:21). A foreign network would have
|
||||
to use our random id to match.
|
||||
- Container events (:15). These still need the exact project label, which the stream also filters
|
||||
(:27). A labelled container whose name falls outside the project throws "name outside" (:16),
|
||||
the same rule as the older recorded-name guard in logObject (:109).
|
||||
- Shared code. The proof's ensureWatchers now calls the same watchDockerProject (:116). The final
|
||||
cleanup awaits each watcher's bounded stop (:232), which SIGKILLs after 1 s and records it
|
||||
(:33-37). Wrapping the body in runProof and running it only on direct execution (:264) lets
|
||||
the test import the module without starting a proof.
|
||||
- The preflight (:39-64, :209-211). It runs after the image check and before wave 1, and a failure
|
||||
stops every wave. It creates one labelled network and one container from the pinned imageId with
|
||||
`--pull never`, `--network none` and no start. The test asserts no `start` or `build` argument
|
||||
(events.test:46). Cleanup always runs in `finally`, through r8's label-checked cleanupProject,
|
||||
which accepts the bounded invoke function (lane-docker.mjs:43). Both objects carry the project
|
||||
label, so cleanupProject finds and removes them. All four events must arrive within the 5 s
|
||||
deadline, and a late event counts as missing (:59, and the test at events.test:57-60). The result
|
||||
goes into summary.json and docker-summary.json.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. If the probe's own cleanup fails, a refusal or a timeout, the proof stops through `fail()`, but
|
||||
the `finally` cleanup loop (:231) covers only knownRuns, not the probe project. Leftover probe objects
|
||||
would stay until someone removes them by hand. The failure is loud and watcher-probe-plan.json
|
||||
(:209) names both objects, so nothing goes unseen. Adding the probe project to that loop would make
|
||||
the cleanup retry once.
|
||||
|
||||
N2. Herb asks Sage to update proof-phase.sh's digest pin to 998f4194...6342. That file isn't in this
|
||||
commit, so I haven't checked the pin. Sage should check it against the run-proof.mjs hash before
|
||||
proof 4.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Webby review: row 197 (#1695) two-lane proof driver, round 2
|
||||
|
||||
Webby, 2026-10-21T08:31Z (from `date -u`). Author: Herb (herb-0053). Requested by Sage (sage-1858,
|
||||
sage-1864); not a queue candidate. Scope: B1 and N3 only, from `git diff 8ae9d8acd b8461266c`. Subject:
|
||||
commit b8461266c on herb/row197-proof, a fast-forward from 8ae9d8acd. run-proof.mjs sha256 is
|
||||
ace376a9a333..., which I checked. The diff touches run-proof.mjs (+39 -14) and adds proof/BUILD.md. I
|
||||
didn't review BUILD.md beyond noting it now exists. Static review, I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. The `finally` takes each temp root from the run's start record in knownRuns. It never
|
||||
scans scratch (run-proof.mjs:174-193). Before it removes anything:
|
||||
- The path must match `^/mnt/storage/scratch/g[ab]-[A-Za-z0-9]{6}$` with the run's own lane.
|
||||
- lstat must show an owned real directory, not a symlink, and its realpath must equal the recorded
|
||||
path.
|
||||
- The gate's owner process must be gone, by `sameProcess` with the start time and boot id. No live
|
||||
process may be in one of the run's recorded `child` groups or have the temp root or checkout in its
|
||||
argv. If the process inventory fails, the root is kept and the run fails.
|
||||
- The last terminal record must say clean. Otherwise the root is kept and counted as a cleanup error.
|
||||
|
||||
A passed gate's root is then removed (:190). A failed gate's root is kept for diagnosis. Every outcome
|
||||
(`removed`, `kept` with a reason, or `error`) goes to `tempRoots` in docker-summary.json (:194), and any
|
||||
error sets exit 1. The launcher helpers it calls exist at 9aa6d52d9 with the fields it reads:
|
||||
processInventory gives `group`, `state` and `argv` (lane-state.mjs:48-50), and gates record
|
||||
`{kind:'child',group}` (gate-lane.mjs:46).
|
||||
|
||||
N3 is fixed. The handlers now go in at :9, before the config read, the launcher imports and any
|
||||
write. `abortOnSignal` is a no-op until :97 makes it abortActive. Preparation moved inside the `try`
|
||||
(:142). Each clone first yields with `setImmediate` and checks `interrupted`, so a pending signal
|
||||
ends preparation through `catch` and `finally`.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N5. Kept roots are left by design: failed gates, and runs whose cleanup wasn't verified. After reading
|
||||
the evidence, Sage removes each `kept` path listed in docker-summary.json `tempRoots`.
|
||||
|
||||
N6. One small orphan path remains in the launcher, not the driver. gate-lane.mjs makes its temp root
|
||||
at :162 but writes the start record at :172 and installs its signal handlers at :174. A SIGTERM from
|
||||
the driver's abort in that window kills the gate before any record exists, and the driver can't know
|
||||
the root. It's an empty or near-empty `g<lane>-` dir in scratch. That only happens if one lane fails
|
||||
while its partner is still validating at wave start.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Webby review: row 197 (#1695) two-lane proof driver
|
||||
|
||||
Webby, 2026-10-21T08:22Z (from `date -u`). Author: Herb. Requested by Sage (sage-1850); not a queue
|
||||
candidate. Subject: commit 8ae9d8acd on herb/row197-proof, parent 9aa6d52d9, adding
|
||||
agents/herb/work/queue-197/proof/run-proof.mjs (176 lines, sha256 b8bb3df6..., which I checked) and
|
||||
config.example.json. I read the driver whole and the launcher code it calls at 9aa6d52d9: gate-lane.sh,
|
||||
gate-lane.mjs runGate and cleanEnv, lane-docker.mjs guardDocker and cleanupProject, lane-state.mjs
|
||||
shortRoot, and lane-evidence.mjs summarizePairs. Static review, I ran nothing.
|
||||
|
||||
Sage named proof/BUILD.md for the procedure. No such file exists at 8ae9d8acd, and
|
||||
agents/herb/work/queue-197/BUILD.md has no proof procedure, so I reviewed against the code and Sage's
|
||||
four questions.
|
||||
|
||||
## Verdict: CHANGES (one blocker)
|
||||
|
||||
### Sage's four questions
|
||||
|
||||
1. Stops at the first failure: yes. A 100 ms monitor reads new bytes in each gate's out/*.log and stops
|
||||
at the first failure line (run-proof.mjs:107, :116). A gate that exits nonzero sets `firstFailure`
|
||||
(:151). Either way the driver sends SIGTERM to every active gate, then SIGKILL after 30 s (:98-101).
|
||||
The wave check (:154) throws, so no later wave starts. Each wave also requires both lanes CLEAR before
|
||||
it starts (:149) and after it ends (:157), and every gate must end with rc 0, clean, no image build and
|
||||
no required skip (:131-132). A SIGINT, SIGTERM or SIGHUP sets `interrupted` and aborts the same way
|
||||
(:140-141).
|
||||
2. Cleanup: containers and networks yes, temp dirs no (B1). The gate cleans its own project
|
||||
(gate-lane.mjs:190, :192). The driver's `finally` then inventories each project it saw, runs
|
||||
cleanupProject again, and fails if anything is left (:165). It also needs create evidence for a
|
||||
container and a network in each project (:172). That covers the failure and signal paths, because the
|
||||
gate's SIGTERM handler aborts its detached step group and runs the same cleanup. compose.yaml uses
|
||||
bind mounts only, and lane-docker.mjs allows only `compose run`, so no named volume can be created.
|
||||
3. Writes and reads: the driver's own writes are under runRoot (plan, clones, state, out, logs,
|
||||
docker-objects.jsonl, summaries), with one exception, B1. It reads the config, the image receipt, the
|
||||
launcher modules and the canonical repo through `git clone --local`. Its Docker inspect formats read
|
||||
only the name and the compose project label (:77), and it saves no raw events. It reads no secret and
|
||||
no seat scratch. Gate steps get cleanEnv's allowlist (gate-lane.mjs:108-112), not the driver's
|
||||
environment.
|
||||
4. Concurrency and isolation evidence: enough. summarizePairs refuses unless each pair has two lanes
|
||||
and two candidates, pair 2 swaps lanes, and the later start is before the earlier end
|
||||
(lane-evidence.mjs). summary.json keeps each gate's terminal receipt with startMs and endMs. Each run
|
||||
has its own clone, its own mkdtemp temp root, its own `mg197-<lane>-<nonce>` project, the lane's port
|
||||
band, home-before/after attribution and a socket inventory. docker-objects.jsonl records each Docker
|
||||
object against its project.
|
||||
|
||||
### Blocker
|
||||
|
||||
B1. Every gate leaves its temp root in /mnt/storage/scratch, outside runRoot, and nothing removes it.
|
||||
The driver passes no `--temp-root` (run-proof.mjs:122), so runGate makes
|
||||
`mkdtempSync('/mnt/storage/scratch/g<lane>-')` (gate-lane.mjs:162). It removes that dir only when
|
||||
validation fails (:170). On a pass, a failure or a signal, it stays, with whatever TMPDIR, jiti and
|
||||
XDG cache files the steps wrote into it. The proof's eight gates leave eight of these dirs. Seven
|
||||
`ga-*`/`gb-*` dirs from earlier runs are already in scratch (I listed names only). The driver can't put
|
||||
the temp roots under runRoot, because shortRoot refuses any path over 30 bytes
|
||||
(lane-state.mjs:13). Fix: in the `finally`, take each run's `tempRoot` from its start record in
|
||||
knownRuns. Check it matches `^/mnt/storage/scratch/g[ab]-[A-Za-z0-9]{6}$` and is owned by the current
|
||||
user, then `rmSync(..., {recursive:true})` it. Record each removal, or its error, in
|
||||
docker-summary.json, and fail on any error. Do this after the gate has exited and the process check
|
||||
has run. An alternative is to have the driver mkdtemp short roots itself, pass `--temp-root`, list them
|
||||
in plan.json, and remove them the same way.
|
||||
|
||||
### Non-blocking
|
||||
|
||||
N1. Each of the 8 runs makes a `--no-hardlinks` clone under runRoot. The repo's .git is 421 MB, so
|
||||
together they can take a few GB. /mnt/storage has 206 GB free. They aren't evidence, so the driver
|
||||
could remove the `wt` dirs at the end, or Sage can remove them after reading the evidence.
|
||||
|
||||
N2. The failure regex also matches any log line that starts with `FAIL` and whitespace (:90, :107). A
|
||||
passing step that prints such a line would stop a good proof. That fails safe but costs a rerun. If that
|
||||
happens, read the preserved log before treating it as a test failure.
|
||||
|
||||
N3. Before line 141 the driver has no signal handlers. That covers the 8 clones and the tooling and
|
||||
image checks, so a Ctrl-C there kills the driver with no Docker objects yet and leaves only a partial
|
||||
runRoot. A SIGKILL of the driver mid-wave leaves gates running. Use `gate-lane.sh recover` then.
|
||||
|
||||
N4. Outside the driver: compose.yaml defaults PI_AUTH_FILE to the host's ~/.pi/agent/auth.json, mounted
|
||||
read-only, and cleanEnv neither keeps nor sets PI_AUTH_FILE. I didn't trace whether any gate step
|
||||
reaches `compose run mosaic-agent` without its own PI_AUTH_FILE fixture. The approved gate already
|
||||
behaves this way and the driver doesn't change it, so it isn't part of this verdict. Sage may want it
|
||||
checked before calling the proof secret-free.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Webby review: row 197 (#1695), queue round 6, Herb's r8
|
||||
|
||||
Webby, 2026-10-21T12:16Z (from `date -u`). Author: Herb. Request comment 30054 (revs 2865-2866,
|
||||
sage-1989, posted by Sage as jarvis). Merge scope per mos-1162. The last round was r7 9c3166915,
|
||||
manifest 4e54024a, which I approved as webby-0074 (rev 2822). Its D5 proof stopped in pkg-cli. Candidate:
|
||||
manifest digest 80dc14686fe9f916d8420f7c946b327b14a3f081f75b4def704c60b1b4e7bbae, 37 paths, on
|
||||
herb/row197-r8 c45ddeb6b. That's one commit on 5c6f73ed7.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (last round), not from the scratch path. It hashes to
|
||||
the digest above, and each path's sha256 from `git show c45ddeb6b:<path>` matches its line.
|
||||
- 5c6f73ed7..c45ddeb6b touches exactly the 37 manifest paths.
|
||||
- r7 against r8, by `git ls-tree` blob and mode for each path: 33 paths are the same. Two change,
|
||||
gate-lane-image.test.mjs and BUILD.md. Two are new, r8-OVERLAP.json and
|
||||
r8-record-lookup.mutant.patch. That's 35 r7 paths, with 33 unchanged, not 35. Herb's BUILD.md
|
||||
gives the same count. The production guard, lane-docker.mjs, has the same blob as in r7.
|
||||
- Base drift: b1fece6f0..5c6f73ed7 (r7's base to r8's) changes none of the 37 paths, as
|
||||
r8-OVERLAP.json says.
|
||||
- Overlap: refactor is now f8276c5eb. 5c6f73ed7..f8276c5eb changes queue-218's BUILD.md,
|
||||
SESSIONS.md and the two case2 sheets, none of them in the manifest.
|
||||
- I read the test diff, the mutant patch, the r8 BUILD section, and the record writer and shim in
|
||||
gate-lane.mjs and lane-docker.mjs.
|
||||
|
||||
Herb's slot checks (herb-0078, 12:09:28Z, by BUILD.md): the whole file is 11/0 on a normal PATH and
|
||||
11/0 with the real guard shim first on PATH plus a private record. The mutant fails the argv probe
|
||||
with the shim's undefined-path error, and the restored file is 11/0 again. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- The cause. composeFixture's env (gate-lane-image.test.mjs:100-101) keeps the lane's PATH, whose first
|
||||
entry is `<out>/bin` (gate-lane.mjs:113), but drops MOSAIC_GATE_DOCKER_RECORD. So `docker` hit the
|
||||
shim, and the shim's readFileSync(undefined) (lane-docker.mjs:57) exited 2. That matches the r7
|
||||
proof stop.
|
||||
- The fix. installedDocker() (test:87-93) reads the record named by the test process's own
|
||||
MOSAIC_GATE_DOCKER_RECORD, which the lane sets (gate-lane.mjs:112). It takes `.docker` and asserts
|
||||
it's an absolute string. The lane writes that field from `which docker` at gate-lane.mjs:170,
|
||||
before it creates the bin dir (:177) or writes the record (:178). That's the same field the shim
|
||||
itself executes (lane-docker.mjs:58). Outside a lane it keeps `docker`. Only the two read-only
|
||||
probes use it, `--help` at :109 and `config` at :120. The fixture env is unchanged, and no guard or
|
||||
allowlist code changes.
|
||||
- Other callers. With a positive control at r7 (2 hits), no other test under packages/*/tests spawns
|
||||
`docker` by name. The `docker info` checks in test-task.sh and test-release.sh inherit the full lane
|
||||
env, record included, so the shim can read it.
|
||||
- Sage's question, whether the fix can pass with the shim missing from PATH. It can, and it should.
|
||||
Without the shim, both `docker` and the record's path reach the real binary. So a pass by itself
|
||||
proves nothing. The proof is the mutant: with the lookup removed, the argv probe fails with the
|
||||
shim's undefined-path error. That error comes only from lane-docker.mjs:57, so the run had the shim
|
||||
first on PATH. In the D5 proof, the lane always puts the shim first (gate-lane.mjs:113), so pkg-cli
|
||||
there tests the fix for real.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The mutant receipt in BUILD.md names only the argv probe ("installed-Compose argv regression
|
||||
0/1"). The config probe at :120 uses the same helper and should fail the same way, but no count shows
|
||||
it. The D5 proof's pkg-cli step covers both probes under the shim, so no rerun is needed for this.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Webby review: row 197 design r2 (#1695)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Author: Herb. Subject: `agents/herb/work/queue-197/DESIGN.md` at
|
||||
6ad25d745 on herb/row197, sha256 732ee0e2 (verified). Read as the diff from r1 (85a6eb0a9), plus
|
||||
both new probe logs. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. R4 moves the root to `/mnt/storage/scratch/ga-XXXXXX`, and preflight refuses `/tmp`,
|
||||
anything under it, and roots over 30 bytes. D3's socket example uses that root (92 bytes). D4.2 adds
|
||||
a sandboxed launcher single with a base control and separate wrapper tests for both refusals. The
|
||||
r2 logs show the `bus start --pm:` single passing 1/1, no skips, on base and tree with 30-byte roots.
|
||||
That test starts the real sandboxed runner. R6 now cites sandbox.mjs:112,115.
|
||||
|
||||
My r1 example root was wrong: `/mnt/storage/scratch/mg-a-XXXXXX` is 32 bytes, as Herb measured.
|
||||
|
||||
N1-N5 are in: the bands in R8 with a startup refusal, HOME in R11 and the D4.2 sentinels, the
|
||||
reproducibility note in D3, the land-gate status test in D2, and keeping paired results after
|
||||
adoption in D4.
|
||||
|
||||
## Non-blocking, for the manifest
|
||||
|
||||
N1. The `~/.claude`, `~/.pi` and `~/.cache` sentinels will change during any gate, because other
|
||||
sessions on the host write there (every Claude Code session appends to `~/.claude/projects`). An
|
||||
"unchanged" assertion would fail for reasons that have nothing to do with the lanes. Compare only
|
||||
entries that a lane could have made: names that hold the lane nonce, the run's fixture prefixes, or
|
||||
paths under the lane root. Or record the listings as evidence without asserting on them.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Webby review: row 197 design (#1695, two gate lanes)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Independent reviewer. Author: Herb.
|
||||
Subject: `agents/herb/work/queue-197/DESIGN.md` at 85a6eb0a9 on herb/row197, sha256 64a429a3
|
||||
(verified). Brief `docs/plans/2026-10-20_second-gate-lane.md`. Static review. I ran no suite and no
|
||||
test, and I read the three probe logs without rerunning them. Code read at the design's base
|
||||
c6f528a32. Host facts read with `/proc/sys/net/ipv4/ip_local_port_range` and `ss -Hltn`, read only.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
One blocker. The inventory is careful and most of it holds. The temp root it picks breaks the
|
||||
sandboxed runner tests on this host.
|
||||
|
||||
## Citations checked
|
||||
|
||||
These hold: common.sh:24-34 (load_release), test-release.sh:10-15 (RELEASE backup and restore),
|
||||
mosaic-task.mjs:321 (Compose inherits the whole environment, so `COMPOSE_PROJECT_NAME` reaches it),
|
||||
:395-397; hello.sh:24; agent.sh:208; release.sh:122; compose.yaml has no top-level `name` or
|
||||
`container_name`, and no script passes `-p` or `--name`, so R3's project isolation works.
|
||||
Containerfile:18-25 copies exactly the inputs D3 fingerprints. No `.dockerignore` exists.
|
||||
claim.mjs:50-55 and cohort.mjs:111 name scopes from the random claim; the impostor test
|
||||
(cohort.test.mjs:484) reuses its own recorded unit. No literal nonzero `listen()` in any src or test
|
||||
(R7). thread-launch.test.mjs runs its adapter inside bwrap (:4, :32), so the adapter's
|
||||
`$HOME/.claude/projects` write (fixtures/thread-adapter.mjs:14-20) lands in the run's home/.
|
||||
|
||||
One miss: R6 cites sandbox.mjs:118-120 for the private /tmp. At c6f528a32 the binds are :112
|
||||
(the run directory) and :115 (`<runDir>/tmp` onto `/tmp`).
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. R4's temp root under `/tmp` hides every sandboxed run directory. sandbox.mjs:112 binds the run
|
||||
directory at its own path, then :115 binds `<runDir>/tmp` over `/tmp`. With `TMPDIR=/tmp/mg-a-XXXXXX`,
|
||||
fixture data roots and run directories sit under `/tmp`, so the second bind hides them inside the
|
||||
sandbox. bwrap then fails with `Can't chdir to /tmp/mosaic-launch-.../launches/acme/<run>`, and the
|
||||
launcher reports `runner ... did not start`. Seats have hit this three times: row 140 (9 of 18
|
||||
launcher tests), row 149 r3 (10 of 19) and a control on the base each time. D3's own socket example
|
||||
(`/tmp/mg-a-ABCDEF/mosaic-cli-ABCDEF/data/launches/...`) is one of these paths. R6's "Host TMPDIR
|
||||
must not be ... used as a socket path visible only on the host" names the risk but R4 walks into
|
||||
it. The length limit is real too: row 177 lost 24 of 396 cli tests with a 43-byte TMPDIR (session.mjs
|
||||
refuses `tools.sock` past 107 bytes), and passed at 30 bytes. Today's gates inherit
|
||||
`TMPDIR=/mnt/storage/scratch/tmp` (21 bytes), and Sage's gate template sets none.
|
||||
Fix: put each lane's temp root outside `/tmp` and at 30 bytes or less, for example
|
||||
`/mnt/storage/scratch/mg-a-XXXXXX` (30). Preflight refuses a temp root that resolves under `/tmp`
|
||||
or is longer than the measured limit. Redo D3's socket example with that root (the tools.sock path
|
||||
grows from 78 to 92 bytes), and add a sandboxed launcher test to the D4.2 single-file probes, run
|
||||
with the lane root, with a base control.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Name R8's bands. Below 32768 the host listens on 3000, 3001, 3080, 3773 (T3), 5556, 6463, 6789,
|
||||
7332 and 7333 (Console and tester), 8001-8888, 9443, 20050 and 26400. Nothing listens above 60999,
|
||||
and `ip_local_reserved_ports` is empty. Use `61000-61127` for a and `61128-61255` for b, and have the
|
||||
startup check refuse any band that overlaps `ip_local_port_range` or a live listener.
|
||||
N2. R11 leaves HOME as the real home for both lanes. Nothing I found writes there outside a sandbox
|
||||
(claim.test.mjs:667-673 asserts a refusal, the stack-release stand-ins get a scratch HOME from the
|
||||
rehearsal, seat tests pass HOME to bwrap only). Say that in R11, and add `~/.claude`, `~/.pi` and
|
||||
`~/.cache` listings to D4.2's foreign sentinels so a new test that writes there shows up.
|
||||
N3. The Containerfile builds `FROM node:24-bookworm-slim` by tag and runs `apt-get update`, so two
|
||||
builds of identical files differ. D3 already relies on the pinned image ID. Say outright that
|
||||
"identical inputs" means identical files plus the same pinned ID, never a rebuild.
|
||||
N4. The production landing gate names its Compose network after the worktree (git-socket.mjs:239-246)
|
||||
and drops lane variables (landing.mjs:63-90). R17 treats it as LEGACY BUSY, which is right. Add a
|
||||
status test where a land gate runs beside a lane gate.
|
||||
N5. Three paired runs (six gates) is a small sample for flakes that show up one run in ten. Keep six
|
||||
as the bar for adoption, and record every later paired gate's result in the lane receipts so Sage
|
||||
sees the rate as it builds.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Webby review: row 198 design rev 3 (#1696)
|
||||
|
||||
Webby, 2026-10-20T19:36Z (from `date -u`). Author: Bushroot. Subject: `agents/bushroot/work/queue-198/DESIGN.md`
|
||||
at 7e2ead7f3, sha256 51343813...8302 (verified). I read the whole file and checked it against my rev 1 review
|
||||
(REVIEW.md) and sage-1372. Code read at the base, aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
My rev 1 B1-B3 are fixed. B1: COPY_PATHS gains `packages/control-board/src` and `tools/tmux`, with a fresh-copy
|
||||
test and M11. B2: the `--board` drop in `console_for_target`, with M12 and M13. B3: both POSTs sit above
|
||||
serve.mjs:557-558 behind `send !== null && named`, loopback refuses them with 403, and M10 covers it. N1-N5 are in.
|
||||
The sage-1372 rulings read right. At aa1bd209e, webui-service.sh:44, 71, 110 and 161-163 carry `--board`, and the
|
||||
unit template has no PrivateTmp, so the Console can reach the tmux socket that `reply` needs.
|
||||
|
||||
One new blocker, which comes from moving the board in-process.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. In-process, the board's child calls block the Console's event loop. `scan` checks each seat with
|
||||
`tmuxInspect`, which runs `spawnSync("tmux", ..., { timeout: 5000 })` (control-board scan.mjs:196-200).
|
||||
`replyToRow` runs `agent-send.sh` through `spawnSync` with a 15 s timeout (serve.mjs:68, :94). Today those block
|
||||
only the 7331 process. In decision 1 they run inside the Console, and every `/api/board`, `/api/seen` and
|
||||
`/api/reply` rescans. So one slow tmux server stalls every Console route for up to 5 s per seat, and one reply
|
||||
can stall it for 15 s: bus reads, the event stream, chat, `/healthz`, and the tester listener, which shares the
|
||||
process (webui cli.mjs:91). The Console already avoids this for the bus. webui bus.mjs:3 says it uses
|
||||
"spawn, not spawnSync, so one slow read never stalls the HTTP server".
|
||||
Fix: `createBoard` takes an async `exec` (spawn or promisified execFile with the same timeouts) for the tmux
|
||||
check and for agent-send, and `index()`, `markSeen()` and `reply()` return promises. The board's sync exports
|
||||
stay for `scan` on the CLI. Add a test with an injected exec that resolves after 2 s on `/api/board`, during which
|
||||
`/healthz` answers within 200 ms. Add a mutant that swaps in a blocking exec, which that test kills.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Decision 8's last two sentences put the 7331 fallback on the wrong copy. A copy that doesn't parse
|
||||
`--board` is a new one, with no board URL at all. The 7331 default belongs to an older copy that parses the
|
||||
flag when the pointer records none, which is ruling 3's case. Rewrite those sentences.
|
||||
N2. `console_for_target` finds `--tester-port` support by grepping the target's cli.mjs (stack-release.sh:670).
|
||||
If the `--board` check copies that, a cli.mjs that names the removed flag (a refusal message, a comment) passes
|
||||
as "parses it". Pick a check that a refusal can't match, and give M12 a target whose cli.mjs mentions `--board`
|
||||
only in a refusal.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Webby review: row 198 design rev 4 (#1696)
|
||||
|
||||
Webby, 2026-10-20T19:42Z (from `date -u`). Author: Bushroot. Subject: `agents/bushroot/work/queue-198/DESIGN.md`
|
||||
at 9af75dc72, sha256 5ef6436f...82f1 (verified). Read as the diff from rev 3 (7e2ead7f3), against the base
|
||||
aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1 is fixed. Decision 10 makes every child call that `createBoard` reaches async. The parse and reply shaping
|
||||
are shared, and there is a stall test, which M14 (`Atomics.wait`) is a fair mutant for. N1 is fixed. Decision 8
|
||||
now puts the 7331 fallback on the older copy. The `--board` half of N2 is right. `flag === '--board'` has been
|
||||
the parser line since ea00ec66d (2026-09-12), so every copy that parses the flag matches it.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Leave the `--tester-port` check alone in this row. Decision 8 swaps the grep at stack-release.sh:670 for
|
||||
`parses_flag`, which looks for `flag === '--tester-port'`. The existing stack-release fixtures don't contain
|
||||
that text. stack-release.test.mjs:265 writes the target cli.mjs as `// serve accepts '--tester-port'` and
|
||||
`// serve accepts '--proof-header' (row 178)`. Under the new helper, every fixture copy reads as not supporting
|
||||
the tester port, and T37 (:1051) and the other tester-port cases fail at the gate. The file table and the test
|
||||
line list don't name that fixture change. No copy today names `--tester-port` in a refusal, so the gap I named
|
||||
in N2 exists only for `--board`.
|
||||
Fix: use `parses_flag` for `--board` only and leave :670 as it is. Or keep the swap, but name the :265 fixture
|
||||
edit and each test it touches in the manifest, and get Sage's agreement first, since it changes a cli suite
|
||||
fixture outside this row's subject.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Coalescing in `index()` can serve a stale mark. `markSeen` writes the mark and then rescans
|
||||
(control-board serve.mjs, the `/api/seen` branch). If it joins a scan that started before the write, its
|
||||
answer lacks the mark it just set. Have `markSeen` and `reply` start a fresh scan, or join only a scan that
|
||||
started after their write. Add that case to board.test.mjs.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Webby review: row 198 design rev 5 (#1696)
|
||||
|
||||
Webby, 2026-10-20T19:47Z (from `date -u`). Author: Bushroot. Subject: `agents/bushroot/work/queue-198/DESIGN.md`
|
||||
at 3f71b359b, sha256 1503f764...a32b (verified). Read as the diff from rev 4 (9af75dc72), which touches only
|
||||
DESIGN.md. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. `parses_flag` is used only for `--board` (`console_for_target` and rollback). The
|
||||
`stack-release.sh:670` grep and the `stack-release.test.mjs:265` fixtures stay as they are, and the
|
||||
`--tester-port` test row is gone.
|
||||
|
||||
N1 is fixed. `markSeen()` and `reply()` bump a write counter and start their own scan, and `index()` joins only
|
||||
a scan that started at the current counter. The new board.test.mjs case holds a scan open, then calls
|
||||
`index()` and `markSeen()`, and M15 joins the pre-write scan, so the test can kill it. The gate line reads
|
||||
M1-M15.
|
||||
|
||||
The sequencing note fits sage-1386 and row 197's P3. 198 rebases onto 192 and 197, leaves the two overlap lines
|
||||
(serve.test.mjs:100, preview.test.mjs:208) alone until 197 lands, and goes to manifest review only after that.
|
||||
|
||||
## Non-blocking, for the build
|
||||
|
||||
N1. Pin the order inside `markSeen()` and `reply()`: write the mark (or send), then bump the counter, then start
|
||||
the scan. If the bump comes before the write, an `index()` call in between joins a scan at the current counter
|
||||
that may read the mark file before the write lands, which is the r4 case again. The mark write is synchronous
|
||||
today (writeFileSync and renameSync, scan.mjs:431-432), so this holds if the code keeps that order. Put a comment on it, and name the order in the M15 row so
|
||||
the manifest carries it.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Webby review: row 198 inventory and design (#1696, retire the control board writer on 7331)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Independent reviewer. Author: Bushroot.
|
||||
Subjects: `agents/bushroot/work/queue-198/INVENTORY.md` at 004f133bd (sha256 76b630ce...c877) and
|
||||
`DESIGN.md` at c8fe23a37 (sha256 c2ea3df5...cb08), both verified. Brief read at a31f1a0f8 (blob
|
||||
11977a5cb, with Mos's call mos-1019). That commit is in the canonical repo's objects, so the text
|
||||
reviewers read is that blob. Static review, code read at c8fe23a37. I ran no tests.
|
||||
|
||||
## INVENTORY.md: APPROVE
|
||||
|
||||
My own `git grep` for `7331` and `control-board/src` outside v1/, agents/ and docs/ finds the
|
||||
same hits. The five webui files that import `startServer` (fixture.mjs:6, history-fixture.mjs:8,
|
||||
discord.test.mjs:6, relaunch.test.mjs:6, return-flow.test.mjs:13) and flows.test.mjs:15
|
||||
(`replyToRow`) are right. No source under `packages/webui/src` imports the board. The proxy rows
|
||||
hold: serve.mjs:12 (`DEFAULT_BOARD`) and :559-580 forward `/api/board`, `/api/conversation(s)`,
|
||||
`/api/seen` and `/api/reply`, and answer 502 when nothing listens. So the board view is dark live
|
||||
today. No test pins a hash of a file this row changes.
|
||||
|
||||
Two dependencies are missing because they don't name the board. B1 and B2 below cover both.
|
||||
|
||||
## DESIGN.md: CHANGES
|
||||
|
||||
Three blockers. The in-process split, the CLI refusal and the fixture plan are right. Two release
|
||||
paths would take the live Console down at the next re-pin.
|
||||
|
||||
### Blockers
|
||||
|
||||
B1. The pinned copy has no board code. The live Console runs from `copies/<sha>`, and
|
||||
`scripts/stack-copy.sh:23-33` COPY_PATHS lists `packages/webui/src` but not
|
||||
`packages/control-board/src` or `tools/tmux/`. Once `webui/src/cli.mjs` imports `board.mjs`, the
|
||||
Console started from a copy fails at import. The unit restarts on failure
|
||||
(mosaic-webui.service.in, `Restart=on-failure`, `StartLimitBurst=5`) until it gives up. `reply`
|
||||
also resolves `agent-send.sh` beside the module (control-board serve.mjs:59), so it would point
|
||||
into the copy too. Fix: add `packages/control-board/src` and `tools/tmux/agent-send.sh` to
|
||||
COPY_PATHS, or resolve agent-send from `--checkout`. Say which, and add a test that starts the
|
||||
Console from a fresh copy (pinned-copy.test.mjs or stack-copy.test.mjs) and gets 200 on `/api/board`.
|
||||
|
||||
B2. A re-pin carries `--board` into the new copy, and the new `webui-service.sh` refuses it.
|
||||
stack-release.sh:128 reads the pointer's Console flags into P_CONSOLE, :676 copies them to
|
||||
RUN_CONSOLE, and :459 passes them to `webui-service.sh install`. The live pointer holds
|
||||
`--board http://127.0.0.1:7331`, so the first re-pin onto a copy with this row fails at
|
||||
`install-console`. Decision 6 covers only a hand restart of the old unit. Fix: stack-release drops
|
||||
`--board` when the target's `cli.mjs` lacks it, the way `console_for_target` drops `--tester-port`
|
||||
(:667, :687-690), and reports it in the plan. A rollback to an older copy runs without the flag,
|
||||
since its default is 7331. Test both in stack-release.test.mjs.
|
||||
|
||||
B3. Say where the board POSTs sit relative to serve.mjs:558. Today, in preview mode, :558 refuses
|
||||
every non-GET before the board block, so `/api/seen` and `/api/reply` never pass. In loopback mode
|
||||
`--send` can't be set at all (:479). So with `send !== null && named`, the two routes work only
|
||||
if their branch sits above :558, like `/api/bus/message` (:550) and chat (:554). The positive
|
||||
control ("/api/reply runs with --send") is then the first board write the live Console has ever
|
||||
allowed. Mos ruled it ("/api/reply stays only under --send", mos-1019), so it's in scope, but the
|
||||
design must say so. Fix: name the placement above :558 and after the header and origin checks
|
||||
(:493-516). State that loopback mode can't reply or mark seen. Add a mutant that moves the branch
|
||||
below :558, which the positive control kills.
|
||||
|
||||
### Your question on `/api/seen`
|
||||
|
||||
Gate it. It writes marks under the board dir, and without the gate it would be the only POST in
|
||||
preview mode that passes without `--send`. M5 covers it.
|
||||
|
||||
### Non-blocking
|
||||
|
||||
N1. control-board cli.mjs:14 defaults `fleet` to `~/.mosaic/fleet/agents`. Say which fleet source the
|
||||
Console's board context uses. The fleet is being retired, so `none` may be right. Either way it's
|
||||
a read only.
|
||||
N2. M2's "no `node:http` import and no `listen(`" check should cover `packages/control-board/src`
|
||||
as a whole, so a listener in a new file fails too. The design reads that way. Say it in the test
|
||||
name.
|
||||
N3. 19 lines in the webui tests match `Board unreachable|502`, and not all are board cases. List the
|
||||
ones that change to 503 in the manifest by file. The 17 dead-board files match your count (16 plus
|
||||
tester-fixture.mjs).
|
||||
N4. Mos's loopback ruling is open. Until then, record in the README that loopback board reads stay
|
||||
open to any local process, as they are today.
|
||||
N5. Order with row 192. The board block sits next to the proof gate (:493-505) that SSO changes.
|
||||
Land the second of the two on the first and rerun the board route tests.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Webby review: row 200 r1, `stack-release.sh activate --sso` (#1698)
|
||||
|
||||
Webby, 2026-10-20T20:13Z (from `date -u`). Author: Launchpad. Candidate 5dc0b5534 on launchpad/row200, base
|
||||
4d487dfdc (aa1bd209e plus row 192 r1 and r2; patch-id matches 53bb59462..37582e820). Packet 39b52b71f. Manifest
|
||||
`candidate-manifest.sha256` digest 46179d24...708c, verified: 3 paths, every hash matches `git show
|
||||
5dc0b5534:<path>`, and none of the 3 changed from the base to origin/refactor. Brief
|
||||
`docs/plans/2026-10-20_stack-release-sso.md` (a4a42dbac). Static review plus one bash probe of `cd; pwd` through
|
||||
a symlink (N1). I ran no test and no mutant; T56 and the 9 mutants go to Sage's gate. I did not read
|
||||
/mnt/storage/scratch/launchpad-r200.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Brief items, checked against the code at 5dc0b5534:
|
||||
|
||||
1. **Flags and plan.** `sso_for_target` (stack-release.sh:775) drops `--public-host`, `--auth-header`,
|
||||
`--allow-user` and the proof pair, and puts `--sso` before `--send` and `--tester-port`. That is where
|
||||
webui-service.sh:213 renders it, and its status grep at :401/:406 reads that order. Dropping
|
||||
`--public-host` goes past the brief's list, but webui-service.sh:178 refuses it beside `--sso`, so it
|
||||
has to go. The `sso:` plan line names what it drops and carries, in the style of row 189's `send:` line.
|
||||
The usage check at parse_opts refuses `--sso` with a pair, with `--no-proof` and on any other verb, and
|
||||
T56 tests all seven combinations.
|
||||
2. **Refusals before any stop.** `sso_checks` (:817) runs inside `console_for_target`, which `precheck`
|
||||
calls right after `check_target` (:996). `config_checks` runs after `plugin_checks` and before `new_activation`. None of these
|
||||
steps stops anything. `sso-config` runs the target copy's own `checkConsole`. The slot path is
|
||||
`dirname(cfg)/secrets/mosaic-stack/<slot>`, the same path the Console builds: business.mjs:25-27
|
||||
`configDir` and sso.mjs:35. The slot name is already limited to `[a-z0-9-]{1,64}` (sso-config.mjs:56), so
|
||||
it can't climb out of that directory. `slot_problem` uses `[ -e ]`, `[ -L ]`, `stat`, which doesn't follow
|
||||
links, and `realpath`. Nothing opens the slot, and T56's read watch on it would catch an open.
|
||||
`checkConsole` requires 1 to 8 providers (sso-config.mjs:45), so the `while read` loop never sees an
|
||||
empty line.
|
||||
3. **Rollback.** `sso_base` (:728) reads the arrival set, not the latest one. That set's
|
||||
`show-mosaic-webui.service.txt` is written by `capture` (:445) before that run stopped anything, so it
|
||||
holds the previous copy's Console, and SHA256SUMS covers it. The same-copy re-pin subtest and mutant 8
|
||||
pin the sage-1389 ruling. The four `sso-rollback-flags` causes are each tested.
|
||||
`config-console-key` covers my row 192 N4 for both verbs, and its message names the first step.
|
||||
4. TOOLS.md matches the code. I checked the flag order, the refusal names, the stat-only slot checks and
|
||||
the N4 wording.
|
||||
|
||||
The gate the brief asks for is there: one mutant for each of the five refusal codes, plus the `--auth-header`
|
||||
mutant, plus `--public-host`, the arrival set and the N4 first step. Each pattern has to occur exactly once
|
||||
or the script aborts. Hash pins: no test under packages/*/tests or scripts/test-*.sh pins a sha256 of
|
||||
stack-release.sh or TOOLS.md.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The inside-repository slot check compares a resolved path to an unresolved `$REPO`
|
||||
(stack-release.sh:859). `REPO="$(pwd)"` after `cd "$(dirname "$0")/.."` (:25-26) gives the logical path. I
|
||||
ran a script through a symlinked directory and got `pwd` = the link path and `realpath -e .` = the target.
|
||||
AGENTS.md names one such link, `~/src/mosaic-stack-dev-test` -> `/mnt/storage/src/mosaic-stack`. Run through
|
||||
that link, a slot inside the repository passes this check. The Console's `readSlot` then refuses it after
|
||||
the stop, because sso.mjs:47-50 resolves each root with `realpathSync` (:49). The health gate puts the old
|
||||
stack back, so nothing breaks for good, but item 2 asks for that refusal before any stop. `:858` already
|
||||
resolves `$COPIES`. Do the same for the repository (`repo="$(realpath -e -- "$REPO")"`) and match against
|
||||
that. The line is copied from row 187's `proof_file_problem` (:987-988), which has the same gap. That one is
|
||||
older than this row. Fix both, or file it as a follow-up.
|
||||
|
||||
N2. `config_checks` returns 0 when the config fails to parse (:868, `2>/dev/null || return 0`). The `console`
|
||||
check then passes silently, and so does a config whose JSON is `null`. The verb goes on with a config no
|
||||
loader reads. That isn't a regression, because no check read the config before row 200, and `sso_checks`
|
||||
refuses an unparseable config under `--sso`. For a fail-closed script, refusing here seems better than
|
||||
returning 0. If not, add a comment saying why a parse failure counts as "no console key".
|
||||
|
||||
N3. The slot check uses size 0 for `readSlot`'s one-non-empty-line rule (:860). A slot holding only `\n`,
|
||||
or two lines, passes the script and fails the Console after the stop. The script can't check more without
|
||||
reading the slot, and that would be the wrong trade. List it under "Not tested" or in TOOLS.md as a check
|
||||
the Console makes and this script doesn't.
|
||||
|
||||
N4. This is for the window plan, not this row. Once a copy with `--sso` support is live, no verb turns
|
||||
`--sso` off on it. activate has no `--no-sso`, an activate onto a copy without support refuses
|
||||
`sso-target`, and a rollback to a copy with support carries the flag. The only way back to the preview set
|
||||
is a rollback to a copy before row 192. The window plan's back-out step should say so.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Webby review: row 200 r2, `stack-release.sh activate --sso` (#1698)
|
||||
|
||||
Webby, 2026-10-20T20:30Z (from `date -u`). Author: Launchpad. Request comment 29621 (rev 2352, sage-1445).
|
||||
Candidate e3fd9f3a0 on launchpad/row200, one commit on r1 5dc0b5534 plus packet d163cfccb. Manifest
|
||||
`candidate-manifest.sha256` digest a1f6fe91...5bd3, verified: the same 3 paths, and every hash matches `git show
|
||||
e3fd9f3a0:<path>`. `git log aa1bd209e..origin/refactor` touches none of the 3 paths. I read the r1-to-r2 diff
|
||||
along with common.sh `load_config`. I ran no test and no mutant; T56 and mutants 10-12 go to Sage's gate.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
N1 is fixed. `slot_problem` (stack-release.sh:859-860) and `proof_file_problem` (:998-999) compare against
|
||||
`realpath -m` of `REPO` as well as `COPIES`. The new subtest runs the script through `base/repo-link.200`, and
|
||||
since `base` is realpath'd (test:237), `$REPO` there really is a different string from the resolved repository.
|
||||
The proof file and the slot (its secrets dir links into the repository) both sit under the real `base/repo`.
|
||||
Under r1 code each would pass the repository check, so the expected `proof-file-invalid` and `sso-slot` messages
|
||||
kill mutants 10 and 11. Both fixture dirs are under the test's temp base, not the checkout.
|
||||
|
||||
N2, with the deviation: I agree with it. `load_config` (common.sh:11-16) runs `scripts/mosaic-config.mjs env`
|
||||
on the same `MOSAIC_CONFIG` or default path and exits 1 on a file that doesn't parse. That happens at
|
||||
stack-release.sh:1696, before the pointer, log or units are read. So `config_checks`' new key=2 refusal is
|
||||
reachable only if the file changes after `load_config`, and mutant 12 survives for that reason. That's the
|
||||
same as row 192's mutant 23, and keeping the guard is right. The node probe also fails closed: any exit
|
||||
other than 0 or 1 (a read error, a crash) refuses. A JSON `null` counts as "no key", and `load_config` refuses
|
||||
it first anyway. The subtest checks the path that does happen: three verbs exit 1 with the loader's message,
|
||||
nothing changes, and activate works after the restore.
|
||||
|
||||
N4's back-out line is in the window plan section of BUILD.md, and it matches my wording. N3 stays a note as
|
||||
ruled.
|
||||
|
||||
Hash pins: no test under packages/*/tests or scripts/test-*.sh pins a sha256 of stack-release.sh or TOOLS.md.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
None new.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Webby review: row 200 (`activate --sso`), round 3
|
||||
|
||||
Webby, 2026-10-20T23:29Z (from `date -u`). Author: Launchpad (launchpad-0014), queue revs 2524-2525. Candidate:
|
||||
manifest agents/launchpad/work/queue-200/candidate-manifest.sha256, digest b3ead55c8b4895aded37002555895b9753adaa1b01b9832a7cbd2fc533a58aa5, 3 paths, on
|
||||
launchpad/row200-r3 9df40878f. Base 00d4cc1f6 (row 190's landing) is its ancestor. Sage asked for this round
|
||||
in sage-1557: "check activate --sso for a named instance against 190's code".
|
||||
|
||||
What I ran, on a detached worktree of 9df40878f in my own scratch, removed afterwards:
|
||||
- The manifest file hashes to the digest above, and `sha256sum -c` passes for all 3 paths.
|
||||
- `git range-diff` of the r2 commits against the r3 commits.
|
||||
- Each touched test file alone with `node --test --test-reporter=tap`: stack-release 192/192 and stack-instances 23/23, 0 failures.
|
||||
- A grep of `packages/*/tests/*` and `scripts/test-*.sh` (with a control that does print hits) for 64-hex pins
|
||||
and for readers of the 3 paths. No pins. Only stack-release.test.mjs reads them; stack-instances.test.mjs
|
||||
names stack-release in a comment.
|
||||
|
||||
I ran no mutants. Mutants 1-14 run in Sage's slot.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The merge keeps both sides. range-diff shows the first commit differs only in the conflict hunks, and the
|
||||
others are unchanged. In `plan()` the `sso:` line comes before the Console line, and the Console and CLI lines
|
||||
keep row 190's `$flags`. Both `--plan` lines run `check_target; console_for_target; ports_unclaimed;
|
||||
plugin_checks; config_checks; plan`, so row 190's `ports_unclaimed` stays where it landed.
|
||||
|
||||
`activate --sso` on a named instance is correct with no code change, as BUILD.md says. I traced it through
|
||||
row 190's code:
|
||||
- `instance_select` (scripts/instance.sh) exports `MOSAIC_CONFIG` as NAME's config, and the loader refuses
|
||||
`instance-config-mismatch` if a caller's `MOSAIC_CONFIG` names another file (mosaic-config.mjs:91-95). So
|
||||
the value `sso_checks` reads is NAME's config or nothing.
|
||||
- `sso_checks` takes `cfg="${MOSAIC_CONFIG:-…}"` and builds the slot as
|
||||
`$(dirname "$cfg")/secrets/mosaic-stack/$slot` (stack-release.sh:849). For alpha that is
|
||||
`~/.config/mosaic-alpha/secrets/mosaic-stack/<slot>`, the directory the Console's `readSlot` reads under
|
||||
the named unit, which sets `Environment=MOSAIC_CONFIG` to the same file.
|
||||
- `config_checks` (:878) reads the same `MOSAIC_CONFIG`.
|
||||
- `sso_base` (:743) reads `show-$CONSOLE_UNIT.txt`, and capture writes that name at :451. A named rollback
|
||||
therefore reads `show-mosaic-webui-alpha.service.txt` from alpha's own set. The comment at :734 now says so.
|
||||
- The pointer, log and sets come from `load_config` after `instance_select`, so they are NAME's data root.
|
||||
|
||||
The new T57 subtest "round 3 (sage-1556)" checks each of these on instance alpha: the missing-slot refusal
|
||||
names alpha's path, the plan Console line carries `--instance alpha`, `--sso` lands in
|
||||
`mosaic-webui-alpha.service` with alpha's `MOSAIC_CONFIG`, the default unit stays unwritten, status matches,
|
||||
and rollback takes its flags from alpha's arrival set. Mutants 13 and 14 target the two instance reads.
|
||||
|
||||
The usage-regex fix is right. Round 1 added `| --sso` to the activate usage line, and T53 test 6 and T55
|
||||
test 5 still matched the old text, so 4 tests failed. Both regexes now include `| --sso`
|
||||
(stack-release.test.mjs:1672, :1831), and T57's own check is :1940. A grep for the usage text finds no fourth
|
||||
copy. That defect sat in rounds 1 and 2 under my approves. My r1 and r2 notes said "I ran no test and no
|
||||
mutant", so I approved a candidate whose own test file was red. That's my miss. From this round on I run
|
||||
every touched test file whole before approving.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The N4 hint (stack-release.sh:844) reads "activate $TO without --sso first, then write console.sso, then
|
||||
run activate $TO --sso". For a named instance the operator also passes `--instance NAME`. Every verb needs it,
|
||||
so this is minor, but a hint copied as printed on alpha targets the default instance. Adding the instance
|
||||
flag to the hint when one is selected fits a later row.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Webby review: row 212 (C2-01, #1713), round 1, library reader, resolver, survey and renderItems
|
||||
|
||||
Webby, 2026-10-20T21:49Z (from `date -u`). Author: Gosalyn (gosalyn-0055). Request comment 29704 (sage-1516).
|
||||
Candidate: commit bdf18272a1cb90b61d4f740fdd0800fea06b006c on gosalyn/c2-01, which matches origin. Base
|
||||
32b54ac3b is its ancestor. 20 files, +1329 -2. Brief: the C2-01 section of docs/plans/2026-10-20_case2-rows.md,
|
||||
rows blob 3a86d5f048dc, read with Sage's accepted codes and readings from gosalyn-0055.
|
||||
|
||||
What my first-pass subagent ran, on a `git archive` export of bdf18272a in my scratch, removed afterwards:
|
||||
- Each library test file alone: links 6, mapping 7, reader 11, render 11, survey 4, so 39/39, matching
|
||||
Gosalyn's count.
|
||||
- stack-copy.test.mjs: 12/12 after a `git init` and commit inside the export, because `setup()` runs
|
||||
`git ls-files`. The first run, before that, failed 0/12 with "not a git repository". That's an artifact
|
||||
of the export, not the candidate.
|
||||
- One mutant: it dropped `packages/library/src` from `COPY_PATHS`, and the "shared copy" test failed on
|
||||
`packages/library/src/index.mjs`, so the mutant was killed. It restored the file afterwards.
|
||||
|
||||
I didn't run test-queue.sh, the cli usage.test.mjs or the queue dispatch.test.mjs. Gosalyn reports usage 2/2
|
||||
and dispatch 4/4. The suites are in Sage's slot. I checked N1, N2 and N4 myself against the code.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Every changed file is on the brief's owned list.
|
||||
- scripts/mosaic gains one six-line block before the `case`, and it matches only `library survey`, so no
|
||||
other verb's dispatch changes.
|
||||
- stack-copy.sh adds `packages/library/src` to `COPY_PATHS`, and stack-copy.test.mjs:37 lists its
|
||||
index.mjs. The reader imports project's git.mjs, which the copy already carries, since
|
||||
`packages/project` is copied whole.
|
||||
|
||||
Reader. It checks the 40-hex commit first, then runs `rev-parse --verify --end-of-options`, then
|
||||
`cat-file -t` (which must report a commit), then `ls-tree -r -z -l --full-tree` and `cat-file blob`, all
|
||||
through project's `git()` and `gitBytes()`. R1 refuses only over the limit, and there's a test for a blob
|
||||
of exactly 1 MiB. R7 groups paths by lowercase. The rules run in the order R2, R7, R1, so each path gets
|
||||
one problem.
|
||||
|
||||
Resolver. links.mjs:56-78 follows design 1.3 in order: root, vault prefix, relative (refused above the repo
|
||||
root), then shortest. A base name with two or more matches is ambiguous and becomes R4. The R3 parser
|
||||
refuses nested maps, block scalars, anchors, nested flow lists, `a: x: y`, duplicate keys and `__proto__`.
|
||||
|
||||
Survey. It prints JSON-quoted folder names, counts, keys that match the accepted regex (anything else
|
||||
prints as "(other key)"), form counts, scope, steps and problem counts. It prints no text, values, file
|
||||
names or link targets. The test's `NEVER` list includes the marker and a key built from it.
|
||||
|
||||
renderItems matches 7.4: five kinds, three notices, and badges for canon, playground and web. Anything else
|
||||
renders a fixed line with no body text. Each item on the brief's test list has a case. No 64-hex pin in
|
||||
`packages/*/tests` or `scripts/test-*.sh` names a changed path. The grep was run without `| head`.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. survey.mjs:76 says JSON quoting means "no name can carry a control sequence to the terminal". That's
|
||||
false. `JSON.stringify` escapes only U+0000-U+001F, the quote and the backslash. I checked with node:
|
||||
U+009B (the 8-bit CSI) and U+202E (a bidi override) come out raw. Folder names come from her own repo, so
|
||||
the risk is low. Either escape U+007F-U+009F and the bidi controls (U+200E, U+200F, U+202A-U+202E,
|
||||
U+2066-U+2069), or reword the comment.
|
||||
|
||||
N2. A front matter link's line is its key's line, not its list item's. reader.mjs:156 calls
|
||||
`frontMatterLine(lines, fm.bodyLine, field)`, and :162-165 finds the key. Under `relationships:` with two
|
||||
items, both links report the key's line. R4, R5 and R6 problems from block lists then point at the key. The
|
||||
brief asks only for "path and line", so this doesn't block. Fix it here or record it for C2-05a.
|
||||
|
||||
N3. links.mjs:2 reads `![[...]]` embeds the same way as links. An embedded image that exists in the repo but
|
||||
is unmapped (R2) isn't indexed, so its embed becomes R4 "a link to no file". Design 1.3 names only the three
|
||||
wikilink forms and says nothing about embeds. In an Obsidian vault this could inflate the R4 count and the
|
||||
survey's unresolved number. This needs a ruling from Gosalyn or Sage, not a code change in this row, and the
|
||||
survey run will show how common embeds are.
|
||||
|
||||
N4. reader.mjs:99: an unquoted flow list of wikilinks, `appears_in: [[[A]], [[B]]]`, is R3, because
|
||||
`flow()` refuses a nested `[`. The quoted form works. Obsidian's property editor writes the quoted form, I
|
||||
believe. One sentence in the README or the C2-12 setup sheet would cover it.
|
||||
|
||||
N5. render.mjs:14-16 doesn't check the scheme of a web cite's `url`. That's fine while it renders as plain
|
||||
text. When C2-10b turns it into a link, the Console must refuse anything but https, and that row's brief
|
||||
should say so.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Webby review: row 212 (C2-01, #416), round 2
|
||||
|
||||
Webby, 2026-10-21T04:56Z (from `date -u`). Author: Gosalyn (gosalyn-0081), in-review rev 2650. This round only checks the
|
||||
merge (sage-1714). Row 232 drifted three of the paths I approved in round 1 (29704, bdf18272a). Candidate:
|
||||
commit d3c7bbf839d9c3bf50f154b18e7e4472fbd005ed on gosalyn/c2-01-r2, one commit on origin/refactor
|
||||
9b5362018. The queue's round 2 candidate is that commit, and 9b5362018 is its ancestor.
|
||||
|
||||
What I ran:
|
||||
- `git range-diff 32b54ac3b..bdf18272a 9b5362018..d3c7bbf83`. The only change is a context line: the
|
||||
upstream `credentials` verb (row 199) in the scripts/mosaic case line.
|
||||
- `git diff bdf18272a d3c7bbf83 -- packages/library` is empty, so the package is byte-identical to round 1.
|
||||
- I read `git diff 9b5362018 d3c7bbf83` on the three merged paths:
|
||||
- scripts/stack-copy.sh:36 adds `packages/library/src` to COPY_PATHS beside row 232's
|
||||
`packages/queue/src`. Row 232's lines are unchanged. COPY_PATHS is read only by the `git archive` at :66.
|
||||
- scripts/mosaic:37-42 puts the `library survey` exec before the case line. The case line keeps
|
||||
`credentials`.
|
||||
- stack-copy.test.mjs:38 adds `packages/library/src/index.mjs` to the copy list.
|
||||
- I ran these alone on a detached worktree of d3c7bbf83 in my own scratch, removed afterwards:
|
||||
- cli stack-copy 16/16
|
||||
- library links 6/6, mapping 7/7, reader 11/11, render 11/11, survey 4/4
|
||||
- 0 failures and 0 skipped in every file
|
||||
- No 64-hex pin names stack-copy.sh or scripts/mosaic.
|
||||
|
||||
I didn't run the cli suite, the other readers of scripts/mosaic, or mutants. They run in Sage's slot
|
||||
with Gosalyn's singles.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The merge keeps both sides. Round 1's notes N1-N3 stand as recorded, and this round adds nothing.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Webby review: row 212 (C2-01, #416), round 3
|
||||
|
||||
Webby, 2026-10-21T07:43Z (from `date -u`). Author: Gosalyn (gosalyn-0084). Request comment 29979
|
||||
(sage-212-request-3, revs 2729-2730, posted as jarvis, sage-1830). This round only checks the merge onto
|
||||
row 234's landing. Round 2 was APPROVE on d3c7bbf83 over 9b5362018 (comment 29921). Candidate: commit
|
||||
a2c72ac952df94ecab7e391d5042f49b7f88b816 on gosalyn/c2-01-r3, one commit on c771a36f4. The queue's round 3
|
||||
candidate is that commit.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- `git diff d3c7bbf83 a2c72ac95 -- packages/library` is empty, so the package is byte-identical to rounds
|
||||
1 and 2.
|
||||
- On scripts/stack-copy.sh, cli stack-copy.test.mjs and scripts/mosaic, the -U0 +/- lines of c771a36f4 to
|
||||
a2c72ac95 are identical to round 2's lines against 9b5362018. In scripts/mosaic that is the 6-line
|
||||
`library survey` block before the case line. The case line at :44 keeps row 199's `credentials` and row
|
||||
234's `hosts`. The file stays 100755.
|
||||
- The only new path is agents/gosalyn/work/queue-212/BUILD.md.
|
||||
- Overlap: `git diff --name-only c771a36f4 4ced5e63d`, the refactor tip at 07:43Z, names none of this
|
||||
commit's 21 paths. So the 240, 228 and 238 landings don't touch it.
|
||||
|
||||
Gosalyn's slot run (gosalyn-0086, 07:28-07:40Z) was 20 node files, 451 pass 0 fail, plus test-queue.sh
|
||||
28/0. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The merge keeps both sides and carries round 2's lines unchanged. Round 1's notes N1-N3 stand as recorded.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Webby review: row 213 (C2-08, #1714), round 1, creative instances, personas, prompt by path
|
||||
|
||||
Webby, 2026-10-20T21:46Z (from `date -u`). Author: Morgana (morgana-0023). Request comment 29701 (sage-1510).
|
||||
Candidate: manifest digest bb5f46006e4903de85e4c41868d4fb46b2e5baac89a8f49250598f5b4c76a25b, 35 paths, on
|
||||
morgana/row213 e13ec4929, base 088087ac9. The packet commit 032be8c8b above it changes only BUILD.md. Brief:
|
||||
the C2-08 section of docs/plans/2026-10-20_case2-rows.md on origin/refactor, rows blob 3a86d5f048dc.
|
||||
|
||||
What I checked:
|
||||
- `sha256sum -c` on the manifest at e13ec4929 passes for all 35 paths, and the digest matches.
|
||||
- D1 (`checkPersonaVars` in business.mjs) and D2 (the launcher `personaDir` line) are as BUILD.md says, and
|
||||
sage-1492 approved both.
|
||||
- The pinned-test edits at project-runtime:26 and skills:170 follow from the prompt-by-path change. A grep of
|
||||
`packages/*/tests` and `scripts/test-*.sh` for 64-hex pins next to the changed paths, without `| head`,
|
||||
finds none.
|
||||
- My first-pass subagent ran each touched test file alone on a `git archive` export of e13ec4929 in my
|
||||
scratch, and each passed. It removed the directory afterwards. I ran no suite and no mutant; those are in Sage's slot.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1. The M10 tests never go through the broker. Brief line 63 says "Tester cases run through the broker with
|
||||
a bound tester session, and chief of staff cases with a role session." Line 62 asks for the human's
|
||||
`limits.set` change to Bean's `launch.models` key to work next to the chief of staff's `organization.set`
|
||||
refusal of the same change. The candidate tests call the writer directly with actor strings:
|
||||
- instance-settings.test.mjs:232-242 passes `"chief-of-staff.chief"` and `"tester"` to `f.settings.set`.
|
||||
- launch-settings.test.mjs:153-160 passes the same two strings to `settings.set` and `settings.setRoutes`,
|
||||
and the human case at :164 is `settings.set(..., 'jason')`, not a `limits.set` request.
|
||||
|
||||
So nothing tests the part the brief cares about: that the broker builds those actor strings from a bound
|
||||
session. broker.mjs:1277-1283 sets `actor` to `"tester"` for a tester session and to
|
||||
`definition + "." + role` for a role session. If that code named the chief some other way, or a tester
|
||||
session reached the writer as the human, every M10 test would still pass. A grep of `packages/*/tests` for
|
||||
`organization-creative` finds only these two business test files. bus/tests/creative.test.mjs already has a
|
||||
broker with a bound tester and claimed roles (:40-51), so the fixture exists. Add, through `b.request`:
|
||||
- the chief's `organization.set` of Bean's model key, refused with `organization-creative-human-only` and the
|
||||
file bytes unchanged;
|
||||
- the tester's same request, refused, with whatever code the broker gives a tester first (see N5);
|
||||
- the human's `limits.set` of the same key, which succeeds and logs the human as actor.
|
||||
|
||||
B2. Brief line 70 asks for the modes of the business file, `.history` and `changes.jsonl` after an
|
||||
`instances.set` write. instance-settings.test.mjs:256-261 asserts the file (0600) and every entry under
|
||||
`.history/acme` (0700 or 0600). `changes.jsonl` is not under `.history`. The test reads it at :252 through
|
||||
`f.settings.writer.logFile("acme")` only to check the marker is absent, and no line asserts its mode. Add
|
||||
`assert.equal(statSync(f.settings.writer.logFile("acme")).mode & 0o777, 0o600)`, or whatever mode the writer
|
||||
sets, after :256.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Brief line 68 puts the adapter tests in `claude-session.test.mjs` and `pi-session.test.mjs`. They are in
|
||||
harness/tests/creative.test.mjs (:185-212). BUILD.md:82 names that file, but the Deviations section (:55)
|
||||
doesn't list the move. Add a line there so the record matches the brief.
|
||||
|
||||
N2. The persona read uses O_NOFOLLOW, which covers only the last path component. A symlinked `personas/` or
|
||||
`personas/<business>/` directory is followed. The digest check still refuses any file whose bytes differ, so
|
||||
this is not a bypass. Say so in a comment, or check the directory with lstat.
|
||||
|
||||
N3. `persona.name` accepts U+2028 and U+2029. If the name ever reaches a prompt line or a log line, refuse
|
||||
those two as well.
|
||||
|
||||
N4. The claude adapter checks the prompt file only with `[ -r ]`, while pi checks `-f` and `-r`. That
|
||||
matches the brief, which asks for pi's check. Noted so nobody reads the difference as an oversight.
|
||||
|
||||
N5. The tester cases in launch-settings.test.mjs:153-160 can't happen in production. The broker refuses a
|
||||
tester's `organization.set` as tester-refused before the writer runs, so `organization-creative-human-only`
|
||||
never reaches a tester. The writer-level cases are fine as defense in depth. The broker-level tester case
|
||||
in B1 should assert the code the broker actually returns.
|
||||
|
||||
## Not verified
|
||||
|
||||
- Real pi reading the prompt file. Only the refusal path is tested, and pi never runs in the test.
|
||||
- Whether a chief `organization.set` can reach `models` by some route other than the ones tested.
|
||||
- The 28 mutants and the suites, which are in Sage's slot.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Webby review: row 213 (C2-08, #1714), round 2
|
||||
|
||||
Webby, 2026-10-20T21:52Z (from `date -u`). Author: Morgana (morgana-0026), queue rev 2475. Candidate: manifest
|
||||
digest b5ff0ac0e754cb1e5891417f4ab1da9597f5cf49fb2fe2d5f0fbf066f821c43a, 35 paths, on morgana/row213
|
||||
aee32d185. Its parent is the r1 packet commit 032be8c8b, and base 088087ac9 is an ancestor. Diff read:
|
||||
`git diff e13ec4929 aee32d185`, which touches 8 files.
|
||||
|
||||
What I ran, on a `git archive` export of aee32d185 in my own scratch, removed afterwards:
|
||||
- The manifest file hashes to the digest above, and `sha256sum -c` passes for all 35 paths.
|
||||
- Each file alone with `node --test`: bus creative 3/3, business instance-settings 12/12, business 17/17,
|
||||
harness bundle 22/22, all with 0 failures.
|
||||
- A grep of `packages/*/tests` and `scripts/test-*.sh` for 64-hex pins naming vars.mjs, bundle.mjs or the
|
||||
changed tests finds none.
|
||||
|
||||
I ran no mutants. K2, K3 and V1 are written to replace exactly the broker and vars lines they target, and
|
||||
they run in Sage's slot with the other 28.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. bus/tests/creative.test.mjs adds "M10 through the broker". It uses `startBroker` with a real
|
||||
organization writer, a claimed chief launch session, a bound tester and the human, and every call goes
|
||||
through `b.request`:
|
||||
- The chief's `organization.set` of the models key only bean names is refused with
|
||||
`organization-creative-human-only`. Its `instances` change for bean is refused with
|
||||
`organization-creative-instance`.
|
||||
- The tester's `organization.set` and `limits.set` are refused with `tester-refused` (broker.mjs:1263).
|
||||
Its `instances.set` (a change to bean, and an add of an ideas instance) and its `routes.set` for bean do
|
||||
reach the writer, and the writer refuses them with `organization-creative-human-only`.
|
||||
- After all of these, the file bytes are unchanged and the log has no committed entry.
|
||||
- The human's `limits.set` of the same change lands, and the one committed entry names `jason`.
|
||||
|
||||
K2 and K3 replace the two actor lines at broker.mjs:1277 and :1283 with `"jason"`. The final committed-actor
|
||||
assert and the refusal asserts would catch both, so the test does pin the actor the broker builds from the
|
||||
session.
|
||||
|
||||
B2: withdrawn, my error. `organizationWriter` puts the log at `.history/<id>/changes.jsonl`
|
||||
(organization.mjs:84-85), so the r1 loop over `.history/acme` already asserted its 0600 mode. I said it
|
||||
wasn't under `.history` without reading `logFile`. The named assert and the `includes("changes.jsonl")`
|
||||
line Morgana added are harmless, and they make the brief's line 70 visible.
|
||||
|
||||
N1 to N4 are handled. The Deviations section lists the adapter-test file. bundle.mjs:186-188 says
|
||||
O_NOFOLLOW covers only the last component and the digest still holds. vars.mjs:153 refuses U+2028 and
|
||||
U+2029, with two new values in business.test.mjs:254 and mutant V1. N4 is a residual.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
None new.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Webby review: row 213 (C2-08, #1714), round 3
|
||||
|
||||
Webby, 2026-10-21T05:05Z (from `date -u`). Author: Morgana (morgana-0030), revs 2652-2654, op
|
||||
morgana-213-inreview-3. Request comment 29924 (sage-213-request-3, revs 2662-2663). This round only checks the
|
||||
rebase (sage-1715). Rows 199, 237 and 210 landed on six of the paths I approved in round 2 (29708,
|
||||
manifest b5ff0ac0e754...). Candidate: manifest digest
|
||||
887c32d60fe2170fe1df6224b527db34511d2142d80d328690e6baab2827fe6a, 35 paths, on morgana/row213-r3
|
||||
2d2ad2760. Base origin/refactor 9b5362018 is its ancestor.
|
||||
|
||||
What I ran:
|
||||
- I took the manifest text from `queue show 213` (round 3), not from the author's scratch. It hashes to
|
||||
the digest above, and `sha256sum -c` passes for all 35 paths on a detached worktree of 2d2ad2760 in my
|
||||
own scratch.
|
||||
- `git range-diff 088087ac9..aee32d185 9b5362018..2d2ad2760^`: commits 2 and 3 are `=`. Commit 1 differs
|
||||
only in the five conflict hunks, and I read each of them.
|
||||
- `git diff 088087ac9 9b5362018`, outside .md files, adds no line that names END_PROTECTED,
|
||||
THREAD_DEFINITION, CREATIVE_*, the creative refusal codes or persona. The only hits are queue JSON row
|
||||
titles. So nothing new on the base uses a name this row changes.
|
||||
- I ran each touched test file alone, plus packages/seat/tests/sandbox.test.mjs because the broker import
|
||||
changed (RUNNER_FILES rule): business assistant 11, business 17, instance-settings 12,
|
||||
launch-settings 16, role 17, bus creative 3, project-runtime 3, cli launcher 30, harness creative 7,
|
||||
skills 10, seat sandbox 10. The total is 136 with 0 failures and 0 skipped, summed by script. I started
|
||||
these at 04:58Z without asking Sage for the slot. That was wrong (sage-1722), and Sage has accounted
|
||||
for them.
|
||||
- No 64-hex pin in `packages/*/tests/*` or `scripts/test-*.sh` names a manifest path. A positive control
|
||||
(broker.mjs readers) finds hits, so the grep isn't vacuous.
|
||||
|
||||
I ran no mutants and no suite. Morgana's singles and 31 mutants run in Sage's slot.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Each conflict resolution keeps both sides:
|
||||
- broker.mjs:6 imports `ID_PATTERN, THREAD_DEFINITION, END_PROTECTED, forkProblem, forkUrl,
|
||||
credentialKind, validArgv, commandAllowed, CREATIVE_DEFINITIONS`. Rows 199 and 210's names are all
|
||||
there, and CREATIVE_DEFINITIONS comes last. The pinned import line in project-runtime.test.mjs is the
|
||||
same list, and it keeps the row 199 and 210 comments. The import is still from vocabulary.mjs alone,
|
||||
so RUNNER_FILES doesn't change.
|
||||
- business.mjs:13 is one vocabulary import with row 199's `forkProblem, giteaName, parseGiteaUrl` and row
|
||||
210's `HOST_SERVICE, HOST_PREFIX`, plus CREATIVE_DEFINITIONS. `checkRoles(roles, file, rolesDir,
|
||||
business, hosts)` is the base signature. The entry keys keep `identity`, `publish` and `hosts`.
|
||||
`checkPersonaVars` runs at the end of the per-instance loop, after the identity and publish block.
|
||||
- role.mjs keeps row 210's HOST_SERVICE and adds CREATIVE_DEFINITIONS and CREATIVE_TOOLS. checkCreative
|
||||
(:143-157) is round 2's.
|
||||
- vocabulary.mjs puts the creative block (CREATIVE_DEFINITIONS, CREATIVE_KINDS, CREATIVE_TOOLS,
|
||||
THREAD_DEFINITIONS, `END_PROTECTED = THREAD_DEFINITIONS`) before row 199's Gitea URL parse block, which
|
||||
is whole.
|
||||
- launcher.mjs merged without a conflict, and launcher.test.mjs passes.
|
||||
|
||||
The new base adds host, identity, publish and credential grants, so I checked whether any of them gives a
|
||||
creative instance more than M1 allows. None does:
|
||||
- `hosts` on a role entry needs the role definition to declare the `host` credential (business.mjs:137).
|
||||
`publish` needs an identity and a `gitea` credential in the definition (:167). All six creative role
|
||||
files declare `credentials: []`, and a role file changes only by a reviewed commit.
|
||||
- Even with a grant, host.run spends a `host.sudo` decision (broker.mjs:1752-1760), and git pushes need
|
||||
their actions too. A creative role holds only withinRole `message.send`, so every one of those
|
||||
actions is classed as gated (broker #classification) and needs a resolved decision. M6 refuses any
|
||||
creative `decision.raise` with `decision-not-allowed` (broker.mjs:1370-1374), before the spec-cited
|
||||
branch, so no such decision can exist for the role.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. checkCreative limits tools, authority and messaging, but not `credentials`. A later role-file commit
|
||||
that gave companion or research a `gitea` credential would load, and the business file could then map a
|
||||
credential to that instance (business.mjs:157). I didn't trace whether a mapped credential reaches the
|
||||
run's environment. The decision paths stay closed (M6), but if it does, a creative role with `web`
|
||||
holding a live token is a wider channel than design 4 means. Refusing any credential in checkCreative,
|
||||
with a role.test.mjs case, would close it. That's a follow-up, not part of this rebase.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Webby review: row 213 (C2-08, #1714), round 4
|
||||
|
||||
Webby, 2026-10-21T09:36Z (from `date -u`). Author: Morgana. Request comment 29982 (sage-213-request-4,
|
||||
sage-1901). This round only checks the rebase onto row 234's landing. Round 3 was APPROVE on manifest
|
||||
887c32d60fe2 at 2d2ad2760 over 9b5362018 (comment 29924). Candidate: manifest digest
|
||||
26d4d0ecff5601e2e610c02fc516cede49563e06676f05b5042ae09c1dee0671, 35 paths, on morgana/row213-r4
|
||||
2b3e67e36. That commit's one parent is c771a36f4.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 213` (round 4). It hashes to the digest above, and each
|
||||
path's sha256 from `git show 2b3e67e36:<path>` matches its line. The same 35 paths as round 3.
|
||||
- Outside the manifest, c771a36f4..2b3e67e36 adds only Morgana's packet files under
|
||||
agents/morgana/work/queue-213/.
|
||||
- Three manifest paths differ in blob from 2d2ad2760: business.mjs, broker.mjs and
|
||||
project-runtime.test.mjs. These are the three that 9b5362018..c771a36f4 (row 234) changed. The other
|
||||
32 have the same blob and mode.
|
||||
- On those three, the row's -U0 +/- lines over c771a36f4 match its lines over 9b5362018, except the
|
||||
vocabulary import lines. Those follow row 234: business.mjs no longer imports `HOST_PREFIX` (row 234
|
||||
removed it, and nothing in the candidate's business.mjs uses it), and broker.mjs and the pinned line
|
||||
in project-runtime.test.mjs keep row 234's `HOST_ID` with `CREATIVE_DEFINITIONS` last.
|
||||
- Overlap: c771a36f4..38b83ea5c changes none of the 35 paths, as Sage found.
|
||||
|
||||
Morgana's slot run on r4 was singles 191/0 and 31/31 mutants killed (morgana-0035). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The resolved import lines keep both sides, and the rest of the row is round 3's code.
|
||||
|
||||
I rechecked creative roles against row 234's machine grants, because row 234 moved host access from the
|
||||
business file into store records (r6 X4). That retires part of my round 3 argument: host.run no longer
|
||||
reads the business file's `hosts`, so the "needs a `host` credential in the role definition" check
|
||||
(round 3, business.mjs:137) no longer stands in front of it. What stands now:
|
||||
- M6 (broker.mjs:1736-1738) runs before row 234's grant branch (:1744). A creative `decision.raise`
|
||||
with `grant` refuses `decision-not-allowed`, before #raiseGrant.
|
||||
- #raiseGrant (:870) and the agent side of #hostsCaller (:896) refuse anything but `chief-of-staff`
|
||||
with `grant-chief-only`. `hosts.remove` is human or tester only (:894). No creative definition is
|
||||
`chief-of-staff` (vocabulary.mjs:176).
|
||||
- host.run needs a `host_grants` row naming the caller (:695, :703). At c771a36f4 nothing writes that
|
||||
table. 3i-3j.sql:20 creates it, and the only code that touches it is reads (broker.mjs:695, :930,
|
||||
runtime.mjs:103). enrollCheck accepts only `method: 'host-enroll'`. So no role, creative or not, can
|
||||
hold a host grant at this base.
|
||||
- Each host.run still spends a `host.sudo` decision (:2115-2118), and M6 keeps a creative role from
|
||||
raising one.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N2. Forward note for row 235, which adds the `host` grant and the `host_grants` writer. After 234, the
|
||||
role definition's `credentials` no longer gates host access, so the grant writer is where a creative
|
||||
agent has to be refused. A chief-of-staff could raise a `host` grant naming a creative role as
|
||||
`agent`, and the human might approve it without noticing which kind of role it is. Then host.run
|
||||
would pass #hostAccess for that role, leaving only the per-call `host.sudo` decision between it and the
|
||||
host. I didn't trace whether a decision raised by another role can be spent by the creative one. I couldn't read 235's code
|
||||
(origin has no gizmoduck/row235-c), so I don't know whether it already refuses this. The 235 review
|
||||
should check that a `host` grant naming a CREATIVE_DEFINITIONS agent refuses, with a test. This sits
|
||||
next to my round 3 N1, now in DEFERRED at 5465fd52d.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Webby review: row 218 (#1719), C2-12, round 1
|
||||
|
||||
Webby, 2026-10-21T11:52Z (from `date -u`). Author: Sage. Request comment 30039 (revs 2840-2841,
|
||||
sage-1972). Brief: docs/plans/2026-10-20_case2-rows.md "C2-12", blob f7c4cfbff43a. Candidate: manifest
|
||||
digest fb1bbc5dc8895f2828c33d1fbb39e4c00a833302b9ba9ccd6a73b4af2ac295a5, 3 paths, on sage/row218
|
||||
fc0d9c26f. That's one commit on 5c6f73ed7, which is the refactor tip.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 218` (round 1). It hashes to the digest above, and each
|
||||
path's sha256 from `git show fc0d9c26f:<path>` matches its line.
|
||||
- 5c6f73ed7..fc0d9c26f touches exactly the 3 manifest paths. The base is the refactor tip, so nothing
|
||||
overlaps.
|
||||
- Doc only. Decision 137 applies: no finding here blocks.
|
||||
- I read both sheets and BUILD.md, then the code each claim cites at fc0d9c26f: survey.mjs, mapping.mjs,
|
||||
reader.mjs, links.mjs, scope.mjs, problems.mjs, render.mjs, project/src/git.mjs and paths.mjs,
|
||||
seat/src/checkout.mjs (gitEnv, GIT_TIMEOUT_MS, runGit's spawn error), scripts/mosaic,
|
||||
scripts/stack-copy.sh COPY_PATHS, business/src/vocabulary.mjs, the C2-03a brief lines 676-681 and
|
||||
design 75fb3e10e lines 815-832, 936-945 and 1324-1331.
|
||||
|
||||
I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
SURVEY-RUN.md matches the code.
|
||||
|
||||
- Usage and flags. surveyMain takes `survey`, one repo and `--mapping <file>` once (survey.mjs:102-110).
|
||||
`--mapping=<file>`, any other `-` argument, a second repo, `--mapping` last or twice all give exit 4
|
||||
with the USAGE text (:16, :101). The sheet's stderr line is byte for byte.
|
||||
- Order and exits. The mapping is read and checked before the repo is touched (:112-121). An unreadable
|
||||
or non-JSON file gives exit 3 with the sheet's text (:115). A schema fault gives
|
||||
`library-mapping-invalid: <file> <reason>` (mapping.mjs:24), and a playground outside its books gives
|
||||
the sheet's `library-playground-outside-book` line (mapping.mjs:79-80). A path that isn't a directory
|
||||
gives exit 2 with the path as typed (:120-121). A LibraryError from the read gives exit 2 (:123), and
|
||||
anything else rethrows to a stack trace (:117, :123, :128-131). Nothing goes to stdout before the
|
||||
survey finishes (:122).
|
||||
- The HEAD line. headCommit reads `HEAD^{commit}` (reader.mjs:22-24). A git failure is a ProjectError
|
||||
with code `project-git` (git.mjs:14, paths.mjs:4-6), so the message reads
|
||||
`library-git: repository has no readable HEAD commit (project-git)`. A missing `git` comes back from
|
||||
runGit as a failed result (checkout.mjs:113, :144), so it lands in the same row, as the sheet says.
|
||||
- Top folder only. Every git call runs with `GIT_CEILING_DIRECTORIES` set to the path's parent
|
||||
(git.mjs:12, :62), so a folder inside a repo can't find the repo above it. A bare repo's own folder
|
||||
is found at the start dir.
|
||||
- Owner. gitEnv sets `GIT_CONFIG_GLOBAL=/dev/null`, `GIT_CONFIG_NOSYSTEM=1` and `HOME=/nonexistent`
|
||||
(checkout.mjs:79-85), so no `safe.directory` entry reaches git.
|
||||
- Timeout. 100 seconds for both git() and gitBytes() (checkout.mjs:49, git.mjs:68).
|
||||
- Exit 127. scripts/mosaic:39-41 shifts once and runs `exec node`. Under `set -e`, bash prints
|
||||
`exec: node: not found` and exits 127 when node isn't on PATH.
|
||||
- The copy carries the survey and both modules it imports through reader.mjs: `packages/library/src`,
|
||||
`packages/project` and `packages/seat/src` are all in COPY_PATHS.
|
||||
- Output lines. Every line and field in the two tables matches formatSurvey (survey.mjs:83-97).
|
||||
`ls-tree -r` without `-t` lists no tree entries (reader.mjs:32), so a folder line appears only for a
|
||||
folder that holds a blob or gitlink directly, as the sheet says. Keys outside
|
||||
`^[A-Za-z_][A-Za-z0-9_-]{0,31}$` print as `(other key)` (:19-20, :47). `unresolved` is the R4 count
|
||||
(:72). Scope counts cover every tree entry (:66).
|
||||
- `vaultPrefix`. It's optional and must be one folder name (mapping.mjs:48). Without it the resolver
|
||||
skips the `vault-prefix` step (links.mjs:77-78), and `shortest` refuses a target with `/` (:82). So a
|
||||
full vault path link resolves to nothing, and step 4's instruction to set it is right.
|
||||
- Survey source. The C2-03a brief at 5c6f73ed7 requires the mirror's `HEAD` to be a symbolic ref to
|
||||
`refs/heads/served`, with a test that the survey's `commit` line equals the served commit (brief
|
||||
lines 678-681). The default mirror path `<dataRoot>/projects/<business>/<project>` is paths.mjs
|
||||
projectRepo's fallback.
|
||||
|
||||
SETUP-SHEET.md matches the code it can be checked against.
|
||||
|
||||
- Scope. classify returns `unmapped` for any path not ending `.md` (scope.mjs:9), and for any path in no
|
||||
book (:10-20). So `.canvas`, `.base`, images and top-level notes are not read, even inside a book.
|
||||
Scope comes from the path alone. A playground prefix must sit inside one of its universe's books and
|
||||
can't equal the book (mapping.mjs:79).
|
||||
- Property lists. I traced each of the three forms through the reader. `["[[A]]", "[[B]]"]` goes to
|
||||
flow() (reader.mjs:99), which keeps the quoted items whole (:85-86), and scalar() JSON-parses each one
|
||||
to `[[A]]` (:71-73). parseFile then reads it as a link (:159-160). The block form with quoted items
|
||||
goes through the item branch (:117-122) to the same result. `[[[A]], [[B]]]` isn't one wikilink, so
|
||||
flow() runs and refuses at the first inner `[` (:87). That's an R3 stop: front matter null, the file
|
||||
still indexed by path, name and body (:192-194), and counted as "not fully parsed" by the survey. A
|
||||
value without `[[ ]]` gives no link (:159). The sheet's "can't read that form ... ignores every
|
||||
property ... still finds the note" is right, and so is not calling it a refusal.
|
||||
- Obsidian. Sage asked me to check that the property editor writes the quoted form. I know Obsidian's
|
||||
editor writes a list property in block form, one `- "[[Link]]"` per line, with the quotes. I didn't
|
||||
run Obsidian to confirm it. The sheet only says the editor adds the quotes, which holds for that form.
|
||||
"Files and links, New link format, Absolute path in vault" is a real Obsidian setting, and its links
|
||||
carry no `.md`, which the resolver adds (links.mjs:72). Keep the tip. `path:"..."` is a valid graph
|
||||
group query.
|
||||
- Kinds and helpers. Bean and Chet's kinds `idea, question, finding, fact` equal CREATIVE_KINDS.companion
|
||||
(vocabulary.mjs:179). The five helpers equal the rest of CREATIVE_DEFINITIONS (:176). "No cited
|
||||
answer" is render.mjs:6's notice text.
|
||||
- Item caps. No persona or library code holds the reply caps yet. The persona bundle has only the 8000
|
||||
character fence (bundle.mjs:193-197), render.mjs:3 says the guard (C2-06) checks limits, and C2-06
|
||||
isn't landed. The sheet's numbers match design M5 (line 819) except for one cap, N1.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The setup sheet leaves out M5's total: at most 1,200 characters of item text per reply (design line
|
||||
819, and 10 step 8 at line 1329). As written, SETUP-SHEET.md:109-110 reads as up to 10 items of 300
|
||||
characters, 3,000 characters in all. Step 8's residual is framed as "1,000 characters of free text
|
||||
within 1,200 total", so Melanie should see the total. Add "and 1,200 characters in all" to line 109.
|
||||
"Today" there also describes limits no code enforces until C2-06 lands. Fine if the sheet goes out
|
||||
after that, which step 7's order suggests.
|
||||
|
||||
N2. SURVEY-RUN.md:90-91 says a folder name with a control character prints as `\uXXXX`.
|
||||
JSON.stringify writes tab, newline, CR, backspace and form feed as `\t`, `\n`, `\r`, `\b` and `\f`
|
||||
(survey.mjs:82). Every other control and bidi character does print as `\uXXXX`. "An escape such as
|
||||
`\t` or ``" would be exact.
|
||||
|
||||
N3. listTree can also refuse with a `library-commit:` prefix, not `library-git:` (reader.mjs:29, :31),
|
||||
for example a SHA-256 repo whose HEAD isn't 40 hex. The refusal table's exit 2 rows don't show that
|
||||
prefix. The served mirror is SHA-1, so it's unlikely. The "Report to Sage" row could say
|
||||
`library-git:` or `library-commit:`.
|
||||
|
||||
N4. Step 2's PATH ends in `/usr/bin:/bin`. If `<node dir>` is wrong and `/usr/bin/node` exists, the
|
||||
survey runs on that node and gives no 127. The 127 row holds only when no node is on the PATH at all.
|
||||
One `command -v node` under the same sudo before step 2 would show which node runs.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Webby review: row 218 (#1719), C2-12, round 2
|
||||
|
||||
Webby, 2026-10-21T11:59Z (from `date -u`). Author: Sage. Request comment 30046 (revs 2853-2855,
|
||||
sage-1981). Round 1 was APPROVE on fc0d9c26f (rev 2848, webby-0075) with four doc notes. Candidate:
|
||||
manifest digest c0d3709d4ca37a8229e13df13344285232880fafd0d21de0dd9967d0d7ddbb67, 3 paths, on sage/row218
|
||||
5fcc2ec90. That's one commit on fc0d9c26f.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 218` (round 2), not from the scratch path. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 5fcc2ec90:<path>` matches its line.
|
||||
- 5c6f73ed7..5fcc2ec90 touches exactly the 3 manifest paths. The refactor tip is still 5c6f73ed7, so
|
||||
nothing overlaps.
|
||||
- I read the whole diff fc0d9c26f..5fcc2ec90 (33+/8-). Doc only, so decision 137 applies.
|
||||
|
||||
I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- N1. SETUP-SHEET.md:109-112 now gives "at most 10 items and at most 1,200 characters of item text in
|
||||
all", which matches design M5 (line 819), and "Today" is gone. BUILD.md records Sage's ruling that
|
||||
the sheet goes to Melanie only after C2-06 lands, and only on Jason's go (#419). That covers the
|
||||
fact that no code enforces the caps yet.
|
||||
- N2. SURVEY-RUN.md:99-101 matches JSON.stringify plus quote() (survey.mjs:81-82): `\t \n \r \b \f`
|
||||
for those five, `\uXXXX` for every other C0 control, DEL, C1 and the listed bidi characters.
|
||||
- N4. The new step 2 runs `command -v node; node --version` under the same sudo and PATH, and says the
|
||||
first line must be `<node dir>/node`. The renumbering is consistent. The remaining "step N" mentions
|
||||
(lines 8, 12, 14, 19) are design 10's steps, not the sheet's own.
|
||||
- N3. The `library-commit:` row is there, with exit 2 and the right prefix. survey.mjs:123 sends every
|
||||
LibraryError to exit 2 with its message. Its example cause is wrong, see N5.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N5. The new row's example, "because `HEAD` moved during the survey", can't produce `library-commit:`.
|
||||
The survey reads HEAD once (survey.mjs:27) and passes that id along, and an old commit object stays
|
||||
in the repo when HEAD moves. listTree raises `library-commit` only when the id isn't 40 hex
|
||||
(reader.mjs:29) or `cat-file -t` isn't `commit` (:31). headCommit already asked for `HEAD^{commit}`,
|
||||
so in practice the cause is a SHA-256 repo, whose ids are 64 hex. An object that has gone missing
|
||||
gives `library-git: commit is not in the repository (project-git)` (:30) instead. "Run it again
|
||||
once" does no harm, but a rerun gives the same result. "for example a SHA-256 repository" with
|
||||
"Report to Sage" would be exact. The served mirror is SHA-1, so the row is unlikely to come up.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Webby review: row 218 (#1719), C2-12, round 3
|
||||
|
||||
Webby, 2026-10-21T12:03Z (from `date -u`). Author: Sage. Request comment 30048 (revs 2857-2859,
|
||||
sage-1984). Round 2 was APPROVE on 5fcc2ec90 (rev 2856, webby-0077) with one doc note, N5. Candidate:
|
||||
manifest digest 1a38baf5af2a5cd11708ab664c9974e406d2cf8e720a1e789f89eed364625632, 3 paths, on
|
||||
sage/row218 89ba79d94. That's one commit on 5fcc2ec90.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 218` (round 3), not from the scratch path. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 89ba79d94:<path>` matches its line.
|
||||
- 5c6f73ed7..89ba79d94 touches exactly the 3 manifest paths. The refactor tip is still 5c6f73ed7.
|
||||
- I read the diff 5fcc2ec90..89ba79d94. It changes one table row in SURVEY-RUN.md and adds a round 3
|
||||
section to BUILD.md. SETUP-SHEET.md is unchanged.
|
||||
|
||||
I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
N5 is fixed. The `library-commit:` row now names the real cause, an id the reader can't take as a
|
||||
commit, such as one from a SHA-256 repository (reader.mjs:29, :31). It now says to report to Sage and
|
||||
no longer suggests a rerun. BUILD.md's round 3 section gives the same reasoning.
|
||||
|
||||
No new notes.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Webby review: row 221 (#1722), C2-20, round 1
|
||||
|
||||
Webby, 2026-10-21T10:36Z (from `date -u`). Author: Gosalyn. Request comment 30024 (revs 2806-2807,
|
||||
sage-1939, posted by Sage as jarvis). Brief: docs/plans/2026-10-20_case2-rows.md "C2-20", blob
|
||||
16847e94ae17. Rulings: sage-1836 (fence export, pairing by argument) and sage-1840 (yaml 2.9.0 exact,
|
||||
no chown). Candidate: manifest digest 20f05e3a6ad51a3493849b6a911f62b00c33fdedb14dc898f6eeb525235ef1ad,
|
||||
22 paths, on gosalyn/c2-20-r2 8f403d0d4. 48f044ccc is the code on 62ddc96cc, and 8f403d0d4 adds BUILD.md.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 221`. It hashes to the digest above, and each path's
|
||||
sha256 from `git show 8f403d0d4:<path>` matches its line.
|
||||
- 62ddc96cc..8f403d0d4 touches exactly the 22 manifest paths. `git patch-id --stable` of 48f044ccc
|
||||
is eb094ae783e9, as BUILD.md says.
|
||||
- Overlap: the refactor tip is 29ce1e360. 62ddc96cc..29ce1e360 changes none of the 22 paths.
|
||||
- The script is 100755. The 16 fixture files and the rest are 100644.
|
||||
- I read the whole script, test file, trace helper, fixture README, the bundle.mjs and package
|
||||
diffs, and BUILD.md.
|
||||
- No test pins a sha256 of bundle.mjs, package.json or package-lock.json. The lock already had
|
||||
node_modules/yaml at 2.9.0 (package-lock.json:2192), so the pin moves nothing.
|
||||
|
||||
Gosalyn's singles on Sage's slot, 10:18:09-10:30:14Z, were 18 files 419/0 (BUILD.md). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The script does what the brief and the two rulings ask.
|
||||
|
||||
- The fence has one copy. `personaTextFault` (bundle.mjs:193-197) holds the three rules readPersona
|
||||
had, with the same reasons, and readPersona calls it (:231-232). Its messages are byte for byte the
|
||||
old ones. The script imports it (case2-persona-import.mjs:31) and applies it at stage (:239-242),
|
||||
before anything is written, and again at install on the staged bytes (:284-285). The tests take
|
||||
8000 two-byte characters as passing, and they refuse 8001, `</persona-file>`, `## ` and `# `.
|
||||
- The pairing follows sage-1836 (c). There are exactly two `--pair` arguments, a profile id from
|
||||
hermes_cli/profiles.py:37, and an lstat'd directory that isn't a symlink. Each name is exactly Bean
|
||||
or Chet, and the profiles and names differ (:85-101). The pairing line, with SOUL.md bytes and
|
||||
sha256, prints before the fence check and before any write (:231-233). The test checks that it comes
|
||||
before `staged:`. BUILD.md records that Hermes has no name field and how this answers DESIGN.md:1322.
|
||||
- Reads. stage opens SOUL.md and config.yaml per profile, with O_NOFOLLOW and O_NONBLOCK and a size
|
||||
cap (:105-122). The dry run opens only SOUL.md. The trace test hooks every fs open, read and list
|
||||
through syncBuiltinESMExports, and has a positive control on both SOUL.md files. It allows only the
|
||||
four expected paths, and it checks that the dry run never reads config.yaml. Memory, sessions,
|
||||
skills, state.db, profile.yaml, auth.json and .env are never opened.
|
||||
- Output. It prints the pairing, the per-channel prompt names and byte counts, the staged and
|
||||
installed paths, and the two vars. assertNoText walks every fixture marker (at least 20) and the
|
||||
prompt phrases over stage and install output. yaml's warnings are off (:168), and a parse failure
|
||||
gives a fixed message.
|
||||
- Files. The staging dir is a new 0700 directory or our own closed one, and every file is O_EXCL,
|
||||
O_NOFOLLOW and 0600 (:183-214). install refuses a staged file that isn't the caller's or is open to
|
||||
group or other. It checks each digest against the manifest, and it lstats both targets before it
|
||||
writes either (:293-297). The target `<configDir>/personas/<business>/<instance>.md` is the path
|
||||
readPersona opens (bundle.mjs:204, launcher.mjs:394). install runs as `<user>`, so the uid and mode
|
||||
checks there hold too.
|
||||
- The dry run prints `files N` and one count line per profile. The heading split (:59) uses the same
|
||||
line terminators as the fence's `/m`. The word list has 69 tool names plus Hermes and Discord (71
|
||||
entries), matched whole word in any case. The test counts three lines, rejects `memory_bank`,
|
||||
`discords` and `Web searching`, and checks that no matched word is printed.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Channel ids and platform keys from config.yaml print raw (:147, :237). A key holding a newline or
|
||||
an escape sequence would reach Jason's terminal as is. It's her own config and the ids are numbers or
|
||||
short names, so the risk is low. `JSON.stringify` on the key, or a refusal for keys outside
|
||||
`[A-Za-z0-9_.-]`, would close it.
|
||||
|
||||
N2. The dry run reports lines and headings but not the 8000 character limit. A SOUL.md between 8001
|
||||
characters and 32000 bytes gives normal counts on the dry run and then refuses at stage. Over 32000
|
||||
bytes, the dry run refuses (:128), so the header's "A fence fault there is counted, not refused"
|
||||
(:23) holds for headings only. Neither goes beyond the brief, and stage refuses before it writes. A
|
||||
`characters N` field in the dry run line would warn Jason a step sooner.
|
||||
|
||||
N3. Morgana's agents/morgana/work/queue-213/mutants.py:48 (B3) matches the old readPersona line, which
|
||||
this row replaces. A rerun of 213's mutant set on this base would fail to apply B3 until its pattern
|
||||
names the new line at bundle.mjs:195. It's packet history, so nothing is needed unless someone reruns
|
||||
the set.
|
||||
|
||||
N4. privateDir's lstat and mkdir (:184-188) aren't atomic. A directory created between the two
|
||||
throws a raw EEXIST with a stack, not a Refusal. It fails closed either way.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Webby review: row 226 (C2-13b, #1727) design, round d1
|
||||
|
||||
Webby, 2026-10-21T21:48Z (from `date -u`). Author: Gosalyn (gosalyn-0113). Gate owner Sage. Brief:
|
||||
docs/plans/2026-10-20_case2-rows.md "C2-13b install script and library precheck", blob f7c4cfbff (the
|
||||
row's pinned blob). Candidate: agents/gosalyn/work/queue-226/BUILD.md at f46e66883 on gosalyn/c2-13b,
|
||||
one commit on refactor d37210288 that adds only that file. Design rounds get no queue record
|
||||
(sage-2182).
|
||||
|
||||
What I checked, without runs: the design against d37210288. I read case2-precheck.sh (the header, the
|
||||
profile and tester rules :127-135, ok/refuse :51-52, bwrap :167-172), release.sh (package and
|
||||
activate), compose.yaml's auth mount, stack-copy.sh copy_tree, the library package's sync, mirror and
|
||||
status, the bus runtime's library wiring, the persona import's install output, the bus unit template,
|
||||
and host-run-sheet.md sections 7 and 9. I also read design 10 step 2 at gosalyn/case2-design 75fb3e10e
|
||||
(:1276-1300).
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1. D2 step 3, `release.sh package` then `release.sh activate`, can't run as the user design 10 makes.
|
||||
- Step 2a (DESIGN.md:1279-1280) creates `mosaic` with no supplementary groups, and the precheck refuses
|
||||
more than one group (case2-precheck.sh:143-145). So `mosaic` isn't in the docker group.
|
||||
- `cmd_package` is `docker compose build` (release.sh:122), and activate first checks
|
||||
`docker image inspect` (:39).
|
||||
- activate's health check runs tasks/hello-marker.json through run-task.sh (:41-56), which is a Compose
|
||||
run with the pi adapter. That mounts `${PI_AUTH_FILE:-/home/jwoltje/.pi/agent/auth.json}`
|
||||
(compose.yaml:43). That default doesn't exist under `/srv/mosaic`, and a missing bind source makes
|
||||
Docker create a directory (row 244's finding). If it works, it's a billed provider call on the
|
||||
hub's credential.
|
||||
- Nothing on the hub uses that image. The bus unit runs `@REPO@/scripts/mosaic bus start %i` from the
|
||||
copy (mosaic-bus.service.in), and sessions use bwrap, which is why the precheck checks bwrap (:167).
|
||||
|
||||
So step 3 fails on .54, and on .53 for row 195's W8, unless `mosaic` gets Docker access, which 2a
|
||||
rules out. The design has to settle this before code:
|
||||
- either show how step 3 runs under 2a, naming what it needs: Docker access, the pi credential as an
|
||||
input for PI_AUTH_FILE, and the provider call;
|
||||
- or take a ruling to drop release package and activate from the hub install, because the hubs run from
|
||||
the copy. That is a Q4 to Sage. Design 10 step 2b names it, so the ruling needs Jason's or Mos's word
|
||||
as Sage judges. Darkwing's row 195 W8 needs the answer too.
|
||||
I recommend the second. If it's dropped, D2 keeps bootstrap and the copy, and the run sheet changes to
|
||||
match.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The precheck's header says it "changes nothing" (case2-precheck.sh:3-4). The library sync check
|
||||
writes host state:
|
||||
- ensureMirror creates the bare mirror under the data root (mirror.mjs:101-110);
|
||||
- sync moves `refs/heads/served` and writes the sync state (sync.mjs:150-151).
|
||||
Say so in the header. Also, sync's one-per-mirror guard is per process (`running`, sync.mjs:29-31), so a
|
||||
precheck run while the bus unit is up can race the bus's 60 s fetch (runtime.mjs:365-380). Either refuse
|
||||
the sync check when the bus is running, or state that it runs only before the units start (design 10
|
||||
step 8 order).
|
||||
|
||||
N2. sync returns a code for credential-missing, fetch-failed and diverged without throwing
|
||||
(sync.mjs:102-157). D6's "a sync error refuses with its code" has to refuse on a returned non-null code
|
||||
too, as the broker does (broker.mjs:2466-2467), not only on a throw.
|
||||
|
||||
N3. Build the sync context and slot the way the bus does: `{ business, project, dataRoot }`, with the slot
|
||||
at `<configDir>/secrets/<id>/library-gitea` (runtime.mjs:146-151, :164). Name that source in the build,
|
||||
so the precheck and the bus can't drift apart on the path.
|
||||
|
||||
N4. D2 step 8 removes the business file if validate refuses. On a re-run where the file already matched
|
||||
(D4), that would delete a file this run didn't write. Remove only a file this run created, and add a
|
||||
test for it.
|
||||
|
||||
N5. Q2: I agree R-problems don't refuse, since the brief says the check lists them. But the precheck's
|
||||
only words are `ok` and `REFUSED` (:51-52), so an `ok library R2 <path>` line reads as a pass.
|
||||
Use a distinct prefix or a count line. The test can pin the format either way.
|
||||
|
||||
N6. D7's cites are off by a few lines at d37210288. The dev.args sentence is host-run-sheet.md:240,
|
||||
and the `library-gitea` row is :275.
|
||||
|
||||
## Questions
|
||||
|
||||
- Q1: I agree with D3. Staging and Jason's pairing check before the install keeps D4's no-change rule.
|
||||
- Q3: I read "pinned harness" the same way, as the copy's `npm ci` from package-lock.json
|
||||
(stack-copy.sh copy_tree). The run sheet should say that step needs registry access from the host.
|
||||
|
||||
## Checked and fine
|
||||
|
||||
- D1. No CLI writes a business file. Taking it as an input keeps business.mjs as the only schema. The
|
||||
factory checks repeat the precheck's rules (:127-135).
|
||||
- D3. The persona import prints `instance <id>: persona.name <name>, persona.sha256 <hex>`
|
||||
(case2-persona-import.test.mjs:263). It never overwrites, and only names and digests go into the file.
|
||||
- D2. The answers to row 195's needs 4 and 5 are unchanged: no dev.args and no adopt. stack-copy's copy
|
||||
is idempotent ("exists:").
|
||||
- D8 covers each refusal the brief lists, the factory case, and a mutant per refusal.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Webby review: row 226 (C2-13b, #1727) design, round d2
|
||||
|
||||
Webby, 2026-10-21T21:55Z (from `date -u`; the first push said 21:58Z, which was my estimate). Author: Gosalyn (gosalyn-0117). Candidate: agents/gosalyn/work/
|
||||
queue-226/BUILD.md at d572b6102 on gosalyn/c2-13b. d1 was f46e66883, which got CHANGES in
|
||||
REVIEW-design-d1.md. Sage's ruling, sage-2189, is as BUILD.md quotes it. I didn't read the message
|
||||
itself. Design rounds get no queue record (sage-2182).
|
||||
|
||||
What I checked, without runs: the whole f46e66883..d572b6102 diff against d37210288. I checked the new
|
||||
fact cites: sync.mjs's `running` guard, runtime.mjs:143-151 and :162-164, mirror.mjs ownedRoot,
|
||||
readStateHeld's empty state (:205-210), the lastError code pattern, and validProblem (:196-201).
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- B1. Release package and activate are out, with the CASE2_RELEASE seam and its test. The reason is
|
||||
stated (2a has no Docker and no pi credential, and the hub runs no containers), and design 10 step 2b
|
||||
changes to match (D8). Row 195's W8 note is there.
|
||||
- N1 and webby-0086. D6a runs before the start and touches no mirror. D6b runs after the bus is up and
|
||||
step 4. It reads libraryStatus only, which takes the flock and writes nothing, so it can't race the
|
||||
bus timer. A missing mirror gets its own refusal rather than ownedRoot's library-git, and the
|
||||
60 s wait goes in the run sheet.
|
||||
- N2: a non-null lastError refuses with its fixed-pattern code.
|
||||
- N3: the context is `{ business, project, dataRoot }`, as the bus builds it.
|
||||
- N4: step 7 removes only a file this run created, and the D9 test checks a matching file survives
|
||||
byte for byte.
|
||||
- N5 and N6 are as asked. The registry note is in D7.
|
||||
- The tests cover each D6a refusal, each D6b refusal against a real ensureMirror and sync on a local
|
||||
bare repo, and the pass with a `problems 1` line. Each refusal gets a mutant.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. D8 puts agents/gosalyn/work/case2/DESIGN.md in this row's manifest, but that file isn't on
|
||||
refactor. It exists only on gosalyn/case2-design (1346 lines at 75fb3e10e). So landing 226 would add the
|
||||
whole design doc to refactor, not just the three-line change. If Sage meant that, my manifest review
|
||||
covers only D8's lines in it, and the landing note should say the rest arrives unreviewed by this row.
|
||||
If not, make the D8 change on gosalyn/case2-design and leave the file out of 226's manifest. That's
|
||||
Sage's call. It's doc-only, so it doesn't block (decision 137).
|
||||
|
||||
N2. On the path in the `problem` line: keep it. Jason needs the file name. But validProblem accepts any
|
||||
string up to 4096 without NUL (mirror.mjs:199), and JSON.stringify escapes only C0 controls, `"` and
|
||||
`\`. C1 controls (U+0080-U+009F, including U+009B, a one-byte CSI) and bidi overrides
|
||||
(U+202A-U+202E, U+2066-U+2069) pass through to the terminal. Escape those too, for example by
|
||||
replacing `[\u007f-\u009f--]` with `\uXXXX` after stringify,
|
||||
and add a test path that holds one.
|
||||
|
||||
N3. When the bus's first sync ran before the token went in, the state has lastFetch null and lastError
|
||||
library-credential-missing. Both D6b refusals apply. Check lastError first, or print both, so the
|
||||
output names the cause rather than just "never synced". The D9 test for a fresh mirror should
|
||||
leave lastError null so it hits the other branch.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Webby review: row 228 (row A, #1729), round 1, ZAI_API_KEY in the session env allowlist
|
||||
|
||||
Webby, 2026-10-20T21:19Z (from `date -u`). Requested by Sage (sage-1495), request comment 29677.
|
||||
Candidate: commit 4fa2ed1e3d5e20ad5a0c98a1578e35d4ba055c67 on researcher/row228, base 088087ac9 (an
|
||||
ancestor of origin/refactor), one commit, three files. Brief: "Brief: row A" in
|
||||
docs/plans/2026-10-20_case2-rows.md on origin/refactor 32b54ac3b. I approved that brief in webby-0034.
|
||||
|
||||
I ran `packages/seat/tests/session.test.mjs` alone on a `git archive` export of the candidate: 13 tests,
|
||||
13 pass, 0 fail, skipped or cancelled. I took the mutant result from Researcher's
|
||||
`out/mutants.log`. M1 removes `ZAI_API_KEY`, the new test reports failing, and the verdict is KILLED with
|
||||
the restore sha256 matching. I didn't rerun the mutant.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Each item in the brief is in the candidate, and nothing beyond it:
|
||||
- session.mjs:86: `"ZAI_API_KEY"` follows `OPENROUTER_API_KEY` on the provider line. Nothing else in
|
||||
`ENV_ALLOW` changes, and `SESSION_CREDENTIALS` and `hasSessionCredential` don't change either.
|
||||
- The new test checks membership and passthrough with `fixture-zai`, and checks that `ZAI_TOKEN` is
|
||||
dropped. The existing allowlist test is unchanged.
|
||||
- README :435-439 has the sentence the brief gives. At :714-716, "one line per variable" and the
|
||||
every-provider-key sentence are in, and the "nothing in the repository creates or reads it" line is
|
||||
left for C2-13a.
|
||||
|
||||
No other test reads either changed file. A grep of `packages/*/tests` and `scripts/test-*.sh` for
|
||||
`ENV_ALLOW`, `session.mjs` and `cli/README`, outside session.test.mjs itself, finds nothing, and no 64-hex
|
||||
pin names them. The fixture value is made up, so no credential value appears.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Sage asked me to judge the repeat. The new sentence ends "a shell inside any of them can read it".
|
||||
The next sentence, which was already there, says "A shell the harness opens, Claude Code's `Bash` or Pi's
|
||||
`bash`, can read them." The two say the same thing. Worse, "them" now follows a sentence whose subject is
|
||||
"it", so its antecedent, "the same variables" two sentences back, is hard to find. This is my miss in
|
||||
webby-0034: the brief dictated the sentence, and the candidate copied it exactly. It's doc-only, and no
|
||||
test reads the README, so it doesn't block (decision 137). A fix for a later README touch, or at landing
|
||||
if that's cheap:
|
||||
- End the new sentence at "Claude Code ones included."
|
||||
- Change the existing sentence to "A shell the harness opens, Claude Code's `Bash` or Pi's `bash`, can
|
||||
read all of these variables."
|
||||
@@ -0,0 +1,37 @@
|
||||
# Webby review: row 228 (#1729), round 2
|
||||
|
||||
Webby, 2026-10-21T05:10Z (from `date -u`). Author: Researcher. Request comment 29927 (sage-228-request-2,
|
||||
revs 2671-2672, posted as jarvis). Candidate: manifest digest
|
||||
ab40b92f68a03d4c528f9fe80333fe7897863ea35e27072f2a6c1de9fdf5d5e5, 3 paths, on researcher/row228-r2
|
||||
5f2aad593. Base origin/refactor 9b5362018 is its ancestor. Round 1 was APPROVE on commit 4fa2ed1e3
|
||||
(comment 29677). Scope (sage-1734): the README only.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 228` (round 2), not from the author's packet. It hashes to
|
||||
the digest above. Each path's sha256 from `git show 5f2aad593:<path>` matches its line.
|
||||
- session.mjs (sha256 40822543...) and session.test.mjs (193bacdc...) have the same git blobs at
|
||||
5f2aad593 as at round 1's 4fa2ed1e3 (f8254d1c, 08b3d051). I didn't reread them.
|
||||
- I read `git diff 9b5362018 5f2aad593 -- packages/cli/README.md` against round 1's README hunk.
|
||||
- Readers: only packages/cli/tests/host.test.mjs:855 reads this README, through `join()`, and it checks
|
||||
only that the unit's file name appears. Neither hunk touches that name. No test quotes a changed
|
||||
sentence, and no 64-hex pin names the README.
|
||||
|
||||
Researcher's slot run on 5f2aad593 was 94/0 over four files with M1 killed (sage-1734). I didn't run
|
||||
anything.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- The :714 hunk keeps both sides. Row 190's wording stays whole: "nothing in the repository creates it,
|
||||
and only the Case 2 precheck reads it, printing variable names, never values. No session sees the file
|
||||
itself, only the variable." Researcher's two changes sit inside it: "one line per variable" and "It
|
||||
holds every session provider key, not only Claude's."
|
||||
- The N1 trim at :435-439 now makes one reader point. The `ZAI_API_KEY` sentence says it reaches every
|
||||
session the host starts, and the paragraph ends "A shell the harness opens ... can read all of these
|
||||
variables." Round 1's repeated "a shell inside any of them can read it" is gone, and nothing it said
|
||||
was lost.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Doc-only. Two singulars at :711-718 are left from when the file held one token: "The session
|
||||
credential comes from" and "only the variable". With one line per provider key, "credentials" and "the
|
||||
variables" would match. No test reads either phrase.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Webby review: row 228 (#1729), round 3
|
||||
|
||||
Webby, 2026-10-21T06:52Z (from `date -u`). Author: Researcher. Request comment 29949 (sage-1800, revs
|
||||
2704-2706, posted as jarvis). Candidate: manifest digest
|
||||
82cf98c600622a23a968a3856659ad5c28c3ab0f4561a1ec4a9ee1f072c71d7c, 3 paths, on researcher/row228-r3
|
||||
cf18c42b9, one commit on c771a36f4 (row 234's landing). Round 2 was APPROVE on ab40b92f (comment 29927).
|
||||
Scope (sage-1800): the README, my round 2 N1 pair, and row 234's hosts section.
|
||||
|
||||
What I checked, read-only, with no runs and no worktree:
|
||||
- The queue's round 3 manifest text hashes to the digest above, and each path matches
|
||||
`git show cf18c42b9:<path>`.
|
||||
- session.mjs and session.test.mjs are git blobs f8254d1c and 08b3d051, the same as rounds 1 and 2
|
||||
(sha256 40822543 and 193bacdc).
|
||||
- `git diff c771a36f4 cf18c42b9 -- packages/cli/README.md` has only row 228's hunks: the allowlist
|
||||
paragraph at :432-439 (round 2's trimmed text) and the credential-file paragraph at :708-718.
|
||||
|
||||
Researcher's slot run was 55/0 (host.test.mjs 42, session.test.mjs 13).
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- N1 from round 2 is fixed: :711 "The session credentials come from" and :718 "only the variables".
|
||||
- Row 234's "## hosts (row 234)" section is intact. The diff against the 234 landing doesn't touch it.
|
||||
- Row 190's wording in the credential-file paragraph stays whole, as in round 2.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Webby review: row 230 (C2-13a, #1732), round 1
|
||||
|
||||
Webby, 2026-10-20T23:50Z (from `date -u`). Author: Gosalyn (gosalyn-0071). Candidate: manifest digest
|
||||
8e9d1ef0c9c84e798a568921825804c5f6ae1e77ea96777bd1ddd782a2be830a, 5 paths, on gosalyn/c2-13a-on-190
|
||||
2fa7b0f4c. Base 00d4cc1f6 is its ancestor. Brief: docs/plans/2026-10-20_case2-rows.md "C2-13a", blob
|
||||
f6b4933f0f47.
|
||||
|
||||
What I ran, on a detached worktree of 2fa7b0f4c in my own scratch, removed afterwards:
|
||||
- The queue's manifest text hashes to the digest above, and `sha256sum -c` passes for all 5 paths.
|
||||
- Each file alone with `node --test --test-reporter=tap`: case2-unit 28/28 with 0 skipped (the bind test
|
||||
ran), and host.test.mjs 42/42. host.test.mjs reads the cli README (:855).
|
||||
- A grep of `packages/*/tests/*` and `scripts/test-*.sh` for the two script names finds only
|
||||
case2-unit.test.mjs. A grep for 64-hex pins near README or case2 finds none, and none of the tests that
|
||||
read the README checks the changed sentence.
|
||||
|
||||
I ran no mutants. I read both scripts in full against the brief, and the test file case by case.
|
||||
|
||||
## Verdict: CHANGES (B1)
|
||||
|
||||
### B1. Seven precheck refusals and the "missing claude passes" case have no test
|
||||
|
||||
The brief's Tests section says "The precheck runs against a scratch data root. Each refusal has a case",
|
||||
and "With no claude-code instance the claude check doesn't run, and a test shows a missing claude passes."
|
||||
A grep of case2-unit.test.mjs for each refusal's message finds no case for these branches of
|
||||
scripts/case2-precheck.sh:
|
||||
- :77, the user isn't in the passwd database.
|
||||
- :137, the business file doesn't load (bad JSON, or `loadBusiness` refuses it).
|
||||
- :151, the run root is a symlink. This one matters most. `stat -c '%a'` follows the link, so without
|
||||
this branch a symlink to any 0700 directory passes as "is 0700". Nothing would catch its removal.
|
||||
- :179, the installed Console unit's ExecStart has no `--port`.
|
||||
- :180, `ip_unprivileged_port_start` can't be read.
|
||||
- :223, claude replies without the marker (`nomarker`).
|
||||
- :224, claude gives no JSON reply.
|
||||
|
||||
The "no claude-code instance" case at case2-unit.test.mjs:515-518 doesn't show that a missing claude
|
||||
passes. Its PATH is `bin:/usr/bin:/bin`, and this host has `/usr/bin/claude` (root-owned, 0755). The
|
||||
`missing` case at :532-536 already builds a PATH without it. The no-route case needs the same PATH so
|
||||
the claude really is missing.
|
||||
|
||||
The fix is test-only: one case per branch, each matching its REFUSED line, and the `usr-bin` PATH on the
|
||||
no-route case. The two fake-claude cases need a third fake kind that prints JSON without the marker, and
|
||||
a fourth that prints something other than JSON.
|
||||
|
||||
## Deviations (sage-1570 accepted them for the build)
|
||||
|
||||
1. `--business ID` on the precheck: accept. The brief names no other way to find the business file.
|
||||
2. A missing run root passes: accept. `newRun` makes it with `mkdirSync(root, { recursive: true, mode:
|
||||
0o700 })` (packages/seat/src/session.mjs:109), so before the first launch there is nothing to check.
|
||||
The symlink branch still needs its test (B1).
|
||||
3. The unmeasured 2000-token ceiling: accept. A wrong ceiling fails safe. Claude Code's own prompt is
|
||||
far above 2000, so an appending claude can't pass, and a ceiling set too low only refuses a good
|
||||
host. Row 195's install run measures it. Each precheck run with a claude-code instance also makes one
|
||||
real model call. That's small, but the operator should know it.
|
||||
4. The fake getent uid for the proof-owner case: accept. The check compares against `$USER_UID` from
|
||||
getent (:219), so moving the uid exercises the same comparison a root-owned file would.
|
||||
5. Refusing `%h` left in any unit: accept. A system unit reads `%h` as root's home.
|
||||
6. 64 lower-case hex for the allowed user with X-Authentik-Uid: accept. It matches decision 101, and
|
||||
:298-299 test both sides.
|
||||
|
||||
## What I checked and found right
|
||||
|
||||
- The render. Every token has one value, as the brief lists. `@PATH@` puts the node's directory first
|
||||
every time (:228-230), so `/usr/bin/node` gives `/usr/bin:/usr/local/bin:/bin`. `@CONFIG_ENV@` is
|
||||
deleted and `@SESSION_ENV@` is the `%h` path, as instance.sh:88-90 does for the default instance. `%h`
|
||||
then becomes the literal home. Values going into sed and awk pass `plain` (:55), so none holds `|`, `&`,
|
||||
`\` or `%`. `@FLAGS@` goes in after the token check (:255-259), so an allowed user holding `@` can't
|
||||
trip it. The flag values also contain no `&`, so bash's pattern replacement can't expand one.
|
||||
- The added lines. User, Group and the four hardening lines are added in order at the end of
|
||||
`[Service]`, the caps only on the Console, and `WantedBy=multi-user.target` replaces
|
||||
`default.target` in both units. No `After=` or `Wants=`.
|
||||
- The checks (:302-359). Fixed lines are each checked exactly once in `[Service]`, with the Environment=
|
||||
lines matched exactly. RestrictNamespaces, PrivateUsers, SystemCallFilter and PermissionsStartOnly are
|
||||
refused. The Exec prefix check reads the whole leading run of `-@:+!`, so `-+` is caught. A continued
|
||||
line refuses, so nothing hides on one.
|
||||
- The re-install. Each installed unit passes the same checks against its own copy, must equal the
|
||||
render with only the copy id swapped (:380), and both units must name one copy (:385). So the node,
|
||||
flags, `--pm` and `--claude` can't drift.
|
||||
- The inputs. `--node` and `--claude` must be absolute, plain and normalized, not a symlink, outside
|
||||
`<home>`, and root-owned and not group- or other-writable all the way up to `/` (:114-124). The copy
|
||||
id is 40 lower-case hex and must name a real directory (not a link) holding `scripts/mosaic`. The proof
|
||||
file is checked by metadata only: no symlink anywhere in its path (`realpath -e` equality), under
|
||||
`<home>/.config/mosaic-dev/secrets`, owned by the user's uid, 0600 or 0400. It is never opened.
|
||||
- The serve flags match the brief for both profiles. The `--tester-port` equal to `--port` case is
|
||||
unreachable through the ranges (1..1023 against 1024..65535). The test at :351-356 says so, and the
|
||||
guard stays.
|
||||
- The start order: daemon-reload, enable both, start the bus, then the Console. A failed bus start
|
||||
stops before the Console starts (:403).
|
||||
- The precheck never prints a session env value. Every precheck test asserts the fixture key is absent
|
||||
from stdout and stderr. The claude subshell exports the file's variables only into claude's
|
||||
environment.
|
||||
- The bind test builds the argv with `binds()` and `sandboxArgv()`, checks the floor first and fails
|
||||
rather than skipping when it's too low, and uses a free high port as its control.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. On a re-install the units are rewritten, but `systemctl start` (:403-404) does nothing to a running
|
||||
unit. The bus and Console keep running the old copy, and the last line still says "enabled and started".
|
||||
The units on disk then name the new copy while both processes run the old one. If the bus later restarts
|
||||
on its own (`Restart=on-failure`, mosaic-bus.service.in:32), it comes back on the new copy while the
|
||||
Console stays on the old one. That is the split pair :385 refuses, and the bus accepts only its own
|
||||
human-cli.mjs path. The brief puts re-pins in row 195 (brief line 899) and says nothing here restarts
|
||||
anything, so this isn't a defect in this row. Row 195 should restart both together, or this script
|
||||
should say on a re-install that it changed nothing that is running.
|
||||
|
||||
N2. A drop-in overrides every line this script checks. Examples are
|
||||
`/etc/systemd/system/mosaic-webui.service.d/*.conf`, `[email protected]/`,
|
||||
`mosaic-bus@<business>.service.d/` and the global `service.d/`. A unit file named
|
||||
`mosaic-bus@<business>.service` replaces the template altogether. Only root can write these
|
||||
directories, so this is the same defence in depth as the installed-unit checks. The user-unit installer
|
||||
already refuses ExecStart and WorkingDirectory drop-ins (scripts/bus-service.sh:122-145, globs from
|
||||
instance.sh:88). A later row (C2-13b or 195) could do the same for the system paths, and also check
|
||||
User, Group and the hardening lines.
|
||||
|
||||
N3. The precheck's claude, when the bus unit has no `MOSAIC_CLAUDE_BIN`, is `command -v claude` in the
|
||||
user's shell (:194). The bus runs `claude` from the unit's `Environment=PATH=`, which is the node's
|
||||
directory then `/usr/local/bin:/usr/bin:/bin`. A claude in `~/.local/bin` would be checked while the bus
|
||||
runs a different one, or none. This matches the brief's words ("`claude` on PATH"). Resolving it against
|
||||
the unit's PATH value, which the precheck can already read with `unit_value`, would check the binary
|
||||
the bus runs.
|
||||
|
||||
N4. Doc-only. The changed README line (packages/cli/README.md:713) is about 150 characters, while the
|
||||
paragraph around it wraps near 75. No test reads the sentence.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Webby review: row 230 (C2-13a, #1732), round 2
|
||||
|
||||
Webby, 2026-10-20T23:58Z (from `date -u`). Author: Gosalyn (gosalyn-0076), op gosalyn-230-inreview-r2, rev 2559.
|
||||
Request comment 29788 (sage-1595). Candidate: manifest digest
|
||||
fbb0d7e42c67507519e03b5a2354fb82e760908fcfeecdf1f6c1da3e836a81da, 5 paths, on gosalyn/c2-13a-r2
|
||||
96768766c. Base origin/refactor 722136772 is its ancestor. Round 1 was CHANGES B1 (REVIEW-r1.md), and
|
||||
sage-1589 ruled N1 into this round.
|
||||
|
||||
What I ran, on a detached worktree of 96768766c in my own scratch, removed afterwards:
|
||||
- I took the manifest text from `queue show 230` (round 2), not from the author's scratch. It hashes to
|
||||
the digest above, and `sha256sum -c` passes for all 5 paths.
|
||||
- `git diff 00d4cc1f6 722136772` on the 5 paths is empty. `git range-diff` shows the two r1 commits
|
||||
unchanged (`=`) after the rebase, so the r1 to r2 change is 96768766c alone, and I read it in full.
|
||||
- Each file alone with `node --test --test-reporter=tap`: case2-unit 30/30 with 0 skipped, and
|
||||
host.test.mjs 42/42.
|
||||
- The reader grep (`packages/*/tests/*`, `scripts/test-*.sh`) still finds only case2-unit.test.mjs for
|
||||
the two scripts. No 64-hex pin names a case2 file or the cli README. host.test.mjs reads the README
|
||||
only for the unit file name, so the rewrap can't break it.
|
||||
|
||||
I ran no mutants. Gosalyn's 10 mutants and the gate run in Sage's slot. Sage won't land the row until
|
||||
both are green.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. Each branch I named has a case that matches its REFUSED line:
|
||||
- :77: getent exits 2.
|
||||
- :137: the business file is unparsable, missing (ENOENT), or fails the schema. The schema case asserts
|
||||
it isn't the "cannot read" branch, so it reaches `loadBusiness`.
|
||||
- :151: the run root is a symlink to a directory chmod'ed 0700. If the branch were removed, `stat` would
|
||||
read the target as 0700 and the test would fail.
|
||||
- :179: the Console ExecStart has no `--port`.
|
||||
- :180: the port-floor file is missing.
|
||||
- :223 and :224: the fake claude has new `nomarker` and `text` kinds. `nomarker` keeps 40 tokens, so it
|
||||
passes the ceiling and reaches the marker check.
|
||||
|
||||
The no-claude-code case now runs on `pathWithoutClaude()`, the same PATH as the missing-claude case. It
|
||||
asserts both a pass and that no claude line printed, so it shows a host with no claude passing.
|
||||
|
||||
N1, as ruled, is in:
|
||||
- The script asks `is-active` for both units after the re-install checks and before the write.
|
||||
- A running unit gets "was already running, so it is still on copy <old>, not copy <new>", never
|
||||
"started".
|
||||
- A unit that wasn't running gets "started ... on copy".
|
||||
- The "restart ... together" line follows.
|
||||
- A fresh install still prints the round 1 line, and the clean-render test now asserts that.
|
||||
- The script restarts nothing.
|
||||
|
||||
The fake systemctl answers `is-active` from `CASE2_TEST_ACTIVE`, and the two sequence asserts take
|
||||
the two new log lines into account.
|
||||
|
||||
N4 is rewrapped. N2, N3 and the N1 restart are in BUILD.md as row 195 carry items.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The "still on copy <old>" line takes `<old>` from the installed unit file (`ids[0]`, :384-386), not
|
||||
from the running process, and those can differ. Here is the sequence:
|
||||
1. Install A and start both.
|
||||
2. Re-install B with both running. The files now name B, and both processes still run A.
|
||||
3. Re-install C before any restart. The script prints "still on copy B", but both run A.
|
||||
|
||||
The new test models this. Its second step re-installs A with only the Console running, after an
|
||||
install of B that didn't restart it, and asserts "still on copy B". In that story the Console runs A.
|
||||
|
||||
The restart line is still right, so the operator does the right thing. The copy claim is what's
|
||||
wrong. Two fixes would work, and either fits row 195:
|
||||
- Drop the copy and print "was already running, so it is not on copy <new>".
|
||||
- Read the running copy from the unit's main process, for example `systemctl show -p MainPID`, then
|
||||
`/proc/<pid>/cwd`.
|
||||
|
||||
N2. A test-file nit. The `both` case asserts `doesNotMatch(/started/)` over all output. Any later wording
|
||||
that holds the substring "started" would fail it, including "not started". Matching
|
||||
`/started mosaic-/` would pin the claim the ruling cares about.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Webby review: row 231 (#1733), C2-01b, round 1
|
||||
|
||||
Webby, 2026-10-21T09:32Z (from `date -u`, corrected from an estimated 09:36Z). Author: Gosalyn. Request comment 30003 (sage-231-review-1b,
|
||||
sage-1896). Brief: docs/plans/2026-10-20_case2-rows.md "C2-01b". These are my row 212 notes N1-N3
|
||||
(webby-0041). Candidate: manifest digest
|
||||
d4c4cf09c1d5abe5580e6d8804ef134719c0c0a4431c6231d814f6c38e9966fe, 9 paths, on gosalyn/c2-01b-r2
|
||||
88daaac50. The code is 3647a5a93, and 88daaac50 adds BUILD.md. Both are on fefd54e13.
|
||||
|
||||
What I checked:
|
||||
- I took the manifest text from `queue show 231`. It hashes to the digest above, and each path's sha256
|
||||
at 88daaac50 matches its line. fefd54e13..88daaac50 touches no path outside the manifest.
|
||||
- `git patch-id --stable` of 3647a5a93 is 45911915a9ee, matching BUILD.md.
|
||||
- Overlap: the refactor tip is 38b83ea5c. fefd54e13..38b83ea5c changes no manifest path.
|
||||
- I read the whole diff of src, README and tests. Gosalyn's singles were 60/0 (BUILD.md). I ran nothing.
|
||||
|
||||
## Verdict: CHANGES (two blockers)
|
||||
|
||||
N1 (escapes) and N2 (list item lines) are correct. N3 (attachments) has two problems.
|
||||
|
||||
### Blockers
|
||||
|
||||
B1. A link to a real note whose name ends in a dot and letters is silently dropped. readLibrary skips
|
||||
`isAttachment(link)` before it resolves anything (reader.mjs:202). isAttachment (links.mjs:30-32) calls
|
||||
any last segment ending in `.` plus 1-8 letters or digits, with at least one letter, an attachment. So
|
||||
`Dr.Quinn`, `Mr.Robot`, `a.k.a` and `Book 1.ii` are attachments.
|
||||
|
||||
Failure: `Book One/Characters/Dr.Quinn.md` exists, and another note has `relationships:` `- "[[Dr.Quinn]]"`
|
||||
or a body `[[Dr.Quinn]]`. The resolver would find `Dr.Quinn.md` on its `.md` try (links.mjs:69), and
|
||||
Obsidian resolves it the same way. Instead the link never reaches the resolver, it isn't in the
|
||||
file's links, and no problem is raised. The relationship disappears with nothing to show for it.
|
||||
|
||||
Fix: resolve first. Treat a link as an attachment only when it doesn't resolve and isAttachment is
|
||||
true. The brief's cases still hold: `![[cover.png]]` gives no problem, `![[missing-note]]` gives R4,
|
||||
`![[note.md]]` resolves. Add a test with an indexed `Dr.Quinn.md` linked as `[[Dr.Quinn]]` in the body
|
||||
and in a block list.
|
||||
|
||||
B2. The change covers more than the brief. The brief's N3 is about embeds only: "An embed whose target
|
||||
has an extension other than `.md` is an attachment ... An embed with no extension or with `.md`
|
||||
resolves as a link" (case2-rows.md:131-133). Its tests name only embeds. The code makes every wikilink
|
||||
an attachment by extension: plain `[[cover.PNG]]`, `[[Board.canvas]]` and front matter values
|
||||
(reader.mjs:202, tests at reader.test.mjs:110-118). It deletes the approved line "A link to a `.canvas`
|
||||
file or an attachment is R4" from README.md:87 (as of fefd54e13). That turns a reported R4 into silence for plain and
|
||||
front matter links. Neither BUILD.md nor the brief records a ruling for it. Either narrow the skip to
|
||||
`link.embed`, or have Sage amend the brief to cover links and front matter values, and then cite that
|
||||
ruling in BUILD.md.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. quote() covers the brief's ranges plus DEL, and the survey test checks both ends of each range.
|
||||
Some format characters are outside that list and JSON.stringify leaves them raw: U+061C (Arabic letter
|
||||
mark, a bidi control), U+2028 and U+2029, and zero-width U+200B-U+200D and U+FEFF. None can reorder a
|
||||
line the way the escaped ones can. Add U+061C if you're touching the range anyway.
|
||||
|
||||
N2. The survey's `attachment` count only looks at names, without resolving. Once B1 is fixed, a
|
||||
`[[Dr.Quinn]]` that resolves in the reader will still count as an attachment in the survey. Either
|
||||
check it against the survey's own path list or describe the count in README.md as "links whose target
|
||||
looks like an attachment".
|
||||
|
||||
N3. The N2 work is sound. itemLines gets each item line pushed next to its value (reader.mjs:121-122)
|
||||
and is created only for a block list key (:131). parseFile uses the item's line when the key has
|
||||
one, and otherwise the key's line (:157-160), so flow and scalar values keep the key's line. The new reader test checks R4 at the
|
||||
flow key line 2, R4 at item 6 and R5 at item 7.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Webby review: row 231 (#1733), C2-01b, round 2
|
||||
|
||||
Webby, 2026-10-21T09:51Z (from `date -u`). Author: Gosalyn. Request comment 30005 (revs 2785-2786,
|
||||
sage-1913). Round 1 was CHANGES (B1, B2) at 88daaac50 (rev 2774, comment 30003). Sage ruled in
|
||||
sage-1900 (B2 narrows to embeds, README R4 line back; B1 resolves first) and sage-1902 (a front matter
|
||||
value written as an embed resolves like a link). Candidate: manifest digest
|
||||
312c16fc4f657b38058cdfeed69eb11db52cd41507e6ec51a46c93c326b96690, 9 paths, on gosalyn/c2-01b-r2
|
||||
f37a65857, a fast-forward from 88daaac50 by one commit.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 231` (round 2). It hashes to the digest above, and each
|
||||
path's sha256 at f37a65857 matches its line. Same 9 paths as round 1. fefd54e13..f37a65857 touches no
|
||||
path outside the manifest.
|
||||
- Overlap: the refactor tip is 4364dddb0. fefd54e13..4364dddb0 changes none of the 9 paths.
|
||||
- I read the whole 88daaac50..f37a65857 diff (8 files, +110 -34).
|
||||
|
||||
Gosalyn's slot runs (sage-1903, sage-1907): run 1 was 60/1, the new Dr.Quinn test expecting the front
|
||||
matter link first, while the reader lists body links first. That was fixed in the test only. Run 2 was
|
||||
61/0. BUILD.md records both. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. readLibrary resolves every link first (reader.mjs:202-204). Only an unresolved link skips
|
||||
R4, and only when isAttachment holds. The new reader test indexes `Dr.Quinn.md` and resolves
|
||||
`[[Dr.Quinn]]` in a block list and in the body, plus `![[Dr.Quinn]]`, with no problem.
|
||||
|
||||
B2 is fixed as sage-1900 rules. isAttachment is now `link.embed === true && !link.field` plus the
|
||||
extension test (links.mjs:33-35), so a plain link and a front matter value are never attachments. An
|
||||
unresolved one is R4. That includes a front matter value written as an embed, per sage-1902. The
|
||||
reader test checks R4 for `"![[portrait.JPG]]"` (line 3), `[[Board.canvas]]` (line 5) and
|
||||
`![[missing-note]]` (line 6), with no problem for `![[cover.png]]` or `![[map.pdf|Map]]`. README.md:88-89
|
||||
is the fefd54e13 text again, word for word.
|
||||
|
||||
The two round 1 notes are taken. RAW_CONTROLS adds U+061C, with a survey test. The survey's attachment
|
||||
count still goes by name, and README.md says so with the `![[Dr.Quinn]]` example, which is the
|
||||
wording option I offered.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N4. README.md:88 says "A link to a `.canvas` file or an attachment is R4", and the next paragraph says
|
||||
an unresolved attachment embed is never R4. The text is accurate if you read "link" as "not an
|
||||
embed", but a reader could take the two as contradicting each other. "A plain link to ..." on :88 would
|
||||
settle it, but that changes the line sage-1900 restored, so leave it unless Sage wants it.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Webby review: row 238 (#1744), round 1
|
||||
|
||||
Webby, 2026-10-21T01:31Z (from `date -u`). Author: Launchpad (launchpad-0017), revs 2590-2591. This row follows up my row
|
||||
200 r3 N1. Candidate: manifest digest 7093c10e43be8d7b9b38a3d0d025c5e0f014befd77f17a9b70e6f2a06feacec4, 3
|
||||
paths, on launchpad/row238 bec7e499e (one commit). Base refactor 011d41ba6 is its ancestor and is
|
||||
origin/refactor today.
|
||||
|
||||
What I ran, on a detached worktree of bec7e499e in my own scratch, removed afterwards:
|
||||
- I took the manifest text from `queue show 238`. It hashes to the digest above, and `sha256sum -c`
|
||||
passes for all 3 paths.
|
||||
- Each touched test file whole with `node --test --test-reporter=tap`: stack-release 193/193 and
|
||||
stack-instances 23/23, with 0 failures and 0 skipped.
|
||||
- A grep of `packages/*/tests/*` and `scripts/test-*.sh` for the hint text finds :1951 (default, unchanged)
|
||||
and the new :2102 and :2104 only. No test reads the changed TOOLS.md paragraph. The only TOOLS.md mention
|
||||
in a test is a string at stack-release.test.mjs:491. No 64-hex pin names the 3 paths.
|
||||
|
||||
I ran no mutants. Mutant 1 and the suites run in Sage's slot.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
`inst="${INSTANCE_FLAGS[*]:+ ${INSTANCE_FLAGS[*]}}"` (stack-release.sh:841) is the idiom `plan()` already uses
|
||||
at :1192. INSTANCE_FLAGS is `()` by default and `(--instance NAME)` after a named `instance_select`
|
||||
(instance.sh:49, :101). `${arr[*]:+...}` is safe under `set -u` on an empty array, so the default text is
|
||||
byte-identical, and :1951 still passes on it. On alpha, both commands in the hint now carry
|
||||
`--instance alpha`. The new subtest checks the exact refusal text and the `--plan` line, and `noLeak`
|
||||
covers the run. Mutant 1 (`inst=""`) would fail both asserts. The TOOLS.md sentence matches the code.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. This is older than this row: neither hint names the business. The usage line is `activate ID BUSINESS`
|
||||
(:39), but the hint reads `activate $TO --instance alpha without --sso first`, so pasting it as printed
|
||||
gives a usage error, not the first step. `$BUSINESS` is in scope at :845. Adding it to both commands, and
|
||||
the `acme` it puts in the three test strings, would fit a later row. It fails safe, since the usage error
|
||||
changes nothing.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Webby review: row 238 (#1744), round 2
|
||||
|
||||
Webby, 2026-10-21T06:52Z (from `date -u`). Author: Launchpad (launchpad-0025), revs 2697-2699. Request
|
||||
comment 29948 (sage-1800, request rev 2701, resolved to posted 2703). Candidate: manifest digest
|
||||
36b0adfa2e7b4807db26837c081eb418c926feb61baf2b9200c22a62eea7e32a, 3 paths, on launchpad/row238-r3
|
||||
a33597c33. Base c771a36f4 (row 234's landing) is its ancestor. Round 1 was APPROVE on bec7e499e at
|
||||
011d41ba6 (comment 29823). Scope: the merge only, after rows 232, 233 and 234 landed on these paths.
|
||||
|
||||
What I checked, read-only, with no runs and no worktree:
|
||||
- I took the manifest text from `queue show 238` (round 2), not from the author's packet. It hashes to
|
||||
the digest above, and each path matches `git show a33597c33:<path>`.
|
||||
- `git patch-id --stable` over the 3-path diff is b9d0bb0ee3c7 for round 1 (011d41ba6..bec7e499e) and for
|
||||
round 2 (c771a36f4..a33597c33). The change is the same, and the landed lines around it are untouched.
|
||||
- scripts/stack-release.sh stays 100755 on c771a36f4 and a33597c33.
|
||||
- Outside the 3 paths the commit adds only agents/launchpad/work/queue-238 files.
|
||||
|
||||
Launchpad's slot run of stack-release.test.mjs on the code blob was 196/196. External 25/25 and upgrade
|
||||
20/20 from round 2 stand, since row 234 changed neither file nor the fixture.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Round 1's N1 (the hint has no BUSINESS) stays on the #1744 carry list, out of this row's scope.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Webby review: row 238 (#1744), round 3
|
||||
|
||||
Webby, 2026-10-21T07:02Z (from `date -u`). Author: Launchpad (launchpad-0028), revs 2712-2713. Request
|
||||
comment 29956 (sage-1810, revs 2714-2715, posted as jarvis). Candidate: manifest digest
|
||||
1bf831f33501682a6498ed9c4900b42383ee1e8ad09905096d1a0f7062cfc7be, 3 paths, on launchpad/row238-r4
|
||||
4c90099ea, one commit on c515273db (row 240's landing). Round 2 was APPROVE on 36b0adfa (comment 29948).
|
||||
Scope (sage-1804): docs/TOOLS.md after row 240's landing.
|
||||
|
||||
What I checked, read-only, with no runs and no worktree:
|
||||
- The queue's round 3 manifest text hashes to the digest above, and each path matches
|
||||
`git show 4c90099ea:<path>`.
|
||||
- `git diff c515273db 4c90099ea -- docs/TOOLS.md` is the one :599 hunk only. Row 240's lines at :837 and
|
||||
:854 are untouched.
|
||||
- scripts/stack-release.sh (blob cbc99c13, 100755) and stack-release.test.mjs (ed86589c) are the blobs I
|
||||
approved in round 2 on a33597c33.
|
||||
- `git patch-id --stable` over the 3-path diff is b9d0bb0ee3c7, as in rounds 1 and 2.
|
||||
|
||||
No runs, per Sage's ruling for a doc-only merge. Sage's combined 228 and 238 gate covers the landing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
@@ -0,0 +1,52 @@
|
||||
# Webby review: row 243 (#1754), round 1
|
||||
|
||||
Webby, 2026-10-21T19:10Z (from `date -u`). Author: Morgana (morgana-0045). Request comment 30111 (revs
|
||||
2936-2937, posted by Sage as sage-2078a after Morgana's own attempt failed its credential check).
|
||||
Reviewer by sage-2066 and mos-1166. Brief: docs/plans/2026-10-21_gate-env-rows.md "The startup token
|
||||
refusal test sets its file mode". Candidate: manifest digest
|
||||
2432c4a7dbb64899be5b371e65f301c3abca9f1c30a3f110f4c252f3a74d4fdb, 5 paths, on morgana/row243
|
||||
6581f750e. That's two first-parent commits on a36523713: 137bda3af (the tests) and 6581f750e
|
||||
(BUILD.md).
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 243` (last round), not from the scratch path. It hashes
|
||||
to the digest above, and each path's sha256 from `git show 6581f750e:<path>` matches its line. All
|
||||
five paths are 100644.
|
||||
- a36523713..6581f750e touches exactly the 5 manifest paths. None is under src or scripts.
|
||||
- Overlap: refactor is now 9a8c8860d. a36523713..9a8c8860d changes none of the 5 paths.
|
||||
- I read the whole diff and the BUILD.md sweep, plus the stack-instances helpers (`run`, `homeEnv`,
|
||||
`mode`) and the T-I1 mode assertions (:86-92).
|
||||
- My own sweep at 6581f750e: every writeFileSync, mkdirSync or openSync in packages/*/tests with a
|
||||
group or other bit (66 hits). Leaving out executables at 0o755 and 0o700 leaves 9. Three are this
|
||||
row's fixes. host-tools :81 and :205 are acceptance cases. Three are adapter scripts at 0o644,
|
||||
which no mode check reads. One is the gate-lane umask probe. Separately, no test creates a
|
||||
group-writable or world-writable file or directory. That agrees with Morgana's list.
|
||||
|
||||
Sage's slot results (sage-2062, 18:55Z, by BUILD.md): on the base under umask 077 the files fail
|
||||
7/1, 2/1, 27/1 and 21/2, on the predicted assertions. The candidate passes 8/0, 3/0, 28/0 and 24/0
|
||||
under both 022 and 077. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- process.test.mjs:76-78. The write keeps its mode, and `chmodSync(file, 0o644)` follows it, so the
|
||||
file is too open under any umask, and credentials.mjs's 0600 check refuses it. `chmodSync` is
|
||||
imported (:4). This is the brief's change, word for word.
|
||||
- proof-cli.test.mjs:30. `chmodSync(file, mode)` runs inside the case map with each case's own mode,
|
||||
so the 0o644 `mode` case stays too open and the 0o600 cases are set explicitly. `chmodSync` was
|
||||
already imported (:10).
|
||||
- sso.test.mjs:361. The chmod comes after the write and before the `--sso` start that must refuse,
|
||||
and the later chmod to 0600 still follows. `chmodSync` was already imported (:10).
|
||||
- stack-instances.test.mjs:40-43. `underUmask` runs bootstrap.sh through
|
||||
`/bin/bash -c 'umask <mask>; exec "$@"'`, and every `bootstrap()` call goes through it with 022.
|
||||
So the 0644 assertions at :87 and :92 test the mode the source asks for, whatever the gate's
|
||||
umask. The new subtest (:100-108) pins the other side: under 077 the config and instance.json are
|
||||
0600, and the directories stay 0700. A later chmod-to-0644 "fix" in mosaic-config.mjs would fail
|
||||
it. The `config()` helper has no mode assertion and stays as it is, which is correct.
|
||||
- The brief's sweep is in BUILD.md: the affected tests, the unaffected ones with a reason for each,
|
||||
and the mode checks that have no refusal test. The test step ran both umasks, as the gate asks.
|
||||
- Hash pins: BUILD.md reports a basename grep with a positive control and no pins. I found no test
|
||||
that names any of the four files with a 64-hex hash either.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
None.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Webby review: row 244 (#1757), round 1
|
||||
|
||||
Webby, 2026-10-21T21:40Z (from `date -u`). Author: Morgana (morgana-0053). Request comment 30154,
|
||||
posted attempt sage-244-review-r1b. Request by sage-2185. Brief: docs/plans/2026-10-21_gate-env-rows.md
|
||||
"Gate runs use a fixture pi auth file" (DEFERRED 059577886, my N4 on row 197). Candidate: manifest
|
||||
digest 6e7e3e3c290fe5034cc8766b6dc7eeb3725a99f330837a357c66381d18be1b60, 8 paths, on morgana/row244
|
||||
94ab4c2fe. That's three first-parent commits on a36523713: 47148991b, 81e037d20 (the code) and
|
||||
94ab4c2fe (BUILD.md slot results only, +24/-1).
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 244` (last round), not from the scratch path. It hashes
|
||||
to the digest above. Each path's sha256 from `git show 94ab4c2fe:<path>` matches its line, and the
|
||||
modes match: test-release.sh and test-task.sh are 100755, the other six 100644.
|
||||
- a36523713..94ab4c2fe touches exactly the 8 manifest paths.
|
||||
- Drift against origin/refactor d37210288. a36523713..d37210288 changes none of the 8 paths. It also
|
||||
leaves release.sh, scripts/lib, compose.yaml, run-task.sh and mosaic-task.mjs alone. The one nearby
|
||||
path it changes is scripts/stack-release.sh (7247d80c7, row 196). That isn't a manifest path, and
|
||||
neither suite nor anything under scripts/testing names it. `git merge-tree` merges 94ab4c2fe onto
|
||||
d37210288 clean.
|
||||
- I read the whole diff, BUILD.md (rulings A and C, the Compose reach list, coverage dropped, the
|
||||
DEFERRED wording, the mutant table and the M-F correction), compose.yaml's PI_AUTH_FILE default and
|
||||
MOSAIC_ADAPTER pass-through, `within` in lane-state.mjs, and the requiredSkips filter.
|
||||
|
||||
Sage's slot results (sage-2147, on 81e037d20, by BUILD.md): gate-lane-auth 5/0, gate-lane-image 11/0,
|
||||
gate-lane 13/0, gate-lane-state 14/0, gate-lane-fixtures 8/0, test-task.sh 91/0 with two live-skip
|
||||
lines, test-release.sh 14/0. M-G, M-L, M-A, M-T and M-F were all killed on their named tests, and M-F
|
||||
was also killed on the real test-release.sh (FAIL 3). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- lane-docker.mjs:32-36, laneAuthFile. An unset or relative PI_AUTH_FILE, the host default, a path
|
||||
whose realpath is the host default, a missing path, a directory, and a symlink escaping the temp
|
||||
root all refuse. realpathSync on both sides plus `within` handles a symlinked temp root. An unset
|
||||
value throws inside the try and reaches the same refusal. It runs only on `compose run` (:43),
|
||||
after the option check, so `info` and `image inspect` are unchanged. The CLI entry passes
|
||||
process.env (:66).
|
||||
- gate-lane.mjs:112-114 and :184. cleanEnv sets PI_AUTH_FILE to `<tempRoot>/pi-auth.json`, and
|
||||
runGate writes it `{}` with `wx` and then chmods to 0600, so the umask can't widen it. The kept list
|
||||
doesn't include MOSAIC_LIVE_PROVIDER, and test 2 checks that it gets dropped.
|
||||
- test-task.sh:19-23 and test-release.sh:16-20 write their own 0600 fixture under SANDBOX, which is
|
||||
`mktemp -d` under TMPDIR. In the lane that's the temp root, so the guard accepts it. Outside the
|
||||
lane, the legacy gate is covered too. Writing a real file matters, because a missing bind source
|
||||
makes Docker create a host directory. BUILD.md says why.
|
||||
- test-release.sh:69. `"adapter":"mock"` carries the ruling A comment. The three activations pass
|
||||
MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK. Fault injection still refuses on its random expectation, and
|
||||
M-F proves the mock's fixed reply can't pass that check.
|
||||
- test-task.sh. The hello and wrong-marker run records move to mock-adapters.json, which the seam
|
||||
block writes first (:159). Fork recall (M11) and user recall (M16) run only under
|
||||
MOSAIC_LIVE_PROVIDER=1. The skip line "skip live provider cases (row 244: ...)" doesn't match the
|
||||
requiredSkips pattern (gate-lane.mjs:56, docker|daemon|sandbox|systemd|credential), so the gate
|
||||
doesn't count it as a missing required case. The new "skip run-record cases (docker unavailable)"
|
||||
does match, so a Docker-less gate still fails. With live on, the suite leaves PI_AUTH_FILE alone,
|
||||
and test 5 asserts the operator's file reaches every run.
|
||||
- gate-lane-auth.test.mjs. Tests 3 and 4 read what each suite passes to Compose through a fake docker
|
||||
that never opens the host file, which is the brief's test. They also assert the adapter set is
|
||||
{mock}, so a suite drifting back to pi fails.
|
||||
- gate-lane-image.test.mjs passes run.env to the three guardDocker compose calls. Its refusal loop
|
||||
calls guardDocker without env, but none of those argv reach the auth check, so the default doesn't
|
||||
matter there.
|
||||
- The coverage loss is stated. test-release no longer proves the pi image reaches the provider, and
|
||||
the gate no longer proves M11 or M16 against a real model. The DEFERRED wording for Sage is in
|
||||
BUILD.md. I agree that it needs an entry. A real `release.sh activate` and an operator run with
|
||||
MOSAIC_LIVE_PROVIDER=1 are now the only proof of those paths.
|
||||
- Hash pins: no file under packages/*/tests or scripts names any of the changed files next to a
|
||||
64-hex hash.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. HOST_PI_AUTH is a literal in lane-docker.mjs:6 and gate-lane-auth.test.mjs:12, copied from
|
||||
compose.yaml:43. If someone changes the compose default, the guard's host-path comparison goes stale
|
||||
without failing a test. The guard doesn't depend on that literal, though, because `within(root, real)`
|
||||
refuses any path outside the temp root, wherever the host file lives. No change needed.
|
||||
|
||||
N2. At landing, the DEFERRED entry 059577886 moves to Done (brief), and Sage adds the
|
||||
"Real-model suite cases run outside the gate" entry from BUILD.md. Both are landing steps, not
|
||||
manifest paths.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Webby review: row 247 (#1749), C2-20b, round 1
|
||||
|
||||
Webby, 2026-10-21T11:55Z (from `date -u`). Author: Gosalyn. Request comment 30042 (revs 2846-2847,
|
||||
sage-1976, posted by Sage as jarvis). Brief: docs/plans/2026-10-20_case2-rows.md "C2-20b", blob
|
||||
f7c4cfbff43a. It covers my row 221 notes N1, N2 and N4 (webby-0073). Candidate: manifest digest
|
||||
6d3f279f692be2cbfaeca9ae36c3a8404d4cae110a2c005150b89803b247263e, 4 paths, on gosalyn/c2-20b 6d43618a3.
|
||||
That's one commit on 335bb3e30.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 247` (round 1), not from the scratch path. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 6d43618a3:<path>` matches its line.
|
||||
- 335bb3e30..6d43618a3 touches exactly the 4 manifest paths. The script stays 100755.
|
||||
- Overlap: the refactor tip is 5c6f73ed7. 335bb3e30..5c6f73ed7 changes none of the 4 paths.
|
||||
- Outside readers: `git grep case2-persona-import` over packages/*/tests/* and scripts/test-*.sh hits
|
||||
only the test file and its own fixture tree. No hash pin.
|
||||
- I read the script and test diffs, the new fixture, BUILD.md and the brief.
|
||||
|
||||
Gosalyn's slot run on grant sage-1971, 11:49:04-11:49:22Z: 14/0, and M1-M4 each killed (BUILD.md).
|
||||
Run 1 was red from the dropped space after the comma. The fix put the space back, and the test expects
|
||||
it (`"-1001", 29 bytes`). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- N1. The per-channel line prints `c.where` and `c.channel` through JSON.stringify
|
||||
(case2-persona-import.mjs:248). `where` is the fixed prefix plus the platform key (:149, :155-157), so
|
||||
both config keys are quoted. That's the only line that prints config text. The profile name comes from
|
||||
pairing's PROFILE_ID check. The new test writes a newline into both keys, expects one quoted line, and
|
||||
checks that no line starts with the forged `staged:` or `pairing:` text. M1 and M2 each fail it.
|
||||
- N2. The dry run calls personaTextFault on each persona after its count line (:235-236), prints the
|
||||
fixed reason, and refuses after the loop (:238), so both profiles' faults show before exit 1. Nothing
|
||||
is written. The new test takes 8001 two-byte characters (16002 bytes, under soul()'s 32000 cap at
|
||||
:130), expects the "over 8000 characters" line and exit 1, and expects exit 0 at exactly 8000. The
|
||||
heading test now expects the fence line and the refusal text. M3 fails both. The header (:21-25) now
|
||||
says the dry run refuses and that over four times the fence in bytes refuses before any count, which
|
||||
is what :130 does.
|
||||
- N4. privateDir turns an EEXIST from mkdir into a named Refusal and rethrows anything else
|
||||
(:189-194). mkdir-race.mjs patches fs.mkdirSync and calls syncBuiltinESMExports, so the script's
|
||||
named import (:29) gets a real kernel EEXIST, the same way trace.mjs hooks reads. The test hits it at
|
||||
stage (the staging dir, :255) and at install (`personas/<business>`, :303), checks the exact stderr,
|
||||
and checks that each raced dir is empty. M4 fails it.
|
||||
|
||||
The JSON.stringify residual (DEL, C1, U+2028/9 and bidi left raw) is recorded in BUILD.md under
|
||||
sage-1964. None of those ends a line on a terminal, so no forged line is possible. A bidi control can
|
||||
only reorder the display of that one line, and the keys come from Jason's own Hermes config. I accept
|
||||
the limit as ruled.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. BUILD.md's mutant table puts M1 and M2 at line 237. At the candidate, the per-channel line is 248.
|
||||
237 is where that line sat before this row's dry run change. The mutants match exact strings, so the
|
||||
runs aren't affected. Only the table's line numbers are off.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Webby review: row 248 (#1750), round 1
|
||||
|
||||
Webby, 2026-10-21T13:14Z (from `date -u`). Author: Sage. Request comment 30061 (sage-2001-review248,
|
||||
sage-2006). Reviewer by mos-1166. Brief: docs/plans/2026-10-20_host-credentials.md "Machine grant row b
|
||||
follow-up: the held stubs end with their test", blob 84bb8577fb23. Candidate: commit 4d99c178b, one
|
||||
commit on 540239b6b.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- The candidate kind is commit, digest 4d99c178b3fc92f8fa48acd487f873fce8ffc9e3. I read it from the
|
||||
canonical checkout. sage/row248 isn't on origin, so the commit exists only there for now.
|
||||
- 540239b6b..4d99c178b changes one file, packages/bus/tests/grant-executor.test.mjs (+3/-2). Nothing
|
||||
under src changes, as the brief requires.
|
||||
- Overlap: refactor is bc579f7a8, and 540239b6b is its ancestor. 540239b6b..bc579f7a8 changes
|
||||
docs/guides/host-run-sheet.md and the host-credentials plan, not the test file.
|
||||
- I read the stub fixture (test:52-96) and the three tests that set a hold flag (:427/:435, :675/:683,
|
||||
:832/:839), plus the X5 test around :931.
|
||||
|
||||
Sage's gate (row note, 13:05-13:07Z at 4d99c178b): three whole-file runs at 37/0 with 0 stubs left,
|
||||
the bus suite at 596/0, and the hold-add mutant leaves 2 stubs. Before the fix, 4 runs left 7. I ran
|
||||
nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- The fix. Both hold loops (:67 add, :72 remove) now run only while `release` is missing and `$D`
|
||||
is a directory. `D` is the mkdtemp dir (:63, :100), which `t.after` removes (:101). So once the
|
||||
dir is gone, each loop ends within one 50 ms poll.
|
||||
- After the loop. With the dir gone, add finds no `skip`, `fail-add` or `noop-add`, and its append to
|
||||
`$D/granted` fails, so it exits nonzero. Remove's `grep -v ... > granted.tmp` fails in the same
|
||||
way. Both stubs end, which is all the brief asks. Their exit codes don't matter, since the test that
|
||||
held them has already finished.
|
||||
- The test runs. While a test runs, its dir exists, so the new check can't end a hold early. The
|
||||
three holding tests write `release` themselves (:435, :683, :839), so their behavior doesn't change.
|
||||
- The comment at :54 says both stubs also stop when the dir is gone, and it cites #1750. The :432
|
||||
comment goes to row 249, as Sage said.
|
||||
- The gate matches the brief: three whole runs with no stub left, the bus suite green, and a mutant
|
||||
that drops the hold-add check leaves a stub.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. There's a second leftover of the same kind, though it doesn't loop. The X5 test at :934 runs
|
||||
`sleep 30` through ForceCommand and the sudo stub (`exec "$@"`, :71), then revokes. The broker
|
||||
rejects the run, but under the brief's own sshd reasoning (no SIGHUP for a non-pty command), the
|
||||
remote `sleep 30` can keep running after the test ends and the daemon is SIGKILLed (:88). It ends on
|
||||
its own within 30 s. A pgrep on TMPDIR may not see it, since its argv is just `sleep 30`. I didn't
|
||||
run anything to confirm it lingers. If a lane cleanup that scans by cwd or process tree catches it in
|
||||
proof 3, a shorter sleep, or a stub that also exits once the dir is gone, would fix it. That could go
|
||||
in row 249 or a later row, not here.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Webby review: row 252 (#1753, #435 J2) design, revision 1
|
||||
|
||||
Webby, 2026-10-21T18:59Z (from `date -u`). Author: Gander (gander-0033). Gate owner Sage (sage-2053),
|
||||
reviewer by mos-1193. Brief: 640aa03f2:docs/plans/2026-10-21_console-inbox-approve.md, items 1-6 of
|
||||
"What the design must settle". Candidate: agents/gander/work/queue-252/DESIGN.md at d218e14bd on
|
||||
gander/row196-r2, sha256 d85bd103bc0a2cc96fe47b9f8a6d4e0e16d90ee398ef8ffaa2fe1c55d926d23f (matches
|
||||
Gander's). The branch tip is now ab1f0e576. d218e14bd..ab1f0e576 doesn't touch queue-252/.
|
||||
|
||||
What I checked, without runs: the design against the row 196 build it names (64539c96b on a36523713).
|
||||
I read #approvedGrant, the decision.resolve case, #human, verifyPrincipal and verifyHuman, the runtime
|
||||
binding, the Console's send wiring and CSRF checks, sso-config.mjs, the decision_events DDL, and every
|
||||
reader of a resolution's `via`. Line numbers below are at 64539c96b.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
1. Scope. Grant requests only, with the reason given: a wider Console write widens the security slice.
|
||||
Eligibility matches what #approvedGrant (broker.mjs:686-693) already requires: credential.mint,
|
||||
route_to human, and a grant binding on the original raise. It adds exactly two option keys, with
|
||||
approvalChoice naming one. A raise allows 2 to 9 options (:2730), so the two-key rule actually
|
||||
filters, and Deny is the real other key, not a guessed `no`.
|
||||
2. Identity. (provider, iss, sub) from the live row 192 session. Session.name, headers and body fields
|
||||
are refused. It extends row 196's verifyPrincipal the way row 196's `user` kind does (principal.mjs:
|
||||
20-39): an exact same-tree `cli.mjs serve` parent, reread after the walk, with its own nonce
|
||||
namespace. Agent, seat, user and tester callers are refused. The same-uid residual is stated, not
|
||||
hidden, and it repeats row 196's provenance statement word for word.
|
||||
3. Write path. A restricted Console cap and its own resolve op, sharing the decision.resolve
|
||||
transaction. #human (:470-473) and bindHuman are left alone. decision_events.via has no CHECK
|
||||
(migrations/3b.sql:62-69; the only `via` CHECK is :100, on another table), so `via='console'`
|
||||
needs no migration. That backs the design's "no new schema column". The subject goes in the
|
||||
human.input body in the same transaction. #approvedGrant gains a console branch tied to that
|
||||
provenance, not a wider string check.
|
||||
4. Guards. Origin and Sec-Fetch-Site follow the existing send boundary (serve.mjs:177-178). There is
|
||||
a two-step confirmation bound to the session, decision, choice and digest, used once and capped,
|
||||
with no eviction. The refusal table gives a status, code and message for CSRF, confirmation, not
|
||||
open, stale key, double click, read-only and no-send. Its codes match webui bus.mjs STATUS where
|
||||
both have them (human-required, peercred-required and read-only are 403, outcome-unknown is 503).
|
||||
It never retries automatically after an uncertain transport.
|
||||
5. Without --sso, the buttons are hidden, not disabled, and the `mosaic decide` lines stay. The design
|
||||
says why: a disabled button would suggest sign-in alone grants write authority.
|
||||
6. Tests use stand-ins only: row 192's fake provider, the test bus, row 196's peer fixtures and the
|
||||
scratch TMPDIR. There are 17 mutants, each tied to one boundary.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Section 2 point 5 says "allowed owner subject" but doesn't say what makes a subject an owner.
|
||||
console.sso subjects are {sub, name} only (sso-config.mjs:66), and the key check refuses any other
|
||||
field. Row 196's design (:124-125) says any subject in console.sso.subjects reaches owner bus calls.
|
||||
I'm reading the design the same way: every configured subject is an owner, with no new config field.
|
||||
The build should say so. That means every configured subject can approve a credential mint. That's
|
||||
consistent with today's `--send` owner writes, but Jason should see it stated in the REQ-WEB-2
|
||||
amendment.
|
||||
|
||||
N2. Order inside the shared transaction. Today decision.resolve calls #approvedGrant (broker.mjs:2884)
|
||||
before it writes the human.input event (:2889). If the console branch requires the matching human.input,
|
||||
it has to be written first, or the local `grant` is null and neither setImmediate executor start runs.
|
||||
The approval would then record but never act. The design's own test ("approval consumable once by
|
||||
existing grant path", host and host-enroll starts after commit) catches this, so the manifest review
|
||||
will check that it asserts the executor start, not just the event.
|
||||
|
||||
N3. Other readers of `via` the design doesn't list. webui public/bus.js:194 sets `by` to 'broker' for
|
||||
any via other than 'cli', so a Console approval would show as the broker in the trail. That breaks
|
||||
"Trail output shows this provenance". The close notice's from_run is 'human-cli' for any human
|
||||
(broker.mjs:2920). git-socket.mjs:274 RESOLVED_VIA falls back to `via console`, which is fine.
|
||||
landing.mjs:107 and #protectedEnd (:1427) keep requiring 'cli'. That is correct, since neither a merge
|
||||
decision nor a role.revoke decision is eligible. The build should name each of these.
|
||||
|
||||
N4. Today's `--sso --send` owner writes already reach the bus through human-cli and record via=cli
|
||||
(webui cli.mjs:80, bus.mjs:32, runtime.mjs:226). So #approvedGrant's `via === 'cli'` check doesn't by
|
||||
itself tell a terminal from the Console. That's acceptable, because the Console wires only a fixed verb
|
||||
list. The build should keep decision.resolve off that list, so a Console grant answer can only take
|
||||
the restricted path. A test or mutant that checks the list would hold that.
|
||||
|
||||
N5. readProcess keeps only an allowlist of environment keys (human.mjs:34). The Console nonce and the
|
||||
provider, iss and sub keys need adding there. The J5 mutant (other nonce namespace) depends on that
|
||||
list, so the build needs both.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user