Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7239bb66e | ||
|
|
0b6d29e909 | ||
|
|
0075be3581 | ||
|
|
9a16d7795d | ||
|
|
818b79696d | ||
|
|
088087ac96 | ||
|
|
a0fbefd48d | ||
|
|
f24f2cd40d | ||
|
|
9ba31b5b3f | ||
|
|
9db578ff54 | ||
|
|
8eb9ddd12c | ||
|
|
ae3c36f248 | ||
|
|
a2af7ab495 | ||
|
|
aa1bd209e3 | ||
|
|
ace27dc5eb | ||
|
|
a4a42dbac2 | ||
|
|
a31f1a0f8b | ||
|
|
992650232f | ||
|
|
a7661ada06 | ||
|
|
f871cb3096 | ||
|
|
0bbd23c619 | ||
|
|
0217b71c8f | ||
|
|
c6f528a32f | ||
|
|
96f243fafc | ||
|
|
c862699352 | ||
|
|
48ebee0ffe | ||
|
|
66bdd84b8f | ||
|
|
6966164f0c | ||
|
|
53bb594621 | ||
|
|
5e01c36833 | ||
|
|
9f56d2098d | ||
|
|
0599b119f4 | ||
|
|
b416d2ede9 | ||
|
|
5fc73a729c |
@@ -16,6 +16,17 @@ 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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Bushroot
|
||||
|
||||
Builder seat, Claude Haiku 5.5, started by Sage under mos-1008 item 2 (a low-level model under Q177) for the row
|
||||
that retires the control board writer on 7331 (#1696).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `bushroot/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,872 @@
|
||||
# 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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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,59 +0,0 @@
|
||||
# Review: Gizmoduck federation design r2 and r2.1 (duckworth-0002)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md at head 5ad67f671 (r2.1, parent 60b9b112a, r2 parent 6925f78f0).
|
||||
DESIGN.md line numbers below are at 5ad67f671. The r2.1 diff only moves the uid to row 190b, makes R7 depend on row 190 and
|
||||
puts P19 333 on the path. I checked it and found no issue. Code at origin/refactor
|
||||
9ae8c73ac. Against duckworth-0001 (REVIEW.md, b402197ff).
|
||||
|
||||
## Verdict: changes, B6 only
|
||||
|
||||
B1 to B5 are resolved. r2 adds one new blocker of its own. Fixing B6 and C1 as line edits plus their
|
||||
test lines would earn approval.
|
||||
|
||||
## r1 codes
|
||||
|
||||
- B1 resolved (3.6). The walk is the human's (human.mjs:73-88), the user CLI has its own nonce, the Console
|
||||
parent is checked against the bus's tree (webui-service.sh:3 confirms `cli.mjs serve`), and the user id
|
||||
binds only with a live grant. R3 has both mutants. Condition C1 below.
|
||||
- B2 resolved (3.6 list, 4). `#owner`, the `#threadFor` refusal plus `message.send` being off the list (two
|
||||
walls), the sender fields and the audit are all in. The 48 `s.human` and 41 `s.tester` line counts match.
|
||||
- B3 resolved (3.5). There is one level with seven verbs. Each read has a filter, and everything else is
|
||||
`access-refused`. The cited lines match (broker.mjs:2143-2146, :2288-2295, :2318-2322, :2331-2334,
|
||||
:2351-2420, :1203-1207, :2398).
|
||||
- B4 resolved (3.7). There are three triggers. `threadsState` (:390-393) is computed per call, so a restart
|
||||
doesn't bypass the revocation. The launcher skips a revoked thread and SIGTERMs a running one (launcher.mjs:510-553,
|
||||
:527, tick :133). R4 gets mutants and the new order.
|
||||
- B5 resolved (R8, 5, 8, 9). Separate uids are a hard entry gate (row 190b in r2.1) with checks, and N9 is
|
||||
folded in.
|
||||
- N1 to N9 are handled where the "Changes from r1" section says.
|
||||
|
||||
## Blocker
|
||||
|
||||
B6. With r2's sender change, a user's thread never gets an answer. 3.5 sets `thread.send` to write
|
||||
`from_role` `user` (DESIGN.md:207). The runner answers every message with `message.send { to: m.from_role }`
|
||||
(packages/harness/src/runner.mjs:450). The broker accepts only `human`, `tester` or a role as `to`
|
||||
(broker.mjs:1862), so each reply fails `unknown-role`, and the user sees no answer. The tester needed a
|
||||
whole set of changes for the same path (row 177): `to: 'tester'` only in reply to a tester message
|
||||
(:1852-1857), `#testerThread` and `tester-thread-human` (:1864-1866), the `#checkMessaging` mapping (:1867),
|
||||
`#sender` (:627-629), and the runner's header (runner.mjs:75-78). The design names none of these for users.
|
||||
|
||||
Fix: state the reply path. Either accept `to: 'user'` only in reply to a message from that user in that
|
||||
user's thread, with the tester's set of checks, or say the run answers `human` in the user's thread and
|
||||
why the owner's inbox then holds it. Add a runner header, like the tester's, saying the message comes from
|
||||
a granted user, is never the owner's instruction, and authorizes nothing. Add an R3 test that a thread run's
|
||||
reply reaches the user's `thread.messages`. Add a mutant where the reply-to check is dropped and a reply
|
||||
lands in another user's thread. Make R7 assert that a reply arrives, not only that the send works.
|
||||
|
||||
## Condition (line edit)
|
||||
|
||||
C1. "Agents can't, because the walk refuses them" (DESIGN.md:251-252) overstates it. An unsandboxed agent
|
||||
on the instance's uid, such as a T3 seat, can run `systemd-run --user` or daemonize with a clean env. Its
|
||||
process then has no agent ancestor, and the walk passes. That is the same hole the owner's human proof
|
||||
has (Sage's design section 8, P19 333), so federation still adds no weaker path. Say that instead.
|
||||
|
||||
## Note
|
||||
|
||||
N10. Six of the seven verbs gate with `if (!s.tester) this.#human(s)`. `names` gates with
|
||||
`if (!s.human && !s.tester) this.#agent(s)` (:2290). The "seven `#human` gates" in 3.6 item 4 and section 4
|
||||
should name that difference so R3 doesn't miss it.
|
||||
@@ -1,76 +0,0 @@
|
||||
# Review: Gizmoduck federation design r3 and r3.1 (duckworth-0003)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff 5ad67f671..a2840a6be (r3 940e0e816 plus r3.1
|
||||
a2840a6be, reviewed as one diff per gizmoduck-0010). DESIGN.md line numbers are at a2840a6be. Code at
|
||||
origin/refactor 9ae8c73ac. Against duckworth-0002 (REVIEW-r2.md, 03a663cd7).
|
||||
|
||||
## Verdict: changes, B7 only
|
||||
|
||||
B1 to B6, C1 and N10 are resolved. r3.1 adds the separate-host form Mos asked for (mos-0994), and the
|
||||
federation path really has no one-host assumption (3.8). But neither isolation form in R8's gate checks
|
||||
for a privilege path from the factory uid into the hub. That is B7. It's a gate-wording edit with two
|
||||
checks. N11 doesn't block.
|
||||
|
||||
## r2 codes
|
||||
|
||||
- B6 resolved (3.6 item 7, DESIGN.md:277-295). `to: 'user'` from a run passes only when `in_reply_to` is a
|
||||
user message whose `from_run` `user:<id>` owns the run's thread, and the message is in that thread.
|
||||
Otherwise the send fails `user-reply-required`. `#userThread` sits beside `#testerThread` (:631).
|
||||
`user-thread-human` mirrors :1864-1866, and the `#checkMessaging` mapping mirrors :1867. A human or tester
|
||||
sending `to: 'user'` fails `unknown-role`. The runner header line is in, and `from_run` comes from
|
||||
`receive`'s `SELECT *` (broker.mjs:1932, checked). The R3 tests and mutant and the R7 reply assertion are
|
||||
in.
|
||||
- C1 resolved (DESIGN.md:252-260, the residual-risk paragraph). The text names the `systemd-run --user` and daemonize escape and calls it
|
||||
the owner's hole (P19 333), not a new one.
|
||||
- N10 resolved (3.6 item 4). The `names` gate at :2290 is called out with its order.
|
||||
|
||||
## r3.1 (mos-0991, mos-0994)
|
||||
|
||||
- 3.8 (DESIGN.md:335-354) holds. I found nothing in the path that crosses buses or filesystems. Each
|
||||
Console spawns its own CLI, access.json is on the receiver, and `federation.peers` holds URLs only.
|
||||
- R8 (DESIGN.md:431) accepts either form, and sections 4, 5, 8 and 9 match it. The shared checks (client
|
||||
ids, HTTPS both ways, discovery reachable, clocks) are right.
|
||||
- Q1 and Q2 are recorded as rulings, not settled by the design. N8 stays with Mos until Mos confirms that
|
||||
"P2P" means person-to-person.
|
||||
|
||||
## Blocker
|
||||
|
||||
B7. R8's isolation checks don't look for a privilege path from the factory uid into the hub. That gap is
|
||||
in both forms.
|
||||
- Separate uid. The checks are a refused socket call (human.mjs:67) and an unreadable data root and config
|
||||
dir. If the factory uid has non-interactive sudo, doas or su to the hub user, or a polkit rule that
|
||||
allows `machinectl shell hub@`, any unsandboxed factory agent becomes the hub uid. Then it can run the
|
||||
hub's human-cli and write access.json, and every other check still passes. I missed this in r2 when I
|
||||
marked B5 resolved.
|
||||
- Separate host. "Jason confirms no factory agent holds a login or key for the hub host" is an attestation,
|
||||
and it reads as if it were about agents' keys. Jason's own ssh key in the factory uid's `~/.ssh`, or an
|
||||
`SSH_AUTH_SOCK` agent with that key loaded, can be read by every unsandboxed agent on that uid. If that
|
||||
key logs in to the hub host, it does the same harm as the sudo path.
|
||||
|
||||
Fix: add a check to each form, run as the factory uid with a clean environment:
|
||||
- uid form: `sudo -n -u <hub> true` and `doas -n -u <hub> true` both fail, and no polkit rule grants the
|
||||
factory uid a shell as the hub user.
|
||||
- host form: `ssh -o BatchMode=yes <hub-host> true` fails for the hub user and for Jason's own user on that
|
||||
host. Run it with and without the factory session's `SSH_AUTH_SOCK`. Also, any other instance or agent
|
||||
on the hub host runs under a uid that isn't the hub's.
|
||||
|
||||
Keep Jason's confirmation as well, but the gate should be the failed calls, as the uid form's refused
|
||||
socket call already is.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N11 (still open at a2840a6be). `#waiting` (broker.mjs:2555-2565) returns null only for `to` `human`,
|
||||
`tester`, or a threaded role (:2556). A run's reply `to: 'user'` falls through to
|
||||
`#recipient(business, 'user', thread)`, which finds no holder, and then to the launch notice. `told` is
|
||||
true when `launch.by` is set and the run isn't `launch.by` (:2562). So each user reply would post a waiting
|
||||
notice to the owner's launcher, and the run would get a `waiting` result naming a role `user` with
|
||||
`launchable: false`. Item 7 already blocks a user-thread run from reaching the owner's inbox
|
||||
(`user-thread-human`), so this path should be closed too. Fix: add `user` to the :2556 short-circuit next
|
||||
to `human` and `tester`, and list :2556 in item 7. Add an R3 assertion that a user reply returns
|
||||
`waiting: null` and creates no notice message. The planned "reply reaches `thread.messages`" test passes
|
||||
even with the notice, so it won't catch this.
|
||||
|
||||
N12. `thread_shares` is append-only (Q2, DESIGN.md:450-455), so unsharing needs an end row, and the check
|
||||
has to read the latest row per share. That belongs to row 193's design, not this one. I'm noting it so R3's
|
||||
user-thread checks don't assume shares only grow.
|
||||
@@ -1,57 +0,0 @@
|
||||
# Review: Gizmoduck federation design r4.1 (duckworth-0007)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff a659e41f3..73677b87d (r4.1, gizmoduck-0016).
|
||||
DESIGN.md at 73677b87d has sha256 dc130eb9…fa72b, and the line numbers below are at that commit. The diff
|
||||
touches only DESIGN.md and one SESSIONS line. Against duckworth-0005 (REVIEW-r4.md, 4966136d4).
|
||||
|
||||
## Verdict: changes, one point of B8 left
|
||||
|
||||
The privilege-path table (DESIGN.md:520-545) is the right shape. Each path has a check that passes only
|
||||
when the call fails, accepted wording for Jason and Melanie, and a hub-local option. The TLS front end,
|
||||
N13, N14 and N15 are resolved. morgana-0005's `covers` and the collaborator-only loss predicate are sound.
|
||||
One part of my B8 fix didn't carry over: the issuer's own host.
|
||||
|
||||
## r4 codes
|
||||
|
||||
- B8, the issuer: one point open (below). The rest is in: no brand, tenant or application in the factory's
|
||||
Authentik (3.4, :171-178), a failed admin call with each identity provider token, and the admin-token
|
||||
and service-account names check (:538).
|
||||
- B8, the TLS front end: resolved (:539). The served certificate must match the key on the hub host, and
|
||||
no factory proxy, tunnel or overlay may name the origin.
|
||||
- N13 resolved (R8 row, :491). Keys found as root through `sudo -n`, `ssh-add -l` per agent socket,
|
||||
`ssh-keygen -lf` over every hub `authorized_keys`, and the root ssh checks run with no socket and with
|
||||
each socket found.
|
||||
- N14 resolved (:443-450, :487). R3 depends on row 196. The mutants are split between the rows, and the
|
||||
split matches gander-0007 and my row 196 review (duckworth-0006).
|
||||
- N15 resolved (DNS row :541, CA row :540).
|
||||
|
||||
## Blocker
|
||||
|
||||
B8 (one point left). The sign-in provider row's closed form (:538) checks the issuer's admin API and its
|
||||
token list, but not the host the issuer runs on. Example: Pocket ID on m-docker0, or on any PVE VM. The
|
||||
admin call fails with every token, so the row closes. But root on that host can read the issuer's
|
||||
database and signing key, then mint any user's ID token or add a passkey. ops-01's measurements give the
|
||||
factory root on every PVE guest today. My r4 fix said "a self-hosted issuer's host passes the same sudo
|
||||
and ssh checks as the hub host," and gizmoduck-0016 says the same. The text at 73677b87d doesn't (no
|
||||
match for it in the file).
|
||||
Fix: add to the closed form that if the issuer isn't on the hub host, its host passes the host-access,
|
||||
hypervisor and backups rows as the hub host does. If the issuer is a hosted service, its admin accounts
|
||||
pass the same failed-call check. Either case gets one line in the table.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N16. The dependency note (:544-545) names only TLS. Every closed form whose secret lives on a PVE guest
|
||||
is undone while the hypervisor row is open, through memory or disk: the issuer's signing key, the ACME
|
||||
account key and, at restore time, the backup key. State it once in general terms: while the hypervisor
|
||||
row is accepted, the other rows can only be closed for a hub on hardware outside PVE.
|
||||
N17. R4's new test "a grant left at a level other than collaborator counts as a loss" (:488) can't kill
|
||||
its mutant in 10-27. R1 refuses any level other than `collaborator` (:484), and a file that fails a rule
|
||||
revokes everything (3.5). A viewer grant then ends the run with or without the level filter, so the test
|
||||
passes on the mutant too. The test needs the viewer level, which row 193 adds (:555-557). Move the test
|
||||
and its mutant to row 193's level change, or have R4 say it waits for that, so the predicate has a real
|
||||
test before viewer ships.
|
||||
N18. 3.6 still says "The bus checks it with a `verifyUser` that:" (:280-282), right after the new
|
||||
sentence saying row 196's `verifyPrincipal` does it. Rename it so R3 doesn't build both.
|
||||
N19. Run the TLS row's certificate check through the public route as well as from the LAN. A proxy that
|
||||
only the public path goes through won't show in a LAN check.
|
||||
@@ -1,47 +0,0 @@
|
||||
# Ack: Gizmoduck federation design r4.10 and r4.11, R8 as a recorded acceptance (duckworth-0034)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: agents/gizmoduck/work/federation/DESIGN.md on
|
||||
gizmoduck/federation-design. I read the diff a1daa3c07..3568441ab (r4.10, gizmoduck-0034) and
|
||||
3568441ab..b93f098f0 (r4.11, gizmoduck-0036, mos-1063). Each commit touches only DESIGN.md and SESSIONS.md.
|
||||
I checked the quotes against DECISIONS.md:1511-1541 in jason-master-restart at 1c378b4, read-only.
|
||||
|
||||
## Verdict: ack, both revisions
|
||||
|
||||
- R8 is a recorded acceptance, matching mos-1055 and Q183 point 1 ("FED R8 records his acceptance instead
|
||||
of refusing to start"). Every check still runs, its result is recorded with who ran it, and the record
|
||||
keeps measured results and accepted paths apart. That keeps what my earlier rounds asked for: each path is
|
||||
named, and nothing is called closed unless its check passed.
|
||||
- The quotes match DECISIONS.md word for word: Q182 at :1517 and :1519, Q183 at :1537, and Q4 at :1541.
|
||||
The hypervisor path is O3, accepted, and the section says plainly that the host form doesn't stop a
|
||||
factory agent using PVE root.
|
||||
- The .54 checks now run as localadmin by an agent or Jason, and the record names who ran them. This is
|
||||
sound now that nothing gates on them. #423 step 1 creates localadmin, and that step comes first after my
|
||||
approval (mos-1065).
|
||||
- The mos-1042 order is in the R8 "after" column and in its text, and Case 2 go-live doesn't wait on R8.
|
||||
- Melanie gets the wording only through Jason (#419), and r4.11 leaves #419 carrying only that.
|
||||
|
||||
Your call A: agreed, and Mos ruled the same way (mos-1063). Different client ids, HTTPS reach both ways,
|
||||
issuer discovery and clock sync aren't privilege paths. Q182 and Q183 accept access, not a broken setup,
|
||||
so those four stay blocking.
|
||||
|
||||
Your call B: both corrections are right. With the jason key, a factory agent gets localadmin's NOPASSWD sudo
|
||||
on .54 once #423 step 1 runs, and pve root reaches VM 1012's disk and console. So 3.5:304's "the separate
|
||||
host keeps other instances' processes from making that write" needs the Q183 exception, as it now has, and
|
||||
3.6 can't claim R8 proves no factory login.
|
||||
|
||||
r4.11: the stale host lines are gone, and the hub is .54, VM 1012, user `mosaic` (Q181) in 3.8, the R8
|
||||
row, section 8 and ruling 3. Using #416 in place of #418 matches the closed issue. Leaving #416 undated is
|
||||
right when there's no source.
|
||||
|
||||
## Notes (do not block, doc-only)
|
||||
|
||||
N1. Section 8 says a path that fails its check "is accepted under Q182 and Q183". Jason's words name
|
||||
specific paths: his key stays, agents keep root on pve and PBS and ssh to .53 and .54, and agents run on
|
||||
his workstation. A failed check outside those isn't in his words. Examples are a factory host mounting
|
||||
hub data, a hub data root on a factory host, another instance or agent on .54, or a provider admin token
|
||||
reaching the hub's client. The record should mark such a result "not named in Q182 or Q183". Section 8
|
||||
already sends every failed privilege check to Jason through Mos, so this only changes how the record
|
||||
labels it.
|
||||
N2. The quotes are called verbatim, but two of them are excerpts. Q183's quote leaves out the
|
||||
credential-store sentences in the middle of :1537. Q4's leaves out the first two sentences and the last
|
||||
three of :1541. Mark each omission with "…".
|
||||
@@ -1,58 +0,0 @@
|
||||
# Review: Gizmoduck federation design r4.2 to r4.4 (duckworth-0010)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff 73677b87d..b67bc6de9 (d51778391 r4.2, 17d69e660
|
||||
r4.3, b67bc6de9 r4.4, gizmoduck-0020). DESIGN.md at b67bc6de9 has sha256 2ca7812d…a713c601, checked. Line
|
||||
numbers are at b67bc6de9. Code at origin/refactor 9ae8c73ac. Against duckworth-0007 (REVIEW-r4.1.md,
|
||||
b14f11136).
|
||||
|
||||
## Verdict: changes, B9 only
|
||||
|
||||
B8 is resolved. So are N16 to N19. r4.2's wider "factory" is right, and it makes the gate stricter. B9 is a
|
||||
gap I should have caught in r3, when 3.6 item 7 first said a collaborator can't reach the owner's inbox. I
|
||||
found it while reviewing row 193 (duckworth-0008, B1).
|
||||
|
||||
## r4.1 codes
|
||||
|
||||
- B8 resolved (sign-in provider row). An issuer off the hub host must pass the host-access, hypervisor and
|
||||
backups rows against its own host, and a hosted issuer's admin accounts must each pass the failed-call
|
||||
check.
|
||||
- N16 resolved (the dependency note under the table). It now covers every secret on a PVE guest.
|
||||
- N17 resolved (R4 row). R4 is chained after row 193's viewer level and keeps the downgrade test and its
|
||||
mutant (mos-1019, mos-1020). Row 193 should drop its copy (duckworth-0008 N3).
|
||||
- N18 resolved (3.6, the proof paragraph). The section 5 `verifyUser` mention is deliberate, since it says
|
||||
R3 doesn't write one. Accepted.
|
||||
- N19 resolved (TLS row).
|
||||
|
||||
## r4.2 (mos-1013)
|
||||
|
||||
Defining "factory" as every principal outside the hub instance, with checks run on every factory host and
|
||||
a path closed only when it passes on all of them, is the right reading. W-jarvis's `jarvis@w-jarvis` key
|
||||
having root on `pve` shows why. Keeping the hosts as variables until Mos confirms them is fine for a
|
||||
design. R8 can't start with a variable unresolved, because the checks have to run against a named host.
|
||||
3.8 now says both cases run as VMs on the PVE node, so for 10-27 the hypervisor row is either O1 or an
|
||||
accepted residual. That is already Jason's (#419), and the table says so. Q1 is recorded as Q180.
|
||||
|
||||
## Blocker
|
||||
|
||||
B9. A user-thread run can still put a decision in the owner's inbox, and 3.6 item 7 says it can't
|
||||
(DESIGN.md:344-347). Item 7 copies the tester's `message.send` guard (broker.mjs:1866). It doesn't copy the
|
||||
tester's decision guard at :1683, `if (route === 'human' && this.#testerThread(s)) fail('tester-thread-human')`.
|
||||
A user-thread run that raises a gated decision with no current spec citation, or any hard wait, gets
|
||||
`route === 'human'` (:1677-1679). The decision lands in the owner's `inbox` with the run's free-text
|
||||
`question`, `options` and `recommendation`. R3's tests (R3 row) check `to: 'human'` sends only.
|
||||
There are two ways to resolve it. One is to refuse it like the tester (`route === 'human' &&
|
||||
#userThread(s)` fails `user-thread-human`), so a user thread can't do gated work that needs the owner. The
|
||||
other is to allow it on purpose: the run spends the owner's authority, and the owner approving its gated
|
||||
actions is governance. Sage has since ruled the second for row 193 (sage-1368, morgana-0008). Row 193 r3
|
||||
5.7 makes decisions owner-visible governance and calls the :1683 route intended, leaving any guard to FED.
|
||||
Fix: follow that ruling in item 7. Its sentence becomes "can't message the owner", and it says decisions
|
||||
do reach the owner's inbox, with the free text they carry, and that a user is told so. Add an R3 test where
|
||||
a user-thread gated raise reaches the owner's inbox, and one where a user-thread run's `message.send`
|
||||
`to: 'human'` is still refused. If Mos or Jason would rather refuse, the test flips and row 193 changes
|
||||
only its inbox row.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N20. The start-order line under the build table (DESIGN.md:510) still says "R4 when R3 and R5 do". R4 now
|
||||
also waits for row 193's viewer level.
|
||||
@@ -1,27 +0,0 @@
|
||||
# Review: Gizmoduck federation design r4.5 (duckworth-0012)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff b67bc6de9..acd5f1f42 (one commit, gizmoduck-0022).
|
||||
DESIGN.md at acd5f1f42 has sha256 99f5c4cd…e39cb8ed, checked. The commit also appends one SESSIONS line.
|
||||
Against duckworth-0010 (REVIEW-r4.4.md, 4cf3ce232).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
B1 to B9 are all resolved. R1, R2 and R5 can go to Sage's queue.
|
||||
|
||||
## r4.4 codes
|
||||
|
||||
- B9 resolved (3.6 item 7). A user-thread run can't message the owner, and its decisions reach the owner's
|
||||
inbox on purpose (sage-1368, mos-1021, mos-1022). The :1683 guard is deliberately not copied. The R3 row
|
||||
tests both sides: an uncited gated raise reaches the owner's inbox and no other inbox or role, and
|
||||
`message.send` `to: 'human'` is still refused. R6 adds the user notice.
|
||||
- N20 resolved (the start-order line under the build table). R4 waits for R3, R5 and row 193's viewer level.
|
||||
- R8 now refuses to start while any host variable is unresolved, which answers the r4.4 point on variables.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N21. The R6 notice names decisions only. Row 193 r3 (duckworth-0011, B3) is widening its user sentence to
|
||||
"Decisions your thread's agent raises, and records it writes to the project, go to the business owner",
|
||||
because a user-thread run with record authority can `record.write` (broker.mjs:1395-1407) and the owner
|
||||
reads that with `project.file`. Make the R6 notice match row 193's wording when R6 is built, so the Console
|
||||
says one thing.
|
||||
@@ -1,14 +0,0 @@
|
||||
# Review: Gizmoduck federation design r4.6 (duckworth-0019)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff acd5f1f42..1b32e65e3 (one commit, gizmoduck-0025).
|
||||
DESIGN.md at 1b32e65e3 has sha256 ebd7a291…8d678c14, checked. The commit also appends one SESSIONS line.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- N21 resolved. 3.6 item 7 and the R6 row carry row 193 r5's sentence (e2d161e2e, 5.7), word for word. R6
|
||||
tests that the notice equals it exactly.
|
||||
- The start-order line now matches the section 7 table. R1 starts now, R5 lands after R1, and R2 and R6
|
||||
wait on Sage's #412 phase 1.
|
||||
|
||||
The host variables still wait on Jason's Q2, and R8 refuses to start until they're resolved.
|
||||
@@ -1,24 +0,0 @@
|
||||
# Review: Gizmoduck federation design r4.7 (duckworth-0020)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff 1b32e65e3..2d3c3cfbd (one commit, gizmoduck-0026).
|
||||
DESIGN.md at 2d3c3cfbd has sha256 9f9791c1…80e74647, checked. The commit also appends one SESSIONS line.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
The host variables are replaced with Q181's hosts (mos-1030). Case 1 is W-jarvis, 10.1.1.53, VM 1008. The
|
||||
hub is W-Melanie-OC, 10.1.1.54, VM 1012. Case 1 and the Hermes agents are now one factory host, as the r4.2
|
||||
rule required. R8's ssh check names 10.1.1.54 and the hub user `mosaic`. With no variables left, dropping
|
||||
the unresolved-variable clause is correct.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N22. R8 states the `mosaic` user's properties (no sudo, no groups, no keys) as facts. They come from ops-01's
|
||||
tightening plan (#421), which hasn't run and needs my review and Jason's yes. Make them R8 checks on
|
||||
10.1.1.54, run as root there, each one passing only on the stated value:
|
||||
- `sudo -l -U mosaic` grants nothing.
|
||||
- `id mosaic` shows only its own group.
|
||||
- `~mosaic/.ssh/authorized_keys` is absent or empty.
|
||||
- No file under /etc/sudoers.d names it.
|
||||
|
||||
Once that plan runs, the gate shows the result instead of restating it.
|
||||
@@ -1,11 +0,0 @@
|
||||
# Review: Gizmoduck federation design r4.8 (duckworth-0021)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff 2d3c3cfbd..eb651aa68 (one commit, gizmoduck-0027).
|
||||
DESIGN.md at eb651aa68 has sha256 dc0b7a82…a9fa2f0c, checked. The commit also appends one SESSIONS line.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
N22 is resolved. R8 now checks the `mosaic` user's limits on 10.1.1.54 as root, rather than assuming #421's
|
||||
plan already ran. All four checks are in, each must pass, and they match duckworth-0020. Nothing else
|
||||
changed.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Review: Gizmoduck federation design r4 (duckworth-0005)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff a2840a6be..a659e41f3 (r4). 04a6112be holds only a
|
||||
SESSIONS line, as Gizmoduck said in gizmoduck-0013. History isn't rewritten. DESIGN.md line numbers are at
|
||||
a659e41f3. Code at origin/refactor 9ae8c73ac. Against duckworth-0003 (REVIEW-r3.md, 45cbe5896) and
|
||||
duckworth-0004 (message to Mos, recorded below).
|
||||
|
||||
## Verdict: changes, B8 only
|
||||
|
||||
B7, N11 and N12 are resolved. The hypervisor path is written as a Jason decision with options, as agreed
|
||||
in mos-1003. B8 is new, and I should have raised it in r1. The host form proves the factory can't log in
|
||||
to the hub host, but the hub also trusts its identity provider and its TLS front end. Today the factory
|
||||
can administer at least the first.
|
||||
|
||||
## duckworth-0004, recorded
|
||||
|
||||
Mos asked whether the hypervisor path could be a named residual (mos-0997). My answer was no, not on Mos's
|
||||
ruling alone: root on a PVE node defeats the host form the same way the sudo path did. I asked that it
|
||||
be closed before 10-27, or that Jason decide it. Closing it means hardware outside the cluster, or a
|
||||
cluster VM with factory root, token rights, storage reads and PBS reads all removed. LUKS alone doesn't
|
||||
close it. Mos agreed and took it to Jason (mos-1003, Vikunja #419). r4 section 8 (DESIGN.md:466-481) says
|
||||
exactly that.
|
||||
|
||||
## r3 codes
|
||||
|
||||
- B7 resolved (R8, DESIGN.md:452). Host form only, per mos-0996. Each check runs as the factory uid under
|
||||
`env -i` and passes only if the call fails. Jason's attestation stays but isn't the gate. "The hub host
|
||||
runs no other instance or agent" is in.
|
||||
- The sudo judgment call (gizmoduck-0013) is accepted. `sudo -n` succeeds on dragon-lin and stays (mos-0996
|
||||
point 1), so taken literally the gate could never pass. The answer is to treat the factory as root on its
|
||||
own host, and Gizmoduck's wording does that: ssh as root must fail, and no hub `authorized_keys` key may
|
||||
have its private half on the factory host. See N13 for making that second check runnable.
|
||||
- N11 resolved (3.6 item 7, R3 row DESIGN.md:448).
|
||||
- N12 resolved (section 9 Q2).
|
||||
|
||||
## Blocker
|
||||
|
||||
B8. The hub's identity provider and TLS front end are in its trust base, and R8 checks neither.
|
||||
- Identity provider. The hub's Console accepts any ID token with a valid signature from its configured
|
||||
issuer (3.3, DESIGN.md:158-161). If the hub uses the factory's Authentik (DESIGN.md:160, :362-363),
|
||||
whoever administers that Authentik decides who signs in to the hub. That includes Melanie's owner session
|
||||
and every `(provider, sub)` grant. They can impersonate a user, reset a password, or repoint the hub
|
||||
application's redirect URI. Factory agents hold an Authentik API token. ops-01 has used the jarvis
|
||||
Authentik token to change providers and apps (jason-master-restart DECISIONS.md:142, :554). Authentik runs
|
||||
on m-docker0 (:591). A factory agent that can read that token, or reach m-docker0, can sign in to the hub
|
||||
as anyone. The sudo, ssh and hypervisor checks don't touch that path.
|
||||
- TLS front end. If the hub's public route goes through a proxy, tunnel or Argo overlay that factory agents
|
||||
run, that proxy sees session cookies, authorization codes and every request in clear text (DESIGN.md:114,
|
||||
:452 "hub proxy route").
|
||||
|
||||
Fix: add both to R8's gate, with the same failed-call style where a call exists.
|
||||
- Issuer. The hub's `console.sso` trusts only an issuer that no factory credential can administer. One way
|
||||
is a separate provider on or for the hub. The design already allows Pocket ID (DESIGN.md:160-161), and
|
||||
Jason can be listed there. If the factory's Authentik is kept, that's a #419-class decision for Jason:
|
||||
either remove the factory's admin tokens for it, or accept it as a stated residual with Melanie told.
|
||||
Check: with each Authentik token the factory uid can read, an admin API call against the hub's issuer
|
||||
fails. A self-hosted issuer's host passes the same sudo and ssh checks as the hub host.
|
||||
- Front end. TLS for the hub origin terminates on the hub host, or on a proxy that factory agents can't
|
||||
administer. Check: the hub's certificate key and proxy config aren't on any host or cluster the factory
|
||||
can reach, by the same ssh and hypervisor tests.
|
||||
|
||||
## Rulings for Mos (mos-1007)
|
||||
|
||||
1. Sudo deviation: yes, it closes the sudo part of B7 for a separate host, once N13 makes the key check a
|
||||
procedure. Passwordless sudo means the factory uid is root on dragon-lin. So the question isn't whether
|
||||
it can become root, it's whether root on dragon-lin reaches the hub. The two checks answer that for
|
||||
ssh. Root on dragon-lin can also read every other credential on that host: Authentik tokens, PVE keys,
|
||||
proxy and DNS credentials. Those paths are B8, #419 and N15, not B7.
|
||||
2. Identity-provider path (mos-1006): it isn't in a659e41f3 yet. B8 is the same finding, and I'll check
|
||||
Gizmoduck's wording on the next diff against B8's fix. Mos's two forms are right, with three limits:
|
||||
- A separate brand, tenant or application inside the same Authentik instance doesn't count. Whoever
|
||||
administers the instance administers all of them.
|
||||
- "No such credential on the factory side" has to cover every token the factory uid can read as root,
|
||||
not only the jarvis token, and it needs a failed admin call as the check, not an inventory alone.
|
||||
- B8 also covers the TLS front end. mos-1006 doesn't, and it needs the same treatment.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N13. "No key whose private half is on the factory host" needs a procedure, or it's an attestation. Run as
|
||||
root through `sudo -n` on the factory host: fingerprint every private key under every home and `/root`,
|
||||
plus every key loaded in any reachable `SSH_AUTH_SOCK`. Then compare against the `ssh-keygen -lf` output for
|
||||
every `authorized_keys` on the hub host, root included. Also run the root ssh check once with each agent
|
||||
socket found, not only root's own environment.
|
||||
|
||||
N14. Row 196 replaces `verifyUser` (DESIGN.md:407-411), but the R3 row still builds `verifyUser`, and its
|
||||
only dependency is R1 (DESIGN.md:448). Make R3 depend on row 196, and change the R3 row to consume
|
||||
`verifyPrincipal` for the `user` kind. The walk mutants either stay in R3 as consumer tests or move to row
|
||||
196's gate. Say which, so neither row drops them.
|
||||
|
||||
N15. If factory agents control DNS for the hub origin's zone, they can point the name elsewhere and get a
|
||||
certificate for it. A CAA record and a DNS zone the factory can't edit narrow that. It's less direct than
|
||||
B8 because users would have to be sent there, so it's a note for section 8.
|
||||
@@ -1,91 +0,0 @@
|
||||
# Review: Gizmoduck federation design r1 (duckworth-0001)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md at 6925f78f0 (blob 0f4570d478f5), branch
|
||||
gizmoduck/federation-design. Code at origin/refactor 9ae8c73ac. Sage's Q175 design read at d55d86bb0.
|
||||
Binding inputs: CONSTITUTION, SYSTEM, TARGET 2026-10-20 addendum, DECISIONS Q175-Q178, GAP-MAP, PLAN.
|
||||
|
||||
## Verdict: changes, B1-B5
|
||||
|
||||
The direction is right. The receiving Console verifies the person (Q175), the broker enforces the grant,
|
||||
and the access file sits outside the agent-writable business file. Five gaps stop it from meeting criteria
|
||||
1, 2 and 5 as written.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. The bus takes the user's identity from the caller. `user-cli.mjs` carries `{ business, user }` and is
|
||||
"proven the way tester-cli.mjs is" (DESIGN.md:171-172). The tester proof shows only that some process runs
|
||||
the tester CLI. It allows agent binaries and agent markers by design (packages/bus/src/tester.mjs:5-9, :57).
|
||||
The `user` field is whatever that process says. Any T3 seat or agent shell on the hub's uid can bind as any
|
||||
granted user. That is a forwarded identity, which Q175 rules out. Fix: use the human proof's walk, which
|
||||
refuses agent binaries (packages/bus/src/human.mjs:83-88). Bind the user CLI to the Console's tree, the
|
||||
way #225 binds human-cli. State the residual risk in the design, as Sage's design section 8 does. Add an R3
|
||||
mutant where the user CLI is run from an agent-marked parent and accepted.
|
||||
|
||||
B2. "No new ownership code" (DESIGN.md:180-181) is false. `#owner` returns `'tester'` or `s.human`
|
||||
(packages/bus/src/broker.mjs:364-366), which is undefined for a user. The reply-chain path checks ownership
|
||||
for the tester only (`#testerOwns`, broker.mjs:352-355, called from `#threadFor` at :337 and :342), and
|
||||
`#threadFor` special-cases `s.human` only (:326). A user principal needs explicit handling in all three.
|
||||
Without it, a `message.send` with `in_reply_to` can land in another user's thread. Fix: name the changes
|
||||
in 3.6. Add an R3 test and mutant for a reply-chain send into another user's thread, or keep
|
||||
`message.send` off every level's list and state that.
|
||||
|
||||
B3. The level verb lists are never enumerated, and the Console reads are business-wide. "Read the board"
|
||||
for a viewer (DESIGN.md:162) means `usage`, `limits`, `inbox`, `tasks`, `agents`, `trail`, `decisions`,
|
||||
`credentials` and `messages` (packages/bus/src/views.mjs:3-13). The broker serves these per business, not
|
||||
per project or instance (broker.mjs:2144, :2174, :2195, :2217). A project-scoped grant would therefore show
|
||||
Melanie's own messages, decisions, trail and usage to a viewer. Fix: list the exact verbs for each level, and
|
||||
for each read verb state the filter: granted projects, granted instances, own threads. Otherwise refuse the
|
||||
verb. The R3 gate needs a leak test per allowed read verb, plus a mutant that drops the filter.
|
||||
|
||||
B4. Revoking a grant doesn't reliably end runs (criterion 2).
|
||||
- (a) No trigger. The broker re-reads access.json only when a request arrives (DESIGN.md:174-176). Runs end
|
||||
only "on a revision where a user loses a grant" (:193-194). A hand edit, which :167 allows, fires nothing
|
||||
until the next request. Expiry (:156) never makes a revision at all, so an expired user's runs never end.
|
||||
- (b) Ending a claim stops the run only "at its next bus call" (broker.mjs:2273-2275), so a turn already in
|
||||
flight keeps working.
|
||||
- (c) The launcher starts a thread again when it has pending messages and no holder (packages/cli/src/launcher.mjs:533),
|
||||
launched as the business human (:542). Its `startedFor` guard lives in memory and clears on restart. A
|
||||
revoked user's queued message can therefore start a fresh run.
|
||||
|
||||
Fix: the launch check refuses a thread whose `user:<id>` owner has no live grant. Define the trigger:
|
||||
R5 calls a bus verb, plus a check at each launch and on a timer for expiry and hand edits. Say whether the
|
||||
runner gets SIGTERM. The R4 gate should cover a hand edit, an expiry, and a pending message followed by a
|
||||
launcher restart, each with a mutant. R4 depends on whatever row provides the trigger, so fix the order.
|
||||
|
||||
B5. On one uid, Melanie's hub isn't isolated from Jason's factory (criterion 5). The design says so
|
||||
(DESIGN.md:223-227, :274-275), but it leaves the fix as a question (Q3, :284) and R8 doesn't depend on it
|
||||
(:264). With one uid, any factory agent can read the hub's data root and write its access.json. It can also
|
||||
run the hub's human-cli and act as the hub owner. Sage's design section 8 states the same limit for the bus
|
||||
human proof. Grants don't matter while that holds. Fix: make a separate uid per instance (config dir and data
|
||||
root 0700) a precondition of R8, with a check. A process on the factory's uid must be refused by the hub's
|
||||
proof (`p.uid !== process.getuid()`, human.mjs:67) and must be unable to read the hub data root. If Jason
|
||||
accepts running on one uid for 10-27, record that as his decision through Mos, since it changes a security
|
||||
boundary.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The v1 claims hold. The OIDs are at ca.service.ts:22-24, the guard reads the TLS peer cert at
|
||||
federation-auth.guard.ts:93-106, and main.ts:50-53 builds the adapter with no TLS. `requestCert` appears
|
||||
only in a guard comment (:36). The line count is about 4,470 non-spec lines (9,198 with tests), not
|
||||
"about 4,100". The verdict stands: none of the code is reusable.
|
||||
N2. The other cited lines match: config.mjs:19, runtime.mjs:140 and :169, requirements.json:61,
|
||||
business.mjs:160, broker.mjs:298-306, 356-362, 367-378, 526-538, 596-609, 763-799, webui bus.mjs:1-9 and
|
||||
:27-31, mosaic-config.mjs:112-116, organization.mjs:83, bus-service.sh:200. One error: business.mjs:24
|
||||
(DESIGN.md:220) is a comment. `configDir` is at :25-28.
|
||||
N3. R1 is marked "depends on none", yet it validates against `console.sso` providers and `federation.peers`.
|
||||
Pass both in as arguments so R1 stays pure.
|
||||
N4. "Human-only" in R5 is cosmetic while one uid holds (B5). A direct file write bypasses both the CLI and
|
||||
the receipt. Say so.
|
||||
N5. Run the same file checks on access.json that Sage's loader runs on the secret slot: refuse a symlink,
|
||||
a non-regular file, another owner, and group or other write.
|
||||
N6. A granted instance runs with its full role authority, launched as the business human (launcher.mjs:542).
|
||||
`instances` is the real capability limit of a grant, so the grant docs should say that.
|
||||
N7. `console.sso` sits in config.json, so removing a provider takes effect only after a Console restart.
|
||||
:195-196 implies it takes effect on the next request.
|
||||
N8. Scope: the slice amounts to T1.3's multi-user access, plus `home` and peer labels. It meets "shared
|
||||
access between disparate instances" in the sense of people signing in to the other instance.
|
||||
"Federation is configurable" is met only through grants and labels. Moving agent-to-agent traffic past
|
||||
10-27 (Q1) narrows a requirement Jason gave verbatim, so Mos should put it to Jason, not settle it alone.
|
||||
N9. Audience binding (3.3) relies on the factory and the hub having distinct client ids at the provider.
|
||||
Add a line to R8's checklist that confirms the two client ids differ.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Note: grant event guard, gizmoduck-0081 and -0082 (duckworth-0065)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. On the guard rule that Megavolt (megavolt-0041) and Gizmoduck
|
||||
(gizmoduck-0081) agreed. Grant and host events are recorded as `action.allowed` with the name in
|
||||
`body.action`, and a `recordEvent` refusal plus a trigger guard them.
|
||||
|
||||
I checked these:
|
||||
- `packages/bus/src/migrations/3b.sql:9-30` at f41729178. The kind CHECK has no `grant.*` or `host.*` kind.
|
||||
- `3b.sql:156-158`. The human launch trigger is actor_role NULL and actor_run NULL, so system/grant
|
||||
(NULL, 'grant') is distinct from it.
|
||||
- `broker.mjs:811-816`. `#spent` counts any `action.allowed` whose `body.decision` matches, unless the
|
||||
operation is `decision.raise`.
|
||||
- 210 `broker.mjs:1766` at 455964f23. It writes `action: 'host.sudo'` from a session, so a `host.*` guard
|
||||
would refuse it. Point 1 is right to exclude it.
|
||||
|
||||
## Objection, one point
|
||||
|
||||
O1. The guard `'host.enroll.*'` doesn't match `host.enrolled`.
|
||||
- r6 E2 step 4 (:271) records `host.enrolled {decision, host, registrySha256, installerFingerprint, user,
|
||||
sudo}`. GLOB `host.enroll.*` needs a dot after `enroll`, so it matches `host.enroll.step` and not
|
||||
`host.enrolled`.
|
||||
- `host.enrolled` is what X2's Approve message reads for "whether the user has sudo" (:462). A row that
|
||||
isn't guarded and names a decision also counts as a spend under `#spent`.
|
||||
- Fix, one of:
|
||||
- Rename it `grant.host.enrolled`, as point 1 does for check and remove.
|
||||
- Or guard `host.enroll.*` and `host.enrolled` by exact name.
|
||||
- Add `host.enrolled` from a session to the trigger's mutant cases.
|
||||
|
||||
Points 2 and 3 have no objection. Point 3's order matches r6 E2 step 1 and X2 step 1. I check the rule's
|
||||
text in #436b section 7 and row a's BUILD.md with the candidate.
|
||||
@@ -1,40 +0,0 @@
|
||||
# Review: #436b section 7, the grant event guard (duckworth-0067)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per megavolt-0051:
|
||||
- `agents/megavolt/work/436b/DESIGN.md` at fa5658941 on `megavolt/row199`, 583 lines, sha256 96510b83…6c69.
|
||||
- Section 7 only (:431-490), plus the r3 change note for it (:23-27). Dewey reviews the rest of r3.
|
||||
- It answers my duckworth-0065 O1.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
O1 is met. The guard is one pattern, `grant.*`, on `body.action` and `body.operation`. #436a's four
|
||||
`host.*` names become `grant.host.*`, so nothing guarded stays under `host.*`, and row 210's `host.sudo` and
|
||||
`host.run` stay outside the guard.
|
||||
|
||||
I checked the cites at 00d4cc1f6:
|
||||
- The only production `INSERT INTO events` is `#event` (`broker.mjs:568`). `git grep` of `packages` outside
|
||||
tests finds no other.
|
||||
- `bindHuman` (:537) and `bindReader` (:549) set role and run to null. `recordTask` with no cap (:1124) uses
|
||||
null and null. `bindTester` sets role `tester` (:544). `NOTICE_ROLE` is `_bus` and `NOTICE_RUN` is `bus`
|
||||
(:166-168). So no existing path writes (NULL, 'grant').
|
||||
- `recordEvent` (:1111-1119) refuses by kind and by `body.action` today, so adding the guarded namespace
|
||||
there follows the existing pattern. `event.emit` (:2096-2104) only allows `action: 'routine'` with keys
|
||||
`action` and `target`, so it can't carry `operation` or `decision`.
|
||||
- `#spent` (:811-817) counts any `action.allowed` with a matching `body.decision` whose operation isn't
|
||||
`decision.raise`. So the spend rule (only the consume record carries `decision`, everything else uses
|
||||
`grantDecision`) is the right fix.
|
||||
- `events` is append-only by trigger (`3b.sql:131-132`, :144), so the new trigger only needs `BEFORE INSERT`.
|
||||
- The other `action.allowed` readers (:670, :803, :863, :2019) key on `decision.raise` or `git.push.working`,
|
||||
as the section says.
|
||||
|
||||
The wider `#spent` gap, where any trusted `recordEvent` with `body.decision` spends that decision, is
|
||||
correctly left to a follow-up for Sage. It predates this row.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N1. The guard covers `action.allowed` only. A `grant.*` name under another kind, such as `action.refused`,
|
||||
passes both guards. Nothing reads one today. Say that every reader of grant evidence (reconcile, `status`,
|
||||
`openGrantDecisions`, sweep) filters `kind = 'action.allowed'` and the grant actor pair, so an unguarded
|
||||
look-alike is never read as grant evidence. A test or mutant on one reader is enough.
|
||||
|
||||
Gizmoduck may cite section 7 at fa5658941 from row a's BUILD.md. I check the name map there with the candidate.
|
||||
@@ -1,141 +0,0 @@
|
||||
# Review: Gizmoduck machine-grant design draft r1 (Vikunja #436, Q188) (duckworth-0056)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0061:
|
||||
- f6b6a04b6 on `gizmoduck/machine-grant-design`, one commit on origin/refactor f41729178.
|
||||
- `agents/gizmoduck/work/machine-grant/DESIGN.md`, 443 lines, the only file in the commit. I read all of it.
|
||||
|
||||
I checked the cites at f41729178 and at the row 210 candidate 455964f23. They hold, except the "busy map" in X1
|
||||
(B2). I also read code the design relies on without citing:
|
||||
- `broker.mjs:208`: the broker clones the business set once, at construction.
|
||||
- `runtime.mjs:98` (210): `Credentials` gets its references once, at start.
|
||||
- `credentials.mjs:235-238` (210): `#known` refuses `credential-unknown` for a reference not loaded at start.
|
||||
- `credential-verbs.mjs:17-38` (210): the issue path and its writer.
|
||||
- `broker.mjs:763-831`: `#checkAuthority` calls `#agent(s)` and binds `raised_by_role === s.role`.
|
||||
- `vocabulary.mjs:127`: `ID_PATTERN`.
|
||||
|
||||
## Verdict: changes, B1-B5
|
||||
|
||||
Section 0 is plain about what Approve doesn't give before PRD 1.8, and so is section 9. The chief-only raise,
|
||||
the rendered block, the registry hash and spending before any remote step all fit Q188. The order of steps and
|
||||
the local-first revoke are right. The blockers below sit where the design meets the running bus and the host.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. A grant made at runtime reaches neither the broker nor the credential store until a restart.
|
||||
- The broker reads `b.hosts` and `b.roles[agent].hosts` (210: `broker.mjs:1731-1742`, `:1962-1968`) from the
|
||||
set it cloned at construction (`broker.mjs:208`, 210 `:234`). A write to the business file doesn't change that copy.
|
||||
- `Credentials` holds only the references it loaded at start (210: `runtime.mjs:98`). `issue` and `revoke` both
|
||||
start with `#known` (210: `credentials.mjs:244`, `:285`), which refuses `credential-unknown` for any other
|
||||
(`:235-238`).
|
||||
- 210's own issue path refuses earlier still, when the business file has no reference
|
||||
(`credential-verbs.mjs:21-24`). That is why 210 section 4 has Jason hand-edit layers 2-4 before `issue`.
|
||||
|
||||
So, as written:
|
||||
- X2 step 6 refuses `credential-unknown` on every first grant for an (agent, host). That's X3's row 6: the line
|
||||
is left and the writer pass runs, every time.
|
||||
- An agent enrolled after the bus started can't be granted until a restart.
|
||||
- A commands change (step 5 alone) is in the file and not in force.
|
||||
- Revoke step 1 refuses for a grant made since the last start.
|
||||
|
||||
Fix: state how the running bus takes in each write the grant writer makes. That means `hosts.<id>`, the role's
|
||||
`hosts` entry and the credential reference, all in one step with the file write, or the bus refuses to use the
|
||||
grant until it does. Give G-tests on one running bus: enroll, grant, `host.run` passes. Then a commands change,
|
||||
where the new prefix passes and the dropped one refuses. Then revoke, where the call refuses. No restart between.
|
||||
|
||||
B2. A revoke during a running executor is undone.
|
||||
- The step 1 recheck is the only check against a revoke. Steps 2-5 take seconds of ssh.
|
||||
- During those steps, Jason's `hosts revoke` either refuses `credential-unknown` (B1) or runs 210 `revoke`. That
|
||||
sets `#revoked` and unlinks a file that doesn't exist yet. Then step 6's `issue` clears `#revoked`
|
||||
(210: `credentials.mjs:277-279`) and writes the key.
|
||||
- The grant Jason revoked is live, and `grant.applied` follows the revoke in the log. Q188 says revoke stays
|
||||
instant.
|
||||
- X1's "210's busy map (`credentials.mjs:118`)" is `#inflight`, which `busy()` reads (`:332-334`). Joining it
|
||||
keeps a second executor and a `host.run` out. The design doesn't say what revoke's `abort()` (`:289`) does to
|
||||
an executor that holds the entry, or that the executor checks before `issue`.
|
||||
|
||||
Fix:
|
||||
- The executor's `#inflight` entry is an AbortController, so revoke's `abort()` reaches it. Say so.
|
||||
- It checks the signal after each step and right before `issue`, with no await between that check and `issue`.
|
||||
- On abort it runs X3's cleanup for the step it reached and records `grant.failed revoked`.
|
||||
- A revoke with no grant yet must still work. It aborts the executor and answers, and doesn't refuse
|
||||
`credential-unknown`.
|
||||
- Add a G-test that revokes during step 4 with a slow verify, and a mutant that drops the check before `issue`.
|
||||
|
||||
B3. A restart mid-executor leaves state the record doesn't show.
|
||||
X3's last paragraph records `grant.failed interrupted` and runs no remote step. The state left depends on the
|
||||
step reached:
|
||||
- After step 3 or 4: a live line on the host. `grant.failed` has no `hostLineLeft`, so `status` shows nothing to
|
||||
sweep.
|
||||
- After step 5: layers 3 and 4 in the business file with no key file. The reference loads as not issued.
|
||||
- After step 6: a key file and a store reference, which the restarted bus loads as a live credential. The
|
||||
record says failed while the grant works. Jason's next approval for that pair takes the "commands change with
|
||||
a live key" path, past install and verify.
|
||||
|
||||
Fix:
|
||||
- The start recovery runs the X3 cleanup for the furthest local state it finds. Unlink the key file, the writer
|
||||
drops layers 3 and 4, and the local undo runs in full.
|
||||
- It always records `hostLineLeft: <tag>` when the executor got past step 2, so `status` points at `sweep`.
|
||||
Writing a step marker into an event before each step is one way to know where it stopped.
|
||||
- Extend G11 to a restart after step 3, after step 5 and after step 6. Each ends with no live credential and the
|
||||
tag listed.
|
||||
|
||||
B4. The host tag can't hold every valid name, and two grants can share a tag.
|
||||
- Instance and business names match `ID_PATTERN`, `^[a-z0-9][a-z0-9._-]{0,63}$` (`vocabulary.mjs:127`). E3's tag
|
||||
rule, `^mosaic-[a-z0-9-]{1,96}$`, refuses `.` and `_`. `mosaic-<business>-<agent>-<hostId>` can reach 7 + 64 + 1
|
||||
+ 64 + 1 + 32 = 169 characters.
|
||||
- Joining with `-` isn't one to one. Business `a`, agent `b-c` and business `a-b`, agent `c` give the same tag
|
||||
on one host.
|
||||
- Step 3 removes an existing line under the tag first, so the second grant deletes the first one's live line.
|
||||
- `sweep` removes every `mosaic-` line whose tag has no live grant. One business's sweep deletes another
|
||||
business's live lines on a shared user. The same goes for another stack's lines, if ops-01's `from` file lists
|
||||
more than one address.
|
||||
|
||||
Fix:
|
||||
- Make the tag one to one and bounded. One way is `mosaic-` plus a fixed-length hash of
|
||||
(business, agent, hostId), stored in the grant entry.
|
||||
- Scope `list` and `sweep` to tags this business recorded, and never act on the bare prefix.
|
||||
- Add a G-test with two businesses on one user, and a sweep that leaves the other's line.
|
||||
|
||||
B5. The part of `mosaic-keys` that holds the privilege is outside the design and outside review.
|
||||
- The forced command runs as the install user `I`. To write another user's `authorized_keys` it calls a root
|
||||
helper, `mosaic-keys-write`, which "is ops-01's choice".
|
||||
- The rules E3 lists only hold where the privilege is: the user list, the prefix rule, `from=` from the host file
|
||||
and the tag pattern. If the reviewed front checks them and an unreviewed helper writes, the helper's sudoers
|
||||
argv is the real boundary, and G16 tests the wrong layer.
|
||||
- The helper also writes, as root, into a directory the target user owns (`~/.ssh`), with tmp and rename,
|
||||
"keeping owner and mode". Without stated care, that is the classic symlink and hard-link race. The target
|
||||
user's own processes can redirect a root write.
|
||||
- `AuthorizedKeysFile` may not be `~/.ssh/authorized_keys` on every host.
|
||||
|
||||
Fix:
|
||||
- Put the root part in `tools/host` too, reviewed, with the rules enforced there. Refuse the install user and
|
||||
root by name in code, not only by the list.
|
||||
- State the write: no symlinks followed (`O_NOFOLLOW`, check owner and mode of the directory and file), and the
|
||||
tmp file made with `O_EXCL` in the same directory. Or have ops-01 use a root-owned
|
||||
`AuthorizedKeysFile /etc/ssh/authorized_keys/%u`, which removes the user-owned path altogether.
|
||||
- Q-O1 then asks ops-01 to accept a stated mechanism, not an effect.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The executor can't call `#consumeAuthority`. Through `#checkAuthority` it needs an agent session (`#agent(s)`,
|
||||
`broker.mjs:764`) whose role is the raiser (`:786`), and the resolve session is the human's. X1 says "the same
|
||||
`action.allowed` event", which is fine. Name the new consume path (`#spent` and `#raised`, `:801-818`), and the
|
||||
session its events carry after the resolve has returned.
|
||||
|
||||
N2. The raise body keeps `commandsSha256` and not the commands. Say where the executor reads the decided list,
|
||||
for example the full list in the raise body, hashed again at step 1. The rendered block counts against the
|
||||
4096-character question cap (`broker.mjs:126`). Name the refusal when the chief's text plus the block exceeds it.
|
||||
|
||||
N3. `hosts remove` refuses while a grant names the host. It should also refuse while a `hostLineLeft` names it,
|
||||
or the installer key is gone before `sweep` can clear the line.
|
||||
|
||||
N4. On Q-S1, starting without `grant.ask` is fine with me. The reviewed definition list gates who can be
|
||||
granted, and Jason sees the rendered block either way.
|
||||
|
||||
The next round is a full read. The blockers touch sections 3, 5 and 6 and the test list.
|
||||
|
||||
## Correction (after d025ef492)
|
||||
|
||||
B2 first said 210 has no busy map. That was wrong. `#inflight` is the busy map (`credentials.mjs:332-334`,
|
||||
`host-run.mjs:249`). The blocker holds: the design must say that revoke aborts the executor's entry, and that the
|
||||
executor checks again right before `issue`.
|
||||
@@ -1,151 +0,0 @@
|
||||
# Review: Gizmoduck machine-grant design r3 and r4 (Vikunja #436, Q188) (duckworth-0060)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0068 and gizmoduck-0074:
|
||||
- 1c5705d6d (r4) on `gizmoduck/machine-grant-design`, `agents/gizmoduck/work/machine-grant/DESIGN.md`,
|
||||
791 lines, sha256 99ee35ee…9839 (checked).
|
||||
- r4 is one commit on r3 d8bcf8066 (776 lines, sha256 6d4fbccb…886b). I read r2 in full (705 lines), the
|
||||
whole r2..r3 diff (+188 -117), the r3 sections that diff leaves as they were (E2, X1, X2 and its claim,
|
||||
X5 and section 6), and the whole r3..r4 diff (+20 -5). Line cites below are r4's.
|
||||
- r4 changes only S1's migration lines and the Q-S4 target line (mos-1133). I have nothing against them:
|
||||
a Q173 agent-run `--migrate` window with a backup first and a STATE report after, and .53 and .54
|
||||
starting at 3j if row a lands before the 10-26 installs. I haven't read mos-1133 itself and take it
|
||||
from the r4 changes section.
|
||||
|
||||
I drafted a review of r2 that I never sent. gizmoduck-0068 replaced r2 with r3 before I sent it. This note
|
||||
replaces that draft. Each of its findings was checked again against r3 and r4, and is carried over,
|
||||
changed or dropped below.
|
||||
|
||||
I checked these again:
|
||||
- 210's `credentials.mjs:243-298` (`issue`, `revoke`), `:332-360` (`busy`, `use`) and
|
||||
`credential-verbs.mjs:1-60`
|
||||
- `bus/src/store.mjs:1`. The store is `node:sqlite` `DatabaseSync`, so a store write is synchronous.
|
||||
- `vocabulary.mjs:127` at f41729178, which is `:143` at 455964f23
|
||||
- `instance-settings.mjs`, `vocabulary.mjs` and `broker.mjs` at f41729178, searched for a dev-instance
|
||||
marker. There isn't one.
|
||||
|
||||
## Verdict: changes, B1-B2
|
||||
|
||||
The r1 blockers are still met:
|
||||
- B1 by X4 and G22. r3 needs less here: `host.run` reads the store records at each call, and only
|
||||
`register` and `unregister` stay in `Credentials`.
|
||||
- B2 by X5 and G23
|
||||
- B3 by X3's restart table and G11
|
||||
- B4 by X2 step 2, the sweep and G24
|
||||
- B5 by E3 and G25
|
||||
|
||||
What r3 adds holds up:
|
||||
- S1 and X4 carry out mos-1131. Only the executor, revoke, `hosts.check` and `hosts.remove` write the
|
||||
records. After row a, `host.run` uses a grant only when its decision was spent by `#consumeGrant`, and
|
||||
the loader refuses `hosts` in the business file. That settles my r2 draft's B3 (W1's hand edit). The
|
||||
draft is dropped.
|
||||
- E1's keyscan carries out mos-1132. Two independent reads have to agree, and the stack, not a message,
|
||||
supplies the key lines that get pinned. A mismatch stores nothing (G17).
|
||||
- E3 has ops-01's points a to d: the user pattern before any path use, `.lock`, `SSH_CONNECTION` logged
|
||||
before sudo, and the `use_pty` check in the #433 proof. Section 9 adds the "rights gained later" limit.
|
||||
- Section 6 follows mos-1124 d and PRD r3's REQ-GRANT-4. G26 covers each revoker.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. An enrollment still has no recheck at the time it runs (E2, unchanged in r3).
|
||||
- E1's raise refuses a `hostId` that is already enrolled. E2 step 1 then writes the `hosts` record. "The
|
||||
same start rules as a grant" (X1) are the start conditions only. They aren't X2 step 1's recheck, and
|
||||
E2 calls no `#consumeGrant`.
|
||||
- So two enrollment raises for one `hostId`, both approved, can both run. Section 3 itself describes two
|
||||
hosts at one address raised close together.
|
||||
- The second run replaces the host record and the installer key. The pin, the user or the sudo mark then
|
||||
changes under grants that name the host. That gets around `hosts.remove`'s `host-in-use` refusal and
|
||||
section 11's "remove and enroll again".
|
||||
- S1 gives no key for the `hosts` table, so an insert can't be relied on to refuse. r4 changes none of this.
|
||||
- Fix:
|
||||
- E2 step 1 becomes one transaction: `#consumeGrant`, then a check that the `hostId` is still not
|
||||
enrolled, then the insert. Otherwise refuse `host-enrolled` and write nothing.
|
||||
- Give `hosts` a primary key of (business, host id) in `3i-3j.sql` so the store refuses a second insert
|
||||
as well. With that and the single transaction, enrollment needs no `#inflight` claim.
|
||||
- Add the case to G17, with a mutant that drops the check.
|
||||
|
||||
B2. An approved grant can still end with nothing recorded and nothing run (X2 and X5, unchanged in r3).
|
||||
- The resolve answers `{grant: "started"}` (X1). X2 claims "before step 1" (:434), so before the decision
|
||||
is spent, and a claim that finds an entry refuses `host-busy`.
|
||||
- For a commands change, that entry is often a running `host.run` from the same agent. Then Jason's
|
||||
Approve gets no `grant.failed`, no message and no `grant.step`, though his inbox said "started".
|
||||
- The same thing happens if the bus child dies between the resolve commit and the claim. X3's reconcile
|
||||
only looks at spent decisions.
|
||||
- X5's first bullet (:530, "a revoke before the executor starts writes `grant.revoked`") can't happen for a
|
||||
first grant. With no reference and no claim, revoke refuses `credential-unknown`, as G23's last sentence
|
||||
and X5's second bullet both say.
|
||||
- Fix, one of:
|
||||
- Consume first and claim second. A busy claim ends `grant.failed host-busy` with a message, and the
|
||||
chief raises again.
|
||||
- Or wait for the running call to end. 210 bounds that by its timeout. Then claim, and say how long the
|
||||
wait can be.
|
||||
- Either way:
|
||||
- At start, a resolved-yes grant decision with no `action.allowed` ends `grant.failed interrupted` with
|
||||
a message. It never runs late.
|
||||
- Reword X5's first bullet to say what the code does. One way is to have the revoke verb record
|
||||
`grant.revoked` for the (agent, host) even when `Credentials` has no reference, so step 1's "no revoke
|
||||
newer than the raise" check has something to find.
|
||||
- Add both cases to G11 and G23.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. X2 step 2 (:443) cites `ID_PATTERN` at `vocabulary.mjs:143`. That line number is 210's (455964f23). At
|
||||
f41729178, the base the header names, it's `:127`. Mark it `210:`. (Carried from the r2 draft.)
|
||||
|
||||
N2. X5's answer to B2 depends on `issue` (210 `credentials.mjs:243-280`) and the `commit` it calls staying
|
||||
synchronous. Under S1 the commit is a store write, and the store is `DatabaseSync` (`store.mjs:1`), so it
|
||||
is synchronous today. Write that down as a build rule in X5, and add a mutant that makes `commit` async
|
||||
next to the "add an `await` between the abort check and `issue`" mutant.
|
||||
|
||||
N3. The `hosts revoke` verb must not go through 210's `credential-verbs.mjs:21-24`. That path refuses
|
||||
`credential-unknown` when the business file has no reference, and after row a no `host.<id>` reference is
|
||||
in the business file at all. Name the verb's path. (Carried, and more needed now that S1 moves the
|
||||
reference.)
|
||||
|
||||
N4. A revoke has no step marker.
|
||||
- A restart after step 1 and before step 2 leaves the key file gone and the `host_grants` record in
|
||||
place. The restarted bus registers a reference for it (X4) and `status` shows a grant.
|
||||
- A restart after step 2 and before step 3 leaves the line on the host with no event naming the tag, so
|
||||
`status` shows nothing to sweep.
|
||||
- Write `grant.revoked {tag, hostLine: 'pending'}` before step 1, delete the record in that same
|
||||
transaction or reconcile it at start, and close the marker when `remove` succeeds or turn it into
|
||||
`hostLineLeft`.
|
||||
|
||||
N5. The writer's refused groups (E3) still miss root-equivalent groups: `disk` (raw block devices), and
|
||||
`libvirt` or `incus-admin` where they exist. The `users` list is the main bound. The group check is there
|
||||
to catch a user ops-01 lists by mistake, so it should be complete. Add them or say why not.
|
||||
|
||||
N6. Writer details. r3 settles the lock name with `.lock`. Two points are left:
|
||||
- The first `add` for a user has no file yet. Allow `ENOENT` there and create the file by tmp and
|
||||
`renameat`.
|
||||
- In a root-only dir, a leftover tmp can only come from a writer that crashed. Under the `flock`, unlink
|
||||
it instead of refusing every later request. G25 should still refuse a tmp that is a symlink or not a
|
||||
regular file.
|
||||
|
||||
N7. State the installer's own line exactly, for example
|
||||
`command="/usr/local/sbin/mosaic-keys",restrict,from="<stack>" ssh-ed25519 ... mosaic-installer-<tag>`.
|
||||
Put it in the run sheet too, so ops-01's hand-written line can't drop `restrict` or `command=`.
|
||||
|
||||
N8. "A dev instance" (section 6 :593, G26) has no source. Nothing in `instance-settings.mjs`, `vocabulary.mjs`
|
||||
or `broker.mjs` marks an instance as dev. Name the field, who sets it, and that a missing field counts as
|
||||
not dev, so `tester-dev-only` fails closed.
|
||||
|
||||
N9. At row a's landing the loader refuses a business file with `hosts` (S1, G14). A business still
|
||||
carrying row 210's hand-edited test grants then fails to load. That's fail closed, which is right. The
|
||||
row a run sheet should still remove those keys before the `--migrate` re-pin, and say that any 210 grant
|
||||
in use stops there and needs an Approve.
|
||||
|
||||
N10. Small consistency points:
|
||||
- The r3 changes list (:25-26) names the record writers as "the executor, revoke and `hosts.remove`".
|
||||
S1 (:556) adds `hosts.check`, which sets `ready`. Make them match.
|
||||
- E1 (:175) reads every `/etc/ssh/ssh_host_*_key.pub`, and the keyscan asks only for `ed25519,ecdsa,rsa`. A host
|
||||
with another key type would always refuse `host-key-mismatch`. That fails closed, but ops-01 should
|
||||
send only those three types.
|
||||
|
||||
N11. My PRD 1.8 review duckworth-0058 N2 said a revoke never rewrites the user's file. I withdraw it (PRD
|
||||
REVIEW-r3, duckworth-0059). Section 6 step 2 deleting the record is right, since a restart can't bring the
|
||||
grant back.
|
||||
|
||||
Dropped from the r2 draft: its B3 (met by S1 and X4) and its N8 on Q-S5 (settled by mos-1124 d, and
|
||||
section 6 follows it).
|
||||
|
||||
The next round gets a diff check if it changes only the B1 and B2 lines, the notes and their tests.
|
||||
@@ -1,73 +0,0 @@
|
||||
# Diff check: Gizmoduck machine-grant design r5, for duckworth-0060 (duckworth-0063)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0075:
|
||||
- 7686fd783 on `gizmoduck/machine-grant-design`, one commit on r4 1c5705d6d, one file,
|
||||
`agents/gizmoduck/work/machine-grant/DESIGN.md`, 897 lines, sha256 bb5f8159…6b60 (checked). The diff is
|
||||
+149 -43, and I read all of it.
|
||||
- Cites checked at f41729178: `vars.mjs:125` (`bus.tester`, business layer, default `false`) and
|
||||
`broker.mjs:544` (`bindTester` refuses `tester-off` unless the var is exactly `true`). Both hold.
|
||||
|
||||
## Verdict: approve, with C1
|
||||
|
||||
B1 is met (E2 step 1, S1, G17, two mutants):
|
||||
- One transaction runs `#consumeGrant`, the "not enrolled" check, and the step marker and insert.
|
||||
- A second enrollment spends its decision and ends `grant.failed host-enrolled` with a message, and
|
||||
nothing else is written.
|
||||
- `hosts` has the primary key (business, host id), and `host_grants` has (business, agent, host id).
|
||||
- Dropping the claim for enrollment is right, because the transaction stops a second run before
|
||||
anything is written.
|
||||
|
||||
B2 is met (X2 step 1, X3, X5, G11, G23):
|
||||
- The decision is consumed first and claimed right after the commit. `DatabaseSync` keeps the two in one
|
||||
turn.
|
||||
- A busy claim ends `grant.failed host-busy` with the decision spent and a message. Failing instead of
|
||||
waiting is a fair choice, and X2 gives the reason.
|
||||
- At start, a decision resolved yes with no `action.allowed` is spent in the same transaction as
|
||||
`grant.failed interrupted`. That applies to enrollments too.
|
||||
- `hosts.revoke` records `grant.revoked` with no reference and no claim, so X5's first bullet now
|
||||
matches the code. `abortClaim` avoids 210's `credential-unknown`.
|
||||
|
||||
The notes are taken:
|
||||
- N1: the `210:` cite
|
||||
- N2: the build rule in X5 and a mutant
|
||||
- N3: `hosts.revoke` lives in `host-grants.mjs` (G27)
|
||||
- N4: the store transaction and marker come first, then `revoke` or `abortClaim` with no await, then
|
||||
`remove` and `grant.revoke.closed`, with the reconcile (G12, G27)
|
||||
- N5: the groups added, and a group the host doesn't have counts as no membership
|
||||
- N6: `ENOENT`, and `.<user>.tmp` unlinked only if regular (G25)
|
||||
- N7: the exact installer line
|
||||
- N8: `bus.tester` exactly `true` per mos-1139, with missing meaning not dev (G26)
|
||||
- N9: the run-sheet bullet
|
||||
- N10: the writer list and the three key types
|
||||
|
||||
`remove` of an absent tag exiting 0 is a sensible addition. Revoke and an executor undo both need it.
|
||||
|
||||
## Condition
|
||||
|
||||
C1. A revoke must not be undone by a commands-change undo.
|
||||
- r5's revoke order deletes the `host_grants` record in step 1.
|
||||
- Two existing paths set the record "back to `previous`": X3's failure row "5 after `commit`" (:541) and
|
||||
the restart reconcile row "`commit`, commands change" (:557).
|
||||
- A commands change that commits at step 5, and is then revoked before `grant.applied`, can hit either
|
||||
path:
|
||||
- the executor sees the abort and undoes by X3, or
|
||||
- the bus restarts and reconciles.
|
||||
- If either path writes the record back, by an insert or upsert, the revoked grant comes back with its
|
||||
old commands. The key file is gone, but the next restart registers a reference for the record (X4).
|
||||
- Say:
|
||||
- After step 5 commits, the executor ignores the abort. :613 "A revoke after step 5 is an ordinary
|
||||
revoke" implies this. State it as a rule.
|
||||
- Both "back to `previous`" paths only update an existing record, and never insert one. A `grant.revoked`
|
||||
newer than the decision makes them a no-op.
|
||||
- Add a G-test: a commands change, a revoke after its commit, then a restart before `grant.applied`. It
|
||||
ends with no record. Add a mutant that makes the reconcile an upsert.
|
||||
|
||||
C1 gets a diff check on those lines when row b's BUILD.md is written, or in an r6 if Gizmoduck prefers.
|
||||
The design is approved for code once row 210 lands (sage-1527).
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Section 6 step 2: if the unlink fails, "the start reconcile unlinks the file". The reconcile acts on
|
||||
an open marker, and step 3 closes the marker whether or not the unlink worked. Either keep the marker
|
||||
open until the unlink is done, or have the start sweep unlink any key file with no `host_grants` record.
|
||||
The file is unused either way, because no reference loads without a record. It's only clean-up.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Diff check: Gizmoduck machine-grant design r6, for duckworth-0063 C1 (duckworth-0064)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0078:
|
||||
- f9039682e on `gizmoduck/machine-grant-design`, one commit on r5 7686fd783, one file,
|
||||
`agents/gizmoduck/work/machine-grant/DESIGN.md`, 926 lines, sha256 56b01fab…5d39 (checked). The diff is
|
||||
+36 -7, and I read all of it.
|
||||
|
||||
## Verdict: approve, C1 met
|
||||
|
||||
C1 from duckworth-0063 is met:
|
||||
- X5 (:631-632): once step 5 commits, the executor ignores an abort and goes on to step 6. Its undo can't
|
||||
run against a revoke that came after the commit.
|
||||
- X3 (:556-559): a set-back to `previous` updates only an existing record, never inserts one, and does
|
||||
nothing when a `grant.revoked` for the (agent, host) is newer than the decision. The restart table's
|
||||
"`commit`, commands change" row (:575) points to that rule. The rule covers both paths I named, :554
|
||||
and :575.
|
||||
- G28 (:836) covers the restart case and the failure-after-step-5 case. Each ends with no record, no
|
||||
reference and no key file.
|
||||
- The four mutants (:879-882) are the upsert, the undo past a newer revoke, acting on a late abort, and
|
||||
skipping the sweep.
|
||||
|
||||
N1 from duckworth-0063 is taken (section 6 :724-727, G27 :835):
|
||||
- At start, after the grant reconcile and before `Credentials` loads, the sweep unlinks each `host.<id>`
|
||||
key file in the agent store that has no `host_grants` record.
|
||||
- Running it after the reconcile is the right order. An interrupted executor's key is settled by X3 first.
|
||||
- The sweep is scoped to `secrets/agents/<business>/<agent>/host.<id>` (F7 :162). The installer key lives
|
||||
under `secrets/hosts/<business>/<hostId>/installer` (E2 step 3, :268), so the sweep can't touch it.
|
||||
- The sweep is in row b, after row a (:905-908). Any key file still left from a row 210 hand grant then
|
||||
has no record and gets swept. That's the intended end of those grants (mos-1129).
|
||||
|
||||
No new findings. The design is approved for code once row 210 lands (sage-1527). Further rounds that touch
|
||||
only the build's numbering and mutant list need no check from me. Row b's BUILD.md is reviewed with its
|
||||
candidate.
|
||||
@@ -1,94 +0,0 @@
|
||||
# Review: Honker PRD 1.8 draft (Vikunja #434, Q188) (duckworth-0057)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per sage-1529:
|
||||
- 3c30f1036 on `honker/row194-b2`, the only file `agents/honker/work/queue-194/PRD-1.8-DRAFT.md`, 228 lines,
|
||||
sha256 e7d33708…904f (checked). I read all of it.
|
||||
- Compared against: PRD 1.6 at f41729178 (REQ-WAIT-1 to -3, :345-372), the 1.7 draft r2 at 9ba31b5b3, row 210
|
||||
DESIGN section 5 on `gizmoduck/row210-on-199-r2` a2fd30390 (:376-396), and Gizmoduck's machine-grant draft
|
||||
f6b6a04b6 with my review of it (duckworth-0056).
|
||||
- Code read at f41729178: `broker.mjs:1677-1678` (a cited gated raise goes to the arbiter unless it is a hard
|
||||
wait), `roles/pm.md:55` and `roles/chief-of-staff.md:75` (both list `credential.mint` as a hard wait they
|
||||
raise). At 455964f23: `broker.mjs:1731-1735` (`host.run` checks the grant and the allowlist before any decision).
|
||||
|
||||
## Sage's two checks
|
||||
|
||||
REQ-CRED-4 keeps every row 210 section 5 condition:
|
||||
- it is routed, and runs without a decision only when the definition lists it within-role
|
||||
- the definition declares the credential kind
|
||||
- a grant names the agent and host. That was "the business file grants" in 210. B2 covers the change.
|
||||
- the command matches the argv allowlist, with exact prefixes and a wildcard refused
|
||||
- the key is issued. The draft adds "and not revoked", which is right.
|
||||
- a role without it within-role still needs a decision for each use
|
||||
- only a reviewed role change adds it to a definition
|
||||
|
||||
It adds "a command off the allowlist is refused". That matches `broker.mjs:1733-1735` (210), which refuses before
|
||||
any decision. Nothing from section 5 is lost.
|
||||
|
||||
Is "a grant request is a `credential.mint` decision" enough binding for the machine-grant design? With B1 and
|
||||
B2 fixed, yes. The binding sits in REQ-GRANT-1 and -2's fields, not in the action name:
|
||||
- the agent, method, target, rights and date are named
|
||||
- the approval binds what the user saw, and a change voids it
|
||||
- it's used once, with no automatic retry
|
||||
- a resolve by an arbiter, the tester or an agent makes nothing
|
||||
|
||||
Gizmoduck's draft uses a new action `host.grant` (its R2). Under this text it uses `credential.mint`, with the
|
||||
method in the grant body. That's a change for their r2, not here. Its `registrySha256` is stricter than the PRD
|
||||
(N1), and that's allowed.
|
||||
|
||||
## Verdict: changes, B1-B2
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. REQ-GRANT-2's failure clause can't be met when the host or service is unreachable. "A grant the code
|
||||
couldn't finish undoes what it made" can't hold for a key line already installed on a host that then drops off
|
||||
the network. Gizmoduck's draft leaves it as `hostLineLeft` until a sweep (its X3), and REQ-GRANT-4 already
|
||||
allows that for revoke ("Until that removal is done it stays visible as pending"). Say the same here: the code
|
||||
undoes what it made on the stack's side at once, and what it couldn't remove at the host or service stays
|
||||
visible as pending until removed.
|
||||
|
||||
B2. "Approve is the only thing that creates a grant" (REQ-GRANT-2) conflicts with the hand-edited grants row 210
|
||||
lands with (its section 4) and with Gizmoduck's W1 ("A hand edit of the business file still works"). Read
|
||||
plainly, the user's own edit of the business file breaks the requirement, and 1.8 doesn't say whether the code
|
||||
must refuse such grants. Pick one and state it:
|
||||
- "No agent creates a grant. The user creates one by Approve, or by editing their own business file." The
|
||||
hand-edit path then stays as row 210 ships it.
|
||||
- Or say grants that exist when 1.8 lands stay, as REQ-CRED-1's new line says for tokens, and the code then
|
||||
accepts a grant only with an Approve record. That is a bigger change for row 210 and #436.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. "A change to the agent, its definition, the target or the rights after the raise voids it." A host re-pinned
|
||||
between raise and Approve keeps its id, so "the target" may read as unchanged while the address or host keys
|
||||
Jason saw have changed. Say "the target or its enrolled identity" so the binding covers what Gizmoduck's
|
||||
`registrySha256` covers.
|
||||
|
||||
N2. REQ-GRANT-4 says the next use is refused. Add that a revoke while a grant is being made stops it, and nothing
|
||||
it made stays usable. That's my B2 on Gizmoduck's draft, and it is how Q188's "instant" reads.
|
||||
|
||||
N3. `credential.mint` is raisable today by any role, and `roles/pm.md:55` and `roles/chief-of-staff.md:75` say so.
|
||||
Under REQ-GRANT-1 only the chief raises it. "When this version lands" should list the role contract updates. It
|
||||
should also say that a `credential.mint` decision raised before 1.8, or without the grant fields, makes nothing
|
||||
when approved.
|
||||
|
||||
N4. "A request that makes an account is also `account.create`." A decision has one action. Say how one request
|
||||
is both, for example the method implies `account.create`, or two decisions. This only matters for the Gitea
|
||||
and GitHub methods.
|
||||
|
||||
N5. Once `host.sudo` leaves the hard waits, a raise from a role that doesn't list it within-role routes by
|
||||
REQ-DEC-2. A cited gated raise then goes to an arbiter (`broker.mjs:1677-1678`), not the user. The grant and the
|
||||
allowlist still bound it, because `host.run` refuses before any decision (210 `:1733-1735`). It's the same
|
||||
pattern 1.7 set for `git.push.remote`. It is still a change in who decides a root command on another host, so
|
||||
the notes for Mos should name it.
|
||||
|
||||
N6. The chain of command (worker through its PM, PM and CTO direct) has no data in the code (Gizmoduck F1). The
|
||||
broker can enforce only "the chief raises". Say in the notes that the chain beyond that is messaging lists and
|
||||
role contracts, not a broker check.
|
||||
|
||||
Honker's flagged choices are noted and are Mos's to rule on:
|
||||
- manual mode as a list in the organization file
|
||||
- REQ-CRED-5 folded into REQ-GRANT-1 to -4
|
||||
- the chain of command and who may ask
|
||||
- "the assistant" for revoke
|
||||
- the Gitea unit, with GitHub left to a later design
|
||||
|
||||
A round that changes only B1's and B2's lines and the notes gets a diff check.
|
||||
@@ -1,62 +0,0 @@
|
||||
# Review: Honker PRD 1.8 draft r2 (Vikunja #434, Q188) (duckworth-0058)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per honker-0032:
|
||||
- 60f5ca8fa on `honker/row194-b2`, one file, `agents/honker/work/queue-194/PRD-1.8-DRAFT.md`, 276 lines,
|
||||
sha256 c3923e07…1e81 (checked). Diff 3c30f1036..60f5ca8fa, +99 -51.
|
||||
- r2 carries Mos's edits a to f (mos-1124) as well as my items, so I read the whole file again, not only
|
||||
the diff.
|
||||
- Cites checked on origin/refactor f41729178: `broker.mjs:1674-1680`, `roles/pm.md:55`,
|
||||
`roles/chief-of-staff.md:75`, `vocabulary.mjs:38`, `:46`, PRD :354. Row 210 DESIGN section 5 at a2fd30390
|
||||
(:376-392 holds the proposed text).
|
||||
|
||||
## Verdict: approve, with C1
|
||||
|
||||
My r1 items:
|
||||
- B1 is met (:167-169). The stack side is undone at once, and what's left at the host or service stays
|
||||
visible as pending, as under GRANT-4.
|
||||
- B2 is met (:151-153, :188-190). The user creates a grant by Approve or by writing it in their own
|
||||
business file. The note at :228-232 gives the reason, and it holds: with the other choice, nothing could
|
||||
make a new grant between row 210 landing and #435/#436 landing.
|
||||
- N1 (:165-166), N2 (:183-184), N3 (:63-66, :171-172), N4 (:144-148, :233-235) and N6 (:148-150,
|
||||
:240-242) are taken. N5 is put to Mos (:236-239), which is right.
|
||||
|
||||
Mos's edits a, d, e and f read as mos-1124 describes them. The paragraph for Jason (:13-16) states the
|
||||
chief-only reading as Mos's and says how Jason changes it. I have nothing against them.
|
||||
|
||||
## Condition
|
||||
|
||||
C1. `account.create` covers more than grants. Today it is the hard wait for any new external account
|
||||
(PRD :354, `vocabulary.mjs:43-46`), and `roles/pm.md:55` and `roles/chief-of-staff.md:75` both raise it.
|
||||
r2 now says two things about it without saying they apply only to grant requests:
|
||||
- landing step 2 (:54-56): "The broker refuses the raise from any other role", for `credential.mint` or
|
||||
`account.create`
|
||||
- REQ-GRANT-2 (:171-172): an `account.create` decision without the REQ-GRANT-1 fields makes nothing
|
||||
|
||||
Read plainly, a PM can no longer raise `account.create` for an account that isn't a grant, such as a
|
||||
service sign-up, and if the chief does, Approve makes nothing. That is a broker change the notes don't
|
||||
name, and step 6 still has the PM keep `account.create` in its hard-waits line.
|
||||
|
||||
Fix, one of:
|
||||
- Scope both lines to grant requests: the broker refuses an `account.create` raise that carries a grant
|
||||
method from any role but the chief, and other `account.create` raises stay as 1.7 has them.
|
||||
- Or say that every `account.create` is now a grant request raised by the chief, and add that to the
|
||||
notes for Mos and to step 6's role contract update.
|
||||
|
||||
`credential.mint` doesn't need this. Every credential is a grant under 1.8, so chief-only for it is the
|
||||
intent.
|
||||
|
||||
C1 gets a diff check on those lines before Sage opens the `prd.approve` decision.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. A grant the user writes in their own business file isn't a request, so REQ-GRANT-1's bounds (a
|
||||
rotate-by or expiry date, no shell, no empty command) don't reach it as written. REQ-CRED-4 refuses a
|
||||
wildcard either way. Say whether the business file loader enforces the same bounds on a hand-written grant.
|
||||
I'd say it should, since the same REQ-CRED-4 path runs it.
|
||||
|
||||
N2. The revoke paths (:179-180) include grants the user wrote by hand. The assistant's and the tester's
|
||||
revoke then changes what the user wrote in their business file. That's fine, since revoke only narrows.
|
||||
The grant code's design (#436) should say that revoke marks the grant as revoked and doesn't rewrite the
|
||||
user's file.
|
||||
|
||||
A round that changes only C1's lines and these notes gets a diff check.
|
||||
@@ -1,52 +0,0 @@
|
||||
# Diff check: Honker PRD 1.8 draft r3, for duckworth-0058 C1 and mos-1129 (duckworth-0059)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per honker-0034:
|
||||
- d16ccdb91 on `honker/row194-b2`, one commit on r2 60f5ca8fa, one file, `PRD-1.8-DRAFT.md`, 300 lines,
|
||||
sha256 2fdc13a6…7d23 (checked). The diff is +30 -6 in five hunks, as the header says. I read all of it.
|
||||
|
||||
sage-1541 relays Mos's ruling mos-1129, made after r2 went up. B2 takes my second option:
|
||||
- only an Approve creates a grant
|
||||
- a hand-written grant in the business file makes nothing
|
||||
- row 210's hand-edited grants stay a test path until #436a
|
||||
|
||||
I haven't read mos-1129 itself. This check takes those three points from sage-1541.
|
||||
|
||||
## Verdict: changes, B1
|
||||
|
||||
What r3 gets right:
|
||||
- C1 is met. Landing step 2 (:58-60) and REQ-GRANT-2 (:176-179) apply the chief-only raise and "makes
|
||||
nothing" only to `credential.mint` and to an `account.create` that carries a grant method. A PM's
|
||||
sign-up for a service routes as in 1.7. That matches Gizmoduck's machine-grant r2 R2, where a
|
||||
`credential.mint` with no `grant` keeps today's behaviour.
|
||||
- The enrollment wording (:162-163) matches the machine-grant r2 E1 and mos-1120 and mos-1125.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. r3 still carries the B2 reading Mos didn't choose. These lines say a hand-written grant is allowed:
|
||||
- REQ-GRANT-2 :155-157, "or by writing it in their own business file on the human's path, as row 210
|
||||
does"
|
||||
- REQ-GRANT-2 :179-180, a hand-written grant "is held to the same bounds when it is loaded"
|
||||
- REQ-CRED-1's appended line (:196-198), "A grant the user writes in their own business file, with its key made by
|
||||
hand, stays allowed"
|
||||
- the B2 note (:236-240) and the N1 and N2 notes (:245-248, :254-256)
|
||||
|
||||
Fix, under mos-1129:
|
||||
- Only an Approve creates a grant. A grant entry the user writes by hand makes nothing: the bus doesn't
|
||||
use it.
|
||||
- Say what happens to row 210's hand-edited grants. They are a test path until #436a lands. After that
|
||||
the loader refuses or ignores a grant with no Approve record, and the refusal is logged.
|
||||
- Drop the hand-written-grant bounds sentence and the N1 and N2 notes. Rewrite the B2 note to the
|
||||
ruling.
|
||||
|
||||
## My r2 notes under mos-1129
|
||||
|
||||
Sage asked whether anything is left of N1 and N2:
|
||||
- N1 goes away. A hand-written grant makes nothing, so no bounds need to reach it.
|
||||
- N2 goes too. I also withdraw it on its own terms. It said a revoke never rewrites the user's file. That
|
||||
was wrong: the machine-grant r2 section 6 step 2 drops the grant through the grant writer, and that's
|
||||
safer, because a restart can't bring the command list back.
|
||||
- One point is left. The text should say how the change from row 210's hand-edited grants to Approve-only
|
||||
happens, as in the second fix bullet. Without it, the row 210 grants that exist when #436a lands
|
||||
either keep working, against mos-1129, or stop with no record.
|
||||
|
||||
A round that changes only these lines gets a diff check.
|
||||
@@ -1,42 +0,0 @@
|
||||
# Diff check: Honker PRD 1.8 draft r4, for mos-1129 and duckworth-0059 B1 (duckworth-0061)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per honker-0036:
|
||||
- 781d91624 on `honker/row194-b2`, one commit on r3 d16ccdb91, one file, `PRD-1.8-DRAFT.md`, 318 lines,
|
||||
sha256 a08ffc95…9833 (checked). The diff is +89 -71. I read all of it.
|
||||
|
||||
My r3 diff check (duckworth-0059, changes B1, committed at 9dceb2148) crossed with r4 and wasn't sent on
|
||||
its own. r4 answers its B1, so this note replaces it.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
B1 from duckworth-0059 is met:
|
||||
- REQ-GRANT-2 (:170-174): only an Approve creates a grant, the code uses a grant only with a matching
|
||||
Approve record, and a grant written by hand makes nothing. Row 210's hand grants stay a test path until
|
||||
#436a moves `host.run` to Approve-made grants.
|
||||
- The hand path is out: the bounds sentence on hand grants is gone from REQ-GRANT-2, REQ-CRED-1's append
|
||||
(:209-210) says "not by hand", and the B2 note (:250-258) states the ruling with Q188's reason.
|
||||
- My 0058 N1 and N2 notes are removed, as 0059 asked.
|
||||
- The top of the draft (:22-30) names the B7 residual and the `host.sudo` arbiter route for Jason. That
|
||||
matches the machine-grant design's section 9 and its S1 "same-user limit".
|
||||
|
||||
The other r4 changes read as mos-1129 rules:
|
||||
- N4 and C1 (:58-62, :69-73, :163-169): a grant that makes an account is one `credential.mint` decision
|
||||
that shows the account name. `account.create` stays as 1.7 has it, and only `credential.mint` is
|
||||
chief-only. That keeps C1's point: a PM's service sign-up routes as before.
|
||||
- N1 "the target or its enrolled identity" (:186), N2 (:204), N3 (:192-193), N5 landing step 5 (:78-80)
|
||||
and N6 (:84, :169) are in.
|
||||
|
||||
The transition after #436a (what the loader does with a leftover hand grant) is a design matter, and the
|
||||
machine-grant design r4 S1 covers it: the loader refuses `hosts` in the business file at load. "Makes
|
||||
nothing" in the PRD is enough as an outcome.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Two notes are stale. Neither is in the requirement text.
|
||||
- :246-247: "Duckworth hasn't reviewed their draft yet". I've reviewed the machine-grant design r1 to r4
|
||||
(duckworth-0056, -0060).
|
||||
- :263-267: "Their design gives revoke to the user and the tester only, and their Q-S5 asks Jason…". The
|
||||
design at r3 and r4 (1c5705d6d section 6) follows mos-1124 d, and its Q-S5 is answered. Point the
|
||||
bullet at 1c5705d6d and drop the Q-S5 clause.
|
||||
|
||||
Doc-only, so they can be fixed before the text goes into the PRD, without another check.
|
||||
@@ -1,51 +0,0 @@
|
||||
# Review: Honker PRD 1.7 draft, REQ-CRED-3 host access through the credential store (duckworth-0038)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: agents/honker/work/queue-194/PRD-1.7-DRAFT.md at
|
||||
0bb1a3bd0 on honker/row194, sha256 0236dee8…40fd (checked), diff +32 -4 over 69544f342, per sage-1428
|
||||
item 2. I checked the quotes against DECISIONS.md:1533-1571 in jason-master-restart at 1c368ff (read only),
|
||||
and the cited requirements against docs/prd/mosaic-stack.md at 0bb1a3bd0.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
What holds:
|
||||
- The Q183 quote (draft :47-53) matches DECISIONS.md:1537 word for word, from "I will still want" to "I am a
|
||||
happy man." It leaves out the first and last sentences, and "in part" says so.
|
||||
- The Q184 quote "Q5: by 10.27" matches :1558. The due date 2026-10-27 is Mos's reading at :1568, and the
|
||||
draft uses it only as the #424 due date.
|
||||
- "Not every agent gets a grant" and "An agent without a grant for that host is refused" rest on "not all
|
||||
agents will need to have that access" and "The system will enforce access to the credentials".
|
||||
- "The agent never sees the key or any other secret value" is already PRD law. REQ-CRED-1 keeps tokens out
|
||||
of an agent's environment and files, and REQ-VAR-2 lets only the broker read a secret's contents. Citing
|
||||
both is right.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. REQ-CRED-3 (draft :113-114) says the stack "then does the ssh and the sudo for the agent", and landing
|
||||
item 5 (:30) says "the stack's ssh and sudo for a granted agent". Jason didn't say that. He said the
|
||||
credential "works as some sort of variable for the agent to call" and that "the system will enforce access
|
||||
to the credentials". Running the ssh on the agent's behalf is one design. Lending the agent a signing socket
|
||||
that never exposes the key is another, and both meet REQ-CRED-1. Which one it is belongs to the #424 design
|
||||
(row 210, Gizmoduck), which I review. Fix: state the outcome and the enforcement only. For example: "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." Change item 5
|
||||
to "adds the host credential grant check and the host access it allows (REQ-CRED-3, Vikunja #424)".
|
||||
|
||||
B2. The last sentence of REQ-CRED-3 (:116), "Jason grants one through a reviewed role or config change",
|
||||
answers the open point that the note at :151-153 says Q183 leaves open. The note says both "Q183 doesn't
|
||||
say who grants" and "The text leaves it as Jason through a reviewed role or config change", so the
|
||||
requirement invents the answer the note calls open. Fix: in the requirement, say it is open. For example:
|
||||
"Who grants a host credential, and how, is set by the #424 design, which Jason approves." Reword the note to
|
||||
match. If Sage wants a floor that already exists in the PRD, "a grant changes only through a reviewed
|
||||
commit (REQ-ROLE-1)" is sourced, but "Jason grants" is not.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Jason's words name "infra agent(s) on my .53 instance". REQ-CRED-3 is general, which is fine because
|
||||
the grant gates it. The source bullet carries the scope, so nothing is lost.
|
||||
N2. Jason's outcome is NOPASSWD sudo. REQ-CRED-3 says only "with sudo". That's fine if the design keeps
|
||||
sudo passwords out of the agent's hands too, which REQ-CRED-1 already requires of any secret. The design
|
||||
review checks it.
|
||||
N3. "Jason through Mos (mos-1067), DECISIONS Q183" (:47): Jason said it in Mos's thread 38bc4c81, and Mos
|
||||
recorded it. "Recorded by Mos" would be more exact than "through Mos". Doc-only.
|
||||
|
||||
A round with only B1 and B2 changed gets a diff check, not a full re-read. Sage's cutoff is 23:00Z.
|
||||
@@ -1,19 +0,0 @@
|
||||
# Review: Honker PRD 1.7 draft r2, REQ-CRED-3, diff check (duckworth-0039)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: agents/honker/work/queue-194/PRD-1.7-DRAFT.md at
|
||||
11cb0423b on honker/row194, sha256 e896746d…8647 (checked), per honker-0019. 11cb0423b is a fast-forward
|
||||
of 0bb1a3bd0, and the diff touches that one file only, +12 -11. I read the diff only, as REVIEW-r1 said I
|
||||
would.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- B1 resolved. REQ-CRED-3 states the check, "before the credential is used", and no longer says how the
|
||||
stack reaches the host. Item 5 reads "the host access it allows". The new note says the #424 design
|
||||
picks the path.
|
||||
- B2 resolved. The requirement now says who grants, and how, is set by the #424 design, which Jason
|
||||
approves. The open-point note matches it and no longer names a granter.
|
||||
- The build line (row 210, #1707, Gizmoduck, on top of row 199) is Sage's routing (sage-1430). It is a
|
||||
reviewer note, not PRD text.
|
||||
- Nothing outside B1, B2 and those notes changed. The quotes I checked in r1 are untouched.
|
||||
|
||||
N1 to N3 from r1 stay as notes.
|
||||
@@ -1,59 +0,0 @@
|
||||
# Review: PRD 1.7 and 1.8 landing (duckworth-0075)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1636 and sage-1641. Subject:
|
||||
`honker/prd-17-18` 46c8adaeb, parent 011d41ba6, 4 files. Checked:
|
||||
- `docs/prd/mosaic-stack.md`: 1196 lines, sha256 a5bba06c…4fc6209f3.
|
||||
- Sources: 1.7 is `PRD-1.7-DRAFT.md` at 9ba31b5b3, sha256 e896746d…1d328647. 1.8 is r7 at 82df17347,
|
||||
sha256 de079281…727440cd.
|
||||
- refactor 816d48b03 hasn't touched the four paths since 011d41ba6.
|
||||
|
||||
## Verdict: approve, no change to what any requirement allows or forbids
|
||||
|
||||
1. Every word of each requirement matches its source. A script took each quoted block of both drafts,
|
||||
collapsed the whitespace and checked each item for one contiguous match in the landed file. All 42
|
||||
items match, including every sub-bullet, the CRED-1 and WAIT-3 appends and the risks bullet. The only
|
||||
two misses are the status sentences, where the drafts had a `<prd.approve decision id>` placeholder.
|
||||
I read the whole diff (8 hunks), and no added line comes from anywhere but the drafts or the five
|
||||
deviations below.
|
||||
2. The deviations are layout and approval record only:
|
||||
- The status field goes from 1.6 to 1.8. The 1.6 landing bumped this same field in place
|
||||
(63c82f1c4).
|
||||
- The 1.7 status cites decision d52febb5 (prd.approve, yes). DECISIONS:1611 confirms decision 21
|
||||
was resolved yes by jason at 2026-10-20T20:36:47Z on the 9ba31b5b3 file.
|
||||
- The 1.8 status says Jason approved it in T3 through Mos (Q192). Its source bullet quotes
|
||||
DECISIONS:1765 exactly, and the Q191 source bullet quotes :1745 ("Q13: that looks better")
|
||||
exactly.
|
||||
- COS-3's 1.7 block sits after ASSIST-3, the last item of the 1.2 list, so the 1.2 items keep
|
||||
their heading.
|
||||
- The CRED-1 append is a paragraph inside CRED-1 after its Vikunja sub-bullet. The two WAIT-3
|
||||
appends are paragraphs inside WAIT-3, the last item before `### Messages`.
|
||||
- TRUST sits after DEC-6, the last Decisions item. CRED-4 and GRANT-1 to 4 sit after CRED-3.
|
||||
LAND-2 sits after LAND-1. The risks bullet ends that list.
|
||||
3. The ten ids are COS-3, CRED-3, CRED-4, GRANT-1 to 4, TRUST-1, TRUST-2 and LAND-2. Each appears in
|
||||
the PRD as a `- **REQ-X-N.**` bullet and in `requirements.json` with the same id and in the same
|
||||
order. `tasks/tests/requirements.test.mjs` asserts the id list equals the PRD's in order, that no
|
||||
bold id is missed, the count of 68 and version 1.8, and that the status line names that version.
|
||||
4. The titles don't change what any requirement allows or forbids. COS-3, CRED-3, GRANT-1, GRANT-3,
|
||||
GRANT-4, TRUST-1, TRUST-2 and LAND-2 are fair summaries. GRANT-2 drops "and the target", but it
|
||||
leaves nothing allowed that the text forbids. On CRED-4 see N1.
|
||||
5. Pins. The tasks test moves 58 to 68, '1.6' to '1.8', and the `twice` index to [68]. Bus
|
||||
`requirements.test.mjs:48` moves 58 to 68. No other test pins the count or the version. No test or
|
||||
script pins a hash of the PRD or `requirements.json` (0 hits, control 11).
|
||||
`requirements.json` is in `scripts/stack-copy.sh:30` and `cli stack-copy.test.mjs:37` as a path
|
||||
only.
|
||||
|
||||
My runs, in a detached worktree of 46c8adaeb:
|
||||
- tasks: requirements 3/0, verbs 31/0, and the whole tasks package 97/0.
|
||||
- bus: requirements 3/0.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The CRED-4 title, "...only with its kind declared, a grant, an allowlisted command and a live key",
|
||||
is stricter than the requirement in two places:
|
||||
- The allowlist applies only "for a command", but the title reads as if every grant action needs an
|
||||
allowlisted command. A Gitea token use has none.
|
||||
- "live key" drops "or token".
|
||||
The PRD text governs, and a stricter label permits nothing extra, so this isn't an allow/forbid
|
||||
change. Agents do read the titles through the bus `requirements` verb, though. A possible wording:
|
||||
"A within-role grant action needs no decision only with its kind declared, a grant, a live key or
|
||||
token and, for a command, an allowlisted command". Sage's call.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Delta check: REQ-CRED-4 title (duckworth-0076)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in honker-0064 (sage-1644, my duckworth-0075 N1).
|
||||
Subject: `honker/prd-17-18` 46c8adaeb..c03d660e7. One line of `packages/tasks/requirements.json`
|
||||
changes, and nothing else.
|
||||
|
||||
## Result: approve, one note
|
||||
|
||||
The new title is "Within-role grant actions need no decision with the kind declared, a grant, a live key
|
||||
or token, any command allowlisted". It's 120 characters, exactly the schema's limit
|
||||
(`packages/tasks/src/requirements.mjs:33`).
|
||||
- "live key or token" now matches the text.
|
||||
- "any command allowlisted" applies only when there is a command, which also matches.
|
||||
- Nothing else in the file changed.
|
||||
|
||||
N1 (doesn't block). The old title said "only with". The new one drops "only", so it lists the
|
||||
conditions as enough to skip a decision but no longer says they're required. REQ-CRED-4 says "only when
|
||||
all of these hold". The title states nothing false and permits nothing the text forbids, and the PRD
|
||||
governs. Still, the one word that carries the requirement's restriction is gone.
|
||||
|
||||
This wording keeps "only" in 117 characters: "A within-role grant action needs no decision only if kind
|
||||
declared, grant, live key or token, any command allowlisted". Sage's call.
|
||||
|
||||
## Follow-up: 2884ee496 (duckworth-0077)
|
||||
|
||||
Asked in honker-0066 (sage-1648). 2884ee496 on c03d660e7 changes one line of
|
||||
`packages/tasks/requirements.json` and nothing else. The REQ-CRED-4 title is now my 117-character
|
||||
wording, byte for byte. That resolves N1. Approve, no notes.
|
||||
|
||||
My runs, in a detached worktree of 2884ee496: tasks requirements 3/0, bus requirements 3/0.
|
||||
@@ -1,32 +0,0 @@
|
||||
# Scoped check: PRD 1.8 r7 with follow-up, 762a6711f..82df17347 (duckworth-0073)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1624, sage-1627, sage-1632 and sage-1633 (Jason
|
||||
Q191, mos-1149, mos-1152). Subject: `honker/prd18-r7` at 82df17347, two commits on r5 762a6711f: 47c2cb33c
|
||||
(r7) and 82df17347 (Mos's r6 rulings). One file, `agents/honker/work/queue-194/PRD-1.8-DRAFT.md`, 342
|
||||
lines, sha256 de079281…727440cd (checked), 8+/4- against 47c2cb33c (checked). This is the one verdict
|
||||
for both commits. It includes duckworth-0072 (`REVIEW-r7-scoped.md`, the r7 commit and the enforcement
|
||||
note at 011d41ba6).
|
||||
|
||||
## Result: passes the scope, no findings
|
||||
|
||||
r7 (47c2cb33c), from duckworth-0072:
|
||||
1. The only changes are the revision paragraph, the top note, one sentence added to REQ-GRANT-1, and the
|
||||
risks notes. No other requirement text changes.
|
||||
2. The top note and the REQ-GRANT-1 sentence match sage-1624 word for word.
|
||||
3. "one role under two names" is gone from the file.
|
||||
4. Every file:line in the enforcement note is true at refactor 011d41ba6 (list in `REVIEW-r7-scoped.md`).
|
||||
|
||||
Follow-up (82df17347 against 47c2cb33c). There are four hunks, all in scope:
|
||||
- `:18-20` revision clause: it names Mos's rulings on r6 items 1, 3 and 4 (mos-1152) and describes the
|
||||
three edits correctly. Item 2 isn't claimed.
|
||||
- `:25` (b): "Also for Jason at approval:" is now "For Jason at approval:".
|
||||
- `:27-28` (1): the first bullet under the heading is Mos's sentence, word for word as sage-1632 quotes it:
|
||||
"Q188's 'you, Mos or the tester' is read as the user, the assistant for grants in its own organization,
|
||||
and the tester only on a dev instance (Mos's reading, mos-1124 d)."
|
||||
- `:253` (3): the citation is now "design r1 to r4 (duckworth-0056 and duckworth-0060)". Both labels are
|
||||
right. 0056 is my machine-grant r1 review (f6b6a04b6). 0060 is my r3+r4 review (1c5705d6d), and my r2
|
||||
draft was dropped unsent, as SESSIONS records.
|
||||
- `:273` (4): ", and their Q-S5 is answered" is removed. The sentence now ends at "(mos-1124 d)." Nothing
|
||||
else in that bullet changed.
|
||||
|
||||
No other line changed, and the file no longer contains "Also for Jason" or "Q-S5".
|
||||
@@ -1,46 +0,0 @@
|
||||
# Scoped check: PRD 1.8 draft r7 (duckworth-0072)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1624 and sage-1627 (Jason Q191, mos-1149).
|
||||
Subject: `honker/prd18-r7` at 47c2cb33c, one commit on r5 762a6711f, one file:
|
||||
`agents/honker/work/queue-194/PRD-1.8-DRAFT.md`, 338 lines, sha256 a50ce730…b0551e (checked), 29+/10-.
|
||||
Sage's scope only. The dangling "Also for Jason at approval:" is known (sage-1627) and not reviewed
|
||||
here.
|
||||
|
||||
## Result: passes the scope, no findings
|
||||
|
||||
1. No other requirement text changed. The diff has four hunks:
|
||||
- the revision paragraph (`:16-18`, convention)
|
||||
- the top note (`:20-21`)
|
||||
- one added sentence at the end of REQ-GRANT-1 (`:170-171`)
|
||||
- the two "Risks and open questions" notes (`:293-314`)
|
||||
REQ-GRANT-2 onward and every other requirement are unchanged.
|
||||
2. The quoted lines are exact.
|
||||
- Top note: "Only the chief of staff raises a grant request. The assistant, the user's main agent,
|
||||
never raises one. It passes the user's request to the chief and can revoke a grant in its own
|
||||
organization."
|
||||
- REQ-GRANT-1: "The assistant and the chief of staff are separate roles. No agent holds both, and the
|
||||
broker refuses a claim that would."
|
||||
Both match sage-1624 word for word.
|
||||
3. "Which roles may request" no longer reads "one role under two names". That phrase appears nowhere in
|
||||
the file. The note now says the assistant is a separate role, passes the request to the chief
|
||||
(REQ-ASSIST-1) and can revoke in its own organization (REQ-GRANT-4).
|
||||
4. Every file:line in the enforcement note is true at refactor 011d41ba6:
|
||||
- `business/src/business.mjs:118-120`: the instance's single `definition` is required and loaded.
|
||||
- `business/src/vocabulary.mjs:134`: `THREAD_DEFINITION = "assistant"`.
|
||||
- `bus/src/broker.mjs:164` (`STAFF_TO = 'chief-of-staff'`) and `:2137` (the `no-chief-of-staff`
|
||||
check against `STAFF_TO`).
|
||||
- `broker.mjs:451-456`: `bindLaunch` takes one `role` (keys at `:454-455`). `:478` sets the run key
|
||||
as `business:run`. `:479` gives `duplicate-run`, and `:499` gives `launch-record-mismatch` on a
|
||||
rebind under another role.
|
||||
- `cli/src/launcher.mjs:438-449`: one `spawnSession` with `role: instance`, then one `bindLaunch`
|
||||
for that instance.
|
||||
- `broker.mjs:1689-1734`: `role.claim` writes only `s.role`. `:1693-1709` is the thread branch,
|
||||
which writes `thread_claims` and returns before `role_claims`. A threaded definition must bind a
|
||||
thread (`:474`, `thread-required`), so an assistant run always takes that branch.
|
||||
- `bus/src/migrations/3b.sql:117-119`: `role_one_holder`, unchanged in `schema.sql:117`.
|
||||
- `broker.mjs:34`: `credential.mint` is in ACTIONS (`:9-43`) and not in ROUTED (`:46-69`), so it's
|
||||
in HARD_WAITS (`:71`). There's no other use in bus or business src, so no check limits which role
|
||||
raises it, as the note says.
|
||||
|
||||
The "not enforced" claims also hold. The run key is per business, and nothing in the broker ties two
|
||||
runs to distinct processes or users beyond the launcher's record.
|
||||
@@ -1,107 +0,0 @@
|
||||
# Review: ops-01 Q180 tightening plan r2 (duckworth-0023)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review of the document. I ran nothing on pve, PBS, .53 or
|
||||
.54. The PVE and PBS permission rules below come from ops-01's reading of the local schemas and source, and
|
||||
I didn't re-check them.
|
||||
Subject: /mnt/storage/scratch/tmp/mosvk/q180-tightening-plan.md, written 19:31:42Z, sha256 1084aa15…585f
|
||||
(checked), Vikunja #421. Mos asked for four checks (mos-1025), and ops-01 asked five questions. Both are
|
||||
answered at the end. Cross-checked against FED R8 at eb651aa68.
|
||||
|
||||
## Verdict: changes, B1 to B5
|
||||
|
||||
The diagnosis in §0 is right and the order is right: §1 first, because one key that both Jason and the
|
||||
factory use opens root everywhere. Removing sudo entirely, instead of moving to a sudo password, is right
|
||||
for accounts that run agents. Five gaps block it.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. Lockout (Mos check 1). The plan relies on "nothing if 1a holds" and an open console session. Neither is
|
||||
proven before the step that depends on it.
|
||||
- Before 1d, and before each of 2a to 2c, Jason logs in with the new key from a fresh session to each account
|
||||
that step leaves him: root on pve, root on PBS, and localadmin on .53 and .54, plus a `sudo -v` there. The
|
||||
step runs only after he has.
|
||||
- Say whether localadmin's sudo takes a password. The `sudo` group asks for one by default, and localadmin
|
||||
has none today. If it's NOPASSWD, say so on purpose.
|
||||
- The rollbacks for 2a to 2c say "console in". A cloud-init image usually has root locked. So a console login
|
||||
on .53 and .54 needs a local password that only Jason holds, tested through the pve console before 2c.
|
||||
- Jason's pve web login (root@pam or his own user) is the fallback for all of the above. Confirm it works
|
||||
and that no factory-readable file holds it.
|
||||
|
||||
B2. Where the new key lives. Two things decide it.
|
||||
- FED R8 fingerprints every private key under every home on every factory host, dragon-lin included. The
|
||||
gate fails if any of them matches an `authorized_keys` on the hub host.
|
||||
- localadmin on .54 trusts Jason's new key.
|
||||
|
||||
So the new key, or a FIDO key handle file, can't sit under any home on dragon-lin, and R8 would fail if it
|
||||
did. A FIDO key used from dragon-lin also lets any factory process in Jason's agent socket ask for a touch.
|
||||
The plan should say "a key on another machine" and drop the dragon-lin FIDO option. After this, Jason admins
|
||||
.54 from a machine that isn't a factory host, or through the pve console.
|
||||
|
||||
B3. Group removal doesn't reach processes already running (2a, 2b). Supplementary groups are fixed when a
|
||||
process starts. Hermes's 12 user units, openclaw-gateway and the others keep `docker`, `lxd` and `sudo`
|
||||
until jarvis's user manager restarts. melanie's two gateways are the same.
|
||||
- After the change, restart `user@<uid>.service` for each account, or end every process the account has.
|
||||
Then show that no process of that uid lists the removed gids (`Groups:` in /proc/<pid>/status).
|
||||
- List any uid 0 process started from a jarvis or melanie session through sudo (by loginuid), and end it.
|
||||
- Losing the `docker` group doesn't close a Docker API on TCP, or a container that mounts docker.sock behind
|
||||
a UI jarvis can reach. Check both on .53, by name only.
|
||||
|
||||
B4. Cloud-init order (Mos check 3). Yes, cloud-init can rewrite sudoers. A change to a VM's cloud-init
|
||||
settings on pve regenerates its config drive. A new instance id makes per-instance modules run again at the
|
||||
next boot, and those write `90-cloud-init-users` and the keys back. 1012 has a boot scheduled at
|
||||
2026-10-21T09:00Z (§5). The order has to be:
|
||||
1. In the guest: remove `90-cloud-init-users` and create `/etc/cloud/cloud-init.disabled`.
|
||||
2. Then on pve: clear `sshkeys`, and on 1012 clear `ciuser` too if it isn't needed.
|
||||
|
||||
The plan has these the other way round. Also check after the 09:00 boot that `90-cloud-init-users` is still
|
||||
gone.
|
||||
|
||||
B5. Paths the plan leaves unaccounted for (Mos check 4). The plan inventories ssh keys only. These other
|
||||
paths can reach the hub VM, its backups or its issuer. Each one needs to be either closed here or listed as
|
||||
open with an owner, so R8's table can name it.
|
||||
- PVE and PBS accounts: `pveum user list`, token and ACL lists, and the same on PBS. Any account or token
|
||||
whose secret is factory-readable (dragon-lin, .53, a Hermes profile) is a path.
|
||||
- PBS `root@pam!pve-backup`, which has Admin on `/`. Where is its secret? Remove it unless something needs it.
|
||||
- Rollback copies (§4). The 1e rollback restores the pve→PBS password file "from the copy". Copies of
|
||||
secrets and of sshd or sudo files have to stay on the host they came from, root-only (for example
|
||||
`/root/q180-rollback/`). They can't go under `~/.config/mosaic-dev/` on dragon-lin, which the factory can
|
||||
read.
|
||||
- Storage backing. Say where 1012's disks and the PBS datastore physically live. If either is on TrueNAS or
|
||||
other network storage, then root or a share on that storage reads the guest disk or the backup chunks,
|
||||
around every PVE and PBS check. The `jwoltje@truenas` key and the "tank" migration point that way.
|
||||
- The two step-ca containers on .53, which is a factory host. Say what they sign. If they sign anything the
|
||||
hub or its sign-in provider trusts, they are the R8 "certificate authority" item.
|
||||
- The hub's sign-in issuer: which host runs it and who can admin it. The plan doesn't name it.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. OpsAudit (§1.1) has to be two roles. A role brings all its privileges to every path it's granted on.
|
||||
One role holding `Sys.Audit` and `VM.Audit` granted on `/vms` would open monitor `info`. Use one role
|
||||
(`Sys.Audit`, `Datastore.Audit`, `Pool.Audit`) on `/nodes` and `/storage`, and a second role (`VM.Audit`
|
||||
only) on `/vms`. Add three endpoints to the table, with what each one shows for 1012:
|
||||
`nodes/{node}/report`, other users' task logs (`tasks/{upid}/log`) and the VM firewall log. Check whether
|
||||
1012 sits in a pool with an ACL, since that is a second grant path to the VM.
|
||||
N2. Melanie's Hermes on .54 conflicts with FED R8 ("the hub host runs no other instance or agent"). R8
|
||||
can't pass until those gateways leave .54. Also, all 11 melanie logins came from dragon-lin with the jason
|
||||
key, so after 2b Melanie has no ssh into .54 at all. Say whether that's intended.
|
||||
N3. The 1b watch fails every hour until it's switched to the API. Switch it in the same step, or silence the
|
||||
row for the change window, so the failure doesn't train people to ignore that row.
|
||||
|
||||
## Answers
|
||||
|
||||
Mos (mos-1025):
|
||||
1. Lockout: possible as written. B1 closes it, and B2 decides where Jason admins from.
|
||||
2. PVEAuditor and DatastoreAudit on 1012: the §1.1 and §1.2 tables are consistent with what I know of
|
||||
PVE 9 and PBS 3. Monitor `info` is the one inside-guest leak, and N1's two-role split closes it. Config
|
||||
and listings show metadata only, with cipassword masked.
|
||||
3. Cloud-init can rewrite sudoers: yes. B4.
|
||||
4. Remaining factory paths: yes, the B5 list. The factory key and jarvis@w-jarvis are closed by §1 and §2.
|
||||
Accounts, tokens, storage, the CA and the issuer aren't covered yet.
|
||||
|
||||
ops-01:
|
||||
1. §0: agreed, and §1 first.
|
||||
2. §1.1: N1. Nothing else from inside a guest that I can name. B5's storage item is the larger leak.
|
||||
3. §1.2: agree with deferring encryption, on the three conditions plus B5. That means no factory reach to
|
||||
the datastore's backing storage, and no factory-readable PBS credential, including `pve-backup`.
|
||||
4. §2: agree, with B1, B3 and B4.
|
||||
5. §6: Jason's call. I agree that Q180 has to override Q172 on these four hosts, or the hub has no boundary.
|
||||
@@ -1,70 +0,0 @@
|
||||
# Review: ops-01 Q180 tightening plan r3 (duckworth-0028)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I reviewed the document only and ran nothing on pve, PBS, .53 or .54.
|
||||
Subject: /mnt/storage/scratch/tmp/mosvk/q180-tightening-plan.md, sha256 cd7165bf…0bc3fce (checked), Vikunja
|
||||
#421. Checked against duckworth-0023 (REVIEW-r2.md, 523a6bddf) and mos-1041 as ops-01 relays it.
|
||||
|
||||
## Verdict: changes, B6
|
||||
|
||||
B1 to B5 are resolved, and so are N1 to N3:
|
||||
- B1: the Jason checklist does what B1 asked. It adds a fresh login with the new key on every remaining
|
||||
account before 1d, 2a, 2b and 2c. It adds a console password tested through noVNC, and a pve web login
|
||||
from the non-factory machine only. That last one is Mos's N4, and the browser-profile reason is right.
|
||||
localadmin's sudo takes a password.
|
||||
- B2: the new key never touches a factory host, and the FIDO-on-dragon-lin option is gone.
|
||||
- B3: after the change, each account restarts its user manager and runs terminate-user. A /proc `Groups:`
|
||||
count must then be 0. uid 0 processes are found by loginuid. The Docker TCP and socket-mount checks were
|
||||
read at 19:39Z and get re-read right before 2a.
|
||||
- B4: the order is guest first, then pve, and is checked at the next boot. The 09:00Z boot of 1012 gets a
|
||||
meta dump before and after, an instance-id comparison and hashes.
|
||||
- B5: §1.4 closes each path or gives it an owner. The hub's sign-in issuer is open, and Sage owns it. The
|
||||
plan says that the row has to be filled before the plan is complete. That meets B5. It also means R8
|
||||
can't pass until the row is filled.
|
||||
- N1: two roles, and no pools. Your `nodes/pve/report` finding (Report.pm dumps guest configs raw, so the
|
||||
`cipassword` hash comes through `Sys.Audit`) is a good catch. Step 1a2, done before `OpsAuditHost` is
|
||||
granted, is the right fix.
|
||||
- N2 and N3: done.
|
||||
|
||||
## Blocker
|
||||
|
||||
B6. Root access already granted isn't undone by removing it. This is new in r3, and it is my miss in r2.
|
||||
For a long time, factory processes had root on all four hosts:
|
||||
- the jason key, which is dragon-lin's key, on pve, PBS and root on .53 and .54
|
||||
- jarvis@w-jarvis on pve, 1416 Hermes logins
|
||||
- NOPASSWD sudo for jarvis and melanie
|
||||
|
||||
The plan removes these paths, but anything a factory process left behind as root survives that. Examples:
|
||||
- another file in sudoers.d
|
||||
- a key in a different user's `authorized_keys`, or in `/etc/pve/priv/authorized_keys`
|
||||
- a uid 0 line in `/etc/passwd`
|
||||
- a root cron entry or systemd unit
|
||||
- a setuid binary, `ld.so.preload`, or a PAM or sshd drop-in
|
||||
The hub (.54) is the host where R8 needs this ruled out. The plan needs one of these, before 2b, with an
|
||||
owner:
|
||||
- (a) A read-only sweep on pve, PBS, .53 and .54, reported by name, owner, mode and hash only:
|
||||
- every file in `/etc/sudoers.d` and `/etc/sudoers`
|
||||
- every `authorized_keys` file on the host, found with `sshd -T`'s `authorizedkeysfile`, plus any
|
||||
`authorizedkeyscommand`
|
||||
- uid 0 and gid 0 accounts in `/etc/passwd`
|
||||
- root and system crontabs and `/etc/cron.*`
|
||||
- units in `/etc/systemd/system` and `/etc/systemd/user` that no package owns
|
||||
- setuid and setgid files that no package owns, and `dpkg -V` failures for sudo, openssh, pam and
|
||||
systemd
|
||||
- `/etc/ld.so.preload`, and drop-ins under `/etc/pam.d` and `/etc/ssh/sshd_config.d`
|
||||
|
||||
Jason reviews anything unexpected, and the sweep runs again after §2.
|
||||
- (b) Rebuild 1012 from a clean image instead of cleaning it in place. That fits N2 too, since Melanie's
|
||||
Hermes has to leave the hub anyway.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N4. localadmin's password has to exist at 1a. 1a creates localadmin with sudo by password, and the
|
||||
checklist runs `sudo -v` there before 2a. But the console-password line says "before 2c". Jason sets the
|
||||
password himself with `passwd localadmin` in the pve console when 1a runs. It never goes through a factory
|
||||
session or script. The noVNC test can stay before 2c, or move up to 1a.
|
||||
N5. Before `OpsAuditHost` is granted, run `pvereport` once as root on pve. Count the matches for password,
|
||||
secret, token, key and `BEGIN` lines, and print counts only. That checks your by-filename reading of
|
||||
Report.pm against this PVE build.
|
||||
N6. On pve, `/root/.ssh/authorized_keys` is a link to `/etc/pve/priv/authorized_keys`, which the cluster
|
||||
filesystem holds. Edit that file, copy that file, and say so in 1d. Then check that `pvecm updatecerts` (or
|
||||
an upgrade) doesn't add back anything other than root@pve.
|
||||
@@ -1,80 +0,0 @@
|
||||
# Review: ops-01 #423 VM tightening r4, steps 1 to 3 (duckworth-0032)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I reviewed the document only and ran nothing on pve, .53 or .54.
|
||||
Subject: /mnt/storage/scratch/tmp/mosvk/q183-vm-tightening-r4.md, sha256 03cd1075…cbc05f (checked), 122
|
||||
lines, Vikunja #423. This is ops-01's 20:00:49Z amendment of eadff6b6. I read eadff6b6 in full. In the
|
||||
amendment, lines 1-49 (§0 to §3) are the same apart from the §0 step-ca correction at :20, and §8 is new.
|
||||
Checked against the mos-1054 remap of my r2 items, my mos-1060 carry item and mos-1062 (step 4 is on hold
|
||||
per mos-1061, and step 5 waits for Bean and Chet). Per mos-1065, step 1 runs first after approval.
|
||||
|
||||
## Verdict: changes on steps 1 to 3, B1 only
|
||||
|
||||
Step 1 and step 3 can run as written once B1 is in the text. If r5 changes only B1 and any notes ops-01
|
||||
takes, I will check the diff and approve it without a full re-read.
|
||||
|
||||
What holds:
|
||||
- mos-1060 carry: met. NOPASSWD comes from its own `50-localadmin` (0440, `visudo -cf` then `visudo -c`),
|
||||
localadmin is made with `useradd`, and neither `90-cloud-init-users` nor a cloud-init `users:` block is
|
||||
involved. The §3 post-boot check covers the `50-localadmin` hash.
|
||||
- B1 (r2) as remapped: one lockout check, `ssh localadmin@<vm> sudo -n true`, before any removal and again
|
||||
before each later step (:30).
|
||||
- B4 (r2): `cloud-init.disabled` in the guest first, then the field deletes on pve, then a meta dump and an
|
||||
instance-id and hash check at the next planned boot (:44-46). Deleting fields will change the config
|
||||
Proxmox renders, so a new instance-id is expected. With cloud-init already disabled it does nothing,
|
||||
which is why this order is the right one.
|
||||
- B5 (r2) as remapped: in-VM paths and the 1008/1012 cloud-init fields only (:5).
|
||||
- Rollback copies stay root-only on the host they came from (:8).
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Step 2's verify can't tell whether `PermitRootLogin no` took effect (:36-37). sshd keeps the first value
|
||||
it reads for a keyword. Ubuntu's and Debian 12's `sshd_config` put `Include /etc/ssh/sshd_config.d/*.conf`
|
||||
at the top, and drop-ins load in lexical order. §0 records `PermitRootLogin without-password` but not where
|
||||
it is set. If it is in a drop-in that sorts before `60-q183.conf` (`50-cloud-init.conf` is a common one),
|
||||
that value wins and `60-q183.conf` does nothing. `sshd -t` only checks syntax. `ssh root@<vm>` is refused
|
||||
either way, because the key is gone, so the stated verify passes in both cases.
|
||||
Fix, in step 2's verify:
|
||||
- `sshd -T -C user=root,host=x,addr=127.0.0.1 | grep -i '^permitrootlogin'` prints `permitrootlogin no`.
|
||||
If it doesn't, find the earlier setting, then rename the drop-in to sort first or edit the setting itself.
|
||||
- From the same `sshd -T`, read `authorizedkeysfile`, and confirm that every file it names under `/root`
|
||||
holds no key (`.ssh/authorized_keys2` is in the default list). Report counts only.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Step 1 `useradd` does not make a home directory without `-m`, and the default shell may be `/bin/sh`.
|
||||
Write `useradd -m -s /bin/bash localadmin`. `~/.ssh` should be 0700 and `authorized_keys` 0600, both owned
|
||||
by localadmin, or `StrictModes` refuses the key. The lockout check would catch either, but it's cheaper to
|
||||
get it right the first time.
|
||||
N2. Step 1 copies "the jason key" from root's file (:29). Copy only that key line, matched by fingerprint,
|
||||
without any options in front of it. Cloud-init's `disable_root` writes a `command="echo 'Please login as
|
||||
…'"` prefix into root's file. If that prefix is copied, every localadmin login runs it. The lockout check
|
||||
would fail closed on it.
|
||||
N3. Write the sudoers file to a temp path, run `visudo -cf` on it, then `install -m 0440 -o root -g root` it
|
||||
into place, then `visudo -c`. A file that fails the check never sits in `/etc/sudoers.d`, where a syntax
|
||||
error makes sudo refuse to run.
|
||||
N4. Before step 2, check whether anything logs in as root by key on a schedule. Count `Accepted publickey
|
||||
for root` lines in the ssh journal for the last 14 days, by source address. Report counts only. A
|
||||
scheduled job like that would break at step 2 and nobody would notice until it fails.
|
||||
N5. The step 2 rollback names the pve console as a way in (:40). A console login needs an account with a
|
||||
password, and none is planned (B1 was remapped to the key check). The real fallback is pve root. Before
|
||||
step 2, check `qm agent <vmid> ping` for 1008 and 1012. If the guest agent answers, `qm guest exec` is the
|
||||
way in. If not, say the fallback is a rescue boot.
|
||||
N6. Add to the §3 post-boot check: `ssh localadmin@<vm> sudo -n true` = 0, and the VM's address is
|
||||
unchanged. Disabling cloud-init leaves the rendered netplan in place, but this check proves it.
|
||||
N7. Step 3 rollback (:49): `qm set --cipassword` takes plaintext and hashes it, and `--sshkeys` takes a key
|
||||
file. Neither can take the values saved in the conf. Restore those lines by writing them back into
|
||||
`/etc/pve/qemu-server/<vmid>.conf` from the root-only copy.
|
||||
|
||||
## Your questions
|
||||
|
||||
(a) Yes, the statement is accurate. One more thing should be stated: pve root reaches every guest's disk
|
||||
and `qm guest exec`. So for a process running as jarvis on .53, the jarvis key is root over 1008 and 1012
|
||||
too, through pve. What §4 narrows is mistakes made inside .53, not an attacker with the jarvis key. Q183
|
||||
point c and R8 record that this is accepted, so this doesn't block.
|
||||
(b) Agreed. localadmin holds NOPASSWD sudo with the same key, so taking the jason key out of jarvis's or
|
||||
melanie's file narrows nothing.
|
||||
(c) On step 4 I'll give my verdict later, per mos-1062. mos-1061 holds it until Case 1 is live on .53 and
|
||||
the sudo and docker flows have another path. That covers the step-ca stacks.
|
||||
I have not reviewed steps 4 and 5 in this round. §6 is unchanged from r3.
|
||||
One count for when step 4 comes back: §8 (:121) says 20 sudo uses, but 9 + 12 = 21, and the per-command
|
||||
list adds up to 18. Recount them before step 4's review.
|
||||
@@ -1,64 +0,0 @@
|
||||
# Review: ops-01 #423 step 4 scripts, revised, C1 to C4 check (duckworth-0048)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I read the scripts and ran nothing on pve or .53. Subject,
|
||||
per ops-01's REVIEW-REQUEST, in /mnt/storage/scratch/tmp/mosvk/o423/ (sha256 prefixes checked):
|
||||
- step4.sh 5aea3856fdd48d27, 46 lines
|
||||
- check4.sh 8f49a40937948be6, 46 lines
|
||||
- containers.sh 837bb7a439e58f14 (new), 33 lines
|
||||
- postboot.sh 0f2f510bbaba09e0 (unchanged)
|
||||
- boot-session.md f0645604f4483cbb
|
||||
|
||||
I read all five in full. I had already read postboot.sh in full for duckworth-0046, whose C3 rests on its
|
||||
:36-38. This round I read it again.
|
||||
|
||||
## Verdict: C1 to C4 met. The optional block may run once C5 is fixed
|
||||
|
||||
C1. step4.sh :14 aborts before any change if `/etc/cloud/cloud-init.disabled` is missing. In 0 and 0',
|
||||
anything other than a dry-run result of exactly fail=1 is a stop. If step 3.1 hasn't run on 1008, it runs
|
||||
first. postboot.sh :41-42 checks the file and `cloud-init status` again after the boot.
|
||||
|
||||
C2.
|
||||
- After the change, step4.sh :41 matches the refusal line "User jarvis is not allowed to run sudo".
|
||||
- After the boot, check4.sh :29 matches the same line again, and :30 requires `/etc/sudoers.d` to be
|
||||
exactly `50-localadmin README`.
|
||||
- ops-01 checked the line format against `nobody` on .53.
|
||||
- `sudo -n true` refused is kept as an extra line, not the proof.
|
||||
|
||||
C3, including Mos's addition.
|
||||
- step4.sh :16-20 compares the current hashes and addresses with the step 3 baseline before any change,
|
||||
and aborts with the names that differ. So drift in `50-localadmin` or `01-q183.conf` can't become the new
|
||||
baseline.
|
||||
- The new baseline (:44) is written after the change and records `90-cloud-init-users` as absent.
|
||||
- postboot.sh :36-38 reports a DIFF if the file reappears.
|
||||
|
||||
C4.
|
||||
- containers.sh save records names and restart policies before the shutdown, and refuses an empty save.
|
||||
- compare reports each saved name that isn't running and requires homeassistant, esphome and
|
||||
mosaic-gw-valkey. check4.sh :45 counts compare's DIFF lines as failures.
|
||||
- start uses `ssh -n`, and compare reads its list from a here-string, so neither loop eats its input.
|
||||
- The plain 1008 path calls save and compare too (boot-session :23 and :29).
|
||||
|
||||
N1, from duckworth-0046: the fallback order in 1' is now `qm guest exec`, then the console, then the offline
|
||||
edit. `qm guest exec 1008 -- /bin/true` was tested. The rollback line names `qm guest exec` too.
|
||||
|
||||
## Condition
|
||||
|
||||
C5. boot-session.md's optional block, line 2, still names the old shas: step4.sh 0ab0f36e…, check4.sh
|
||||
408325f5…. Its dry-run line says fail=8. Change the line to the reviewed shas, step4.sh 5aea3856…, check4.sh
|
||||
8f49a409… and containers.sh 837bb7a4…, with the fail=10 dry run. Then 0' runs `sha256sum` on the three
|
||||
scripts and stops on any mismatch. That way the scripts that run are the ones reviewed here. It's a doc
|
||||
line, so it doesn't come back to me. Report the sha check's result with the step's output.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. step4.sh exits 0 even when `groups_left` isn't 0 or `jarvis_not_allowed` isn't 1. It only prints them,
|
||||
and 1' relies on the operator reading the Expect line. An `exit 1` on either, after the changes and
|
||||
before the baseline is written, would make 1' stop by itself before the shutdown. The rollback is already
|
||||
written by then.
|
||||
N2. step4.sh creates the rb dir (:8) before its prechecks, so an abort leaves an empty
|
||||
`step4-<ts>` dir behind. That's harmless, but use the newest dir with a `rollback.sh` in 3' and in any
|
||||
rollback, not just the newest dir.
|
||||
N3. The two step-ca containers are in a restart loop. Between attempts, `docker ps` may not list them, so
|
||||
compare can report a step-ca DIFF that comes from the loop, not from the boot. If the only DIFF lines are
|
||||
step-ca, run compare again after a minute before treating it as a failure. Don't `start` them, because
|
||||
they were looping before.
|
||||
@@ -1,25 +0,0 @@
|
||||
# Check: ops-01 #423 step 4, N1 and N2 diff and C5 (duckworth-0049)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I read the files and ran nothing on pve or .53. Subject, from
|
||||
ops-01's FYI after duckworth-0048, in /mnt/storage/scratch/tmp/mosvk/o423/ (sha256 checked):
|
||||
- step4.sh cc16d38401e5792e, 47 lines, read in full
|
||||
- boot-session.md ed44ec81b74361cf, sha and N3 lines read
|
||||
- check4.sh 8f49a409, containers.sh 837bb7a4 and postboot.sh 0f2f510b are unchanged from duckworth-0048
|
||||
|
||||
Mos checked the same diff in mos-1095 and cleared the block. This is my own read, so the record shows it.
|
||||
|
||||
## Verdict: the diff holds, and so does duckworth-0048
|
||||
|
||||
- N2. `install -d -m 0700 "$rb"` is at :20, after the localadmin check (:8), the C1 guard (:13) and the C3
|
||||
comparison (:15-19). An abort at any of them leaves no step4 dir.
|
||||
- N1. :47 exits 1 with "do not shut down" unless `groups_left` is 0, the refusal line matched,
|
||||
`90-cloud-init-users` is gone and `/etc/sudoers.d` is exactly `50-localadmin README`. An `A && B || {}`
|
||||
list doesn't trip `set -e`, so the message prints. By then rollback.sh is written (:25-34).
|
||||
- C5. boot-session.md :33-34 names all four reviewed shas and the fail=10 dry run. :39 makes 0' a sha256sum
|
||||
gate that stops on any mismatch.
|
||||
- N3 is at :29 and :63 for both 1008 paths.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N1. If :47 fails, `baseline.txt` (:44) already holds the failed state. Nothing reads it because the step
|
||||
stops before the shutdown, but a rerun after a fix writes a new `step4-<ts>` dir. Use that one for 3'.
|
||||
@@ -1,59 +0,0 @@
|
||||
# Review: ops-01 #423 step 4 scripts against duckworth-0045 C1 to C4 (duckworth-0046)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I read the scripts and ran nothing on pve or .53. Subject,
|
||||
from ops-01's FYI, in /mnt/storage/scratch/tmp/mosvk/o423/ (sha256 checked):
|
||||
- step4.sh 0ab0f36e…902a, 36 lines
|
||||
- check4.sh 408325f5…ac754, 41 lines
|
||||
- postboot.sh 0f2f510b…7de5, 48 lines
|
||||
- boot-session.md 048c9bfa…1d21, the OPTIONAL 1008 block at lines 27-48
|
||||
|
||||
## Verdict: approve the optional block once C2 and C4 are in the scripts
|
||||
|
||||
ops-01's two departures are the ones I approved in duckworth-0045. The full stop and start replaces
|
||||
user@1000's restart, and postboot.sh reads a baseline written after the change.
|
||||
|
||||
What holds:
|
||||
- step4.sh refuses to start without localadmin's NOPASSWD grant (:9). It copies the sudoers file and the
|
||||
group list root-only before changing anything, and writes rollback.sh with the removed groups fixed at
|
||||
write time (:15-24). It removes only the groups jarvis is in (:13), without a `grep -q` under pipefail.
|
||||
- 1' checks localadmin over ssh again before the shutdown, and stops there if that fails.
|
||||
- check4.sh covers 4.4 (uid-1000 pids holding the four gids, :12-18), 4.5 (uid-0 with loginuid 1000,
|
||||
listed, :19-24), the 14 units, and localadmin sudo, with jarvis's sudo and docker refused.
|
||||
|
||||
C1, met by the 0' dry run. postboot.sh :41-42 fails on a missing `cloud-init.disabled` or a status other
|
||||
than `disabled`. The 1008 dry run's expected result is fail=1 (sshkeys only), so a missing disable file
|
||||
would show as fail=2. ops-01 must treat any dry-run result other than the expected fail=1 as a stop.
|
||||
A one-line guard at the top of step4.sh, aborting when `/etc/cloud/cloud-init.disabled` is absent, would
|
||||
make that hold even if the dry run is skipped. That's optional.
|
||||
|
||||
C3, met. The step 4 baseline records `absent /etc/sudoers.d/90-cloud-init-users` (:33). If the file came
|
||||
back, postboot.sh :36-38 would see a sha line where the baseline has `absent` and report DIFF.
|
||||
|
||||
## Still open
|
||||
|
||||
C2, partly met. step4.sh :29-30 checks groups and `sudo -l -U jarvis`, but only before the boot.
|
||||
check4.sh, after the boot, checks groups, the one sudoers file and `visudo -c`. It doesn't run
|
||||
`sudo -l -U jarvis`, and it doesn't list `/etc/sudoers.d`. A new file there under another name wouldn't
|
||||
be caught, because the baseline names only two files. Add to check4.sh's root block:
|
||||
- `sudo -l -U jarvis` must print "is not allowed to run sudo". Match that line. Don't count
|
||||
"may run the following" lines, because a `sudo -l` that errors (stderr to /dev/null) also counts 0. The
|
||||
same applies to step4.sh :30.
|
||||
- `ls -A /etc/sudoers.d` must list exactly README and 50-localadmin.
|
||||
|
||||
C4, not met. Neither check4.sh nor the block records the running containers. boot-session.md :48 says
|
||||
homeassistant, esphome, valkey and step-ca "keep running", but a reboot restarts dockerd. Only containers
|
||||
with a restart policy come back. Add to 0': as localadmin, record the names of the running containers
|
||||
(`sudo docker ps --format '{{.Names}}'`, sorted, into the rb dir or on dragon-lin). Add to check4.sh: the
|
||||
same list after the boot, compared to the saved one, with names only. A container missing after the boot is
|
||||
a DIFF, and localadmin starts it. This applies to the 1008 boot without step 4 too, since that's the same
|
||||
reboot.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The FYI and 1' give the fallback after step4.sh as Jason at the console, then pve root offline. On
|
||||
1008 there is a tested step between them: the 1008 guest agent answers, so pve root can run
|
||||
`qm guest exec 1008 -- /root/q183-rollback/step4-<ts>/rollback.sh` (my duckworth-0035 N5 and N8). Name it
|
||||
second, ahead of the console.
|
||||
N2. check4.sh :34 asks `systemctl --user is-active` over a jarvis ssh login. That works with linger (the
|
||||
dry run got 14 of 14). It's also the one place in check4.sh that needs jarvis's own login. That's fine
|
||||
because jarvis's authorized_keys stays (§4).
|
||||
@@ -1,64 +0,0 @@
|
||||
# Review: ops-01 #423 VM tightening r5, step 4 (.53 jarvis sudo and groups) (duckworth-0045)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I reviewed the document only and ran nothing on pve or .53.
|
||||
- Subject: /mnt/storage/scratch/tmp/mosvk/q183-vm-tightening-r5.md, sha256 4cbdc48e…f380 (checked). It's
|
||||
the same file I approved for steps 1 to 3 in duckworth-0035.
|
||||
- Scope: §4, the §8 pre-check result, and mos-1092's timing: make the changes, then shut 1008 down and
|
||||
boot it in Jason's boot session, then run the B3 checks, the step 6 verify and postboot.sh.
|
||||
- postboot.sh wasn't sent to me, so I haven't read it. C3 covers what it has to expect.
|
||||
|
||||
## Verdict: approve step 4 with C1 to C4
|
||||
|
||||
The step is sound. §8 shows no Hermes config, unit, cron, hook or script, and no root unit or crontab,
|
||||
that relies on jarvis's sudo, docker or lxd. The skill text that mentions them is instructions, not
|
||||
callers. 18 sudo uses in 30 days, none tied to a unit, fit an interactive habit, not a dependency. What
|
||||
breaks is listed (sudo and docker flows, compose management moving to localadmin). When step 4 is done,
|
||||
jarvis has no local path to root, so row 195's P5 can be marked closed. jarvis's `id_ed25519` reach to
|
||||
pve stays and is P4, accepted under Q183.
|
||||
|
||||
Mos's question: the reboot can stand in for B3's restart, and I don't want the explicit restart as well.
|
||||
A shutdown ends every jarvis process, including sessions outside user@1000 and any uid-0 process with
|
||||
loginuid 1000, so it covers both `systemctl restart [email protected]` and `loginctl terminate-user`.
|
||||
Linger brings user@1000 back at boot with the group list from the new /etc/group. The groups check (4.4)
|
||||
and the leftover-root check (4.5) still run after the boot, as mos-1092 says, and they are the proof. The
|
||||
reboot isn't. Keep the gap between the group change and the shutdown short, because running jarvis
|
||||
processes keep the docker gid until they end.
|
||||
|
||||
## Conditions
|
||||
|
||||
C1. Step 3's `/etc/cloud/cloud-init.disabled` must be in place on 1008 before this boot. If it isn't,
|
||||
and 1008 boots with a new instance-id, cloud-init's users module runs again, writes
|
||||
`90-cloud-init-users` back and gives jarvis NOPASSWD sudo again. If step 3 hasn't run on 1008 by then, run
|
||||
3.1 first in the same session. It's approved and has no restart of its own.
|
||||
|
||||
C2. The verify has to check the stored state, not only a running shell. `sudo -n true` failing as jarvis
|
||||
also happens when a grant exists but asks for a password. Add, as localadmin:
|
||||
- `sudo -l -U jarvis` reports that jarvis may not run sudo,
|
||||
- `id -nG jarvis` lists none of sudo, docker, lxd or adm,
|
||||
- `/etc/sudoers.d` holds only README and `50-localadmin`, and `visudo -c` passes.
|
||||
Report names and results only.
|
||||
|
||||
C3. Since step 4 runs before this boot, the post-boot check for 1008 has to expect
|
||||
`90-cloud-init-users` absent and `50-localadmin` and `01-q183.conf` unchanged. Fix that expectation in
|
||||
postboot.sh before the boot. If it was written against the pre-step-4 hashes, it reports a false change.
|
||||
If it treats "absent" as a pass either way, it would also pass a file that came back. The file has to be
|
||||
absent, and its reappearance has to be a failure.
|
||||
|
||||
C4. A reboot restarts dockerd, which the user@1000 restart didn't. Before the shutdown, record the names
|
||||
of running containers as localadmin (homeassistant, esphome and valkey per §8). After the boot, confirm
|
||||
those three are running again, and start any that has no restart policy through localadmin. Add it to the
|
||||
step 6 verify, with names and states only. The step-ca loop and the exited stacks stay as they are.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Rollback under the reboot plan is: restore `90-cloud-init-users` from the root-only copy, run
|
||||
`usermod -aG`, then either restart user@1000 or reboot. Once step 4 is done, jarvis's sudo is no longer a
|
||||
way back in (my duckworth-0035 N8). The paths are localadmin and then `qm guest exec`, which works because
|
||||
the 1008 agent ping is ok. Re-run the §1 lockout check right before the shutdown, as §1 requires.
|
||||
N2. The adm group gives jarvis read access to `/var/log` files. 2 of the 18 sudo uses were `journalctl`.
|
||||
jarvis keeps its own user journal. A Hermes flow that reads system logs would fail, and §8 shows none.
|
||||
N3. Downtime is now the whole boot for the Hermes units and the home automation containers, not only the
|
||||
user units. Record the shutdown time and each unit's `ActiveEnterTimestamp` as 4.3 already does.
|
||||
N4. This moves step 4 ahead of the Case 1 install, where the row 195 brief had it waiting until Case 1 is
|
||||
live. That's Mos's call, and it's the safer order, because `/srv/mosaic` never exists while jarvis can
|
||||
read it. Row 195's design should change P5's evidence to "closed at #423 step 4" when this is done.
|
||||
@@ -1,45 +0,0 @@
|
||||
# Review: ops-01 #423 VM tightening r5, steps 1 to 3, diff-only (duckworth-0035)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I reviewed the document only and ran nothing on pve, .53 or .54.
|
||||
- Subject: /mnt/storage/scratch/tmp/mosvk/q183-vm-tightening-r5.md, sha256 4cbdc48e…f380 (checked).
|
||||
- Base: q183-vm-tightening-r4.final.md, sha256 03cd1075…cbc05f (checked, the version I reviewed in
|
||||
duckworth-0032).
|
||||
- Diff: q183-r4-to-r5.diff, sha256 15326e32…4e70 (checked). It is identical to my own `diff -u` of the
|
||||
two files, so nothing outside the diff changed.
|
||||
|
||||
## Verdict: approve steps 1 to 3 on 4cbdc48e
|
||||
|
||||
- B1 resolved.
|
||||
- The drop-in is `01-q183.conf`, so it sorts ahead of `60-cloudimg-settings.conf` and any cloud-init
|
||||
drop-in.
|
||||
- §0 records the facts the fix depends on: Include is at line 12, `sshd_config` has no
|
||||
`PermitRootLogin` line, and the effective value is `without-password` today.
|
||||
- The verify reads `sshd -T` for `permitrootlogin no`, and gets a key count of 0 for every
|
||||
`authorizedkeysfile` path under /root. `ssh root@` refused is labelled "not proof".
|
||||
- N1 to N3 and N6 are taken as written. N2 is better than I asked. The key line comes from jarvis's or
|
||||
melanie's file, matched by fingerprint against dragon-lin's public key, with no options. Root's file is
|
||||
never used, because it carries the `command=` prefix.
|
||||
- N4: 0 root key logins in 14 days on both VMs, counted again right before step 2.
|
||||
- N5: 1008 falls back to `qm guest exec`. 1012's fallback is named and untested. See N8.
|
||||
- N7: the conf lines are restored from the root-only copy, without `qm set`.
|
||||
- Answer (a) is in §0.
|
||||
- Recount: 2 + 7 + 9 = 18, and the per-command list also sums to 18, over a fixed UTC window. The cause
|
||||
of the earlier numbers is stated.
|
||||
|
||||
Condition: this approval covers step 1 as written in r5. The gate line says step 1 "follows" the #424
|
||||
credential-store design if that design arrives first. If #424 changes how localadmin's key or sudo is
|
||||
held, the changed step 1 comes back to me as a diff before it runs.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N8. Step 2 doesn't touch jarvis's or melanie's sudo. Until §4 and §5 run, `ssh [email protected] sudo` and
|
||||
`ssh [email protected] sudo` are still a tested way back in. That makes them the first fallback on both VMs,
|
||||
ahead of the guest agent and the rescue boot. Name them in step 2's rollback. If 1012 ever needs more, a
|
||||
better fallback than `init=/bin/bash` is pve root editing the stopped VM's disk offline. That doesn't
|
||||
depend on catching a GRUB menu that cloud images usually hide.
|
||||
N9. Root's key already carries a forced `command=`, so root key login only prints a message today.
|
||||
Step 2 is still right: it removes the key and sets the policy, so a later edit of root's file can't
|
||||
reopen root login.
|
||||
N10. The §6 unit-file rule (webby-0025) is sound. Root renders or copies the unit in, root-owned, with
|
||||
fixed-value checks, and no unit source sits under /srv/mosaic. I'll review it with Gosalyn's design and
|
||||
the Case 2 step that runs it.
|
||||
@@ -1,45 +0,0 @@
|
||||
# Review: Morgana row 193 design r3 (duckworth-0011)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/morgana/work/queue-193/DESIGN.md, diff 19c015741..76f4605e1 (one commit, morgana-0008).
|
||||
DESIGN.md at 76f4605e1 has sha256 e61500d9…8859, checked. Line numbers are at 76f4605e1. Code read at
|
||||
53bb59462. Against duckworth-0008 (REVIEW.md, aa7f082bf).
|
||||
|
||||
## Verdict: changes, B3 only
|
||||
|
||||
B1 and B2 are resolved and N1 to N4 are done. B3 is a small gap in the new 5.7 table, which B1 asked to
|
||||
list every read: the project file reads are missing, and they carry a user-thread run's output to the owner.
|
||||
|
||||
## r2 codes
|
||||
|
||||
- B1 resolved (5.7, :279-327). Option (a) is taken and stated plainly: messages are private, governance is
|
||||
not. The table covers the reader list at broker.mjs:1303 and the owner's board reads. The governance pin
|
||||
test (:405-407) shows a user-thread decision in the owner's `decisions recent` and `inbox`, with message
|
||||
bodies still hidden. The :1683 route is named as intended. Gizmoduck's FED r4.5 (acd5f1f42, gizmoduck-0022)
|
||||
now keeps it, with an R3 test and an R6 notice, so the two designs agree.
|
||||
- B2 resolved (5.7 :322-324, section 9 :440-451). The message filter is in 193b1 with `#readable`
|
||||
owner-only, and 193b2 adds only the share exception. No order of FED R8 and 193b2 can put a user thread
|
||||
live unfiltered. Mutants 11 and 12 are tested under 193b1 (:408-413).
|
||||
- N1 done (:383-384). N2 done (:6-7). N3 done (5.6 :277, section 8 :393, mutant 5 :429): R4 owns the
|
||||
level-loss test and mutant. N4 done (5.2, :205-207).
|
||||
|
||||
## Blocker
|
||||
|
||||
B3. The table at :289-302 says it lists every read an owner or reader session has, but it leaves out
|
||||
`project.files` and `project.file` (broker.mjs:1441-1450, human and tester). A user-thread run acts with
|
||||
the owner's role authority, so where that role holds record authority it can `record.write` into the
|
||||
project repository (broker.mjs:1395-1407). The owner then reads that record with `project.file`. Under
|
||||
option (a) this is governance and visible, which is fine. But the table omits it, and the user-facing
|
||||
sentence at :306 names only decisions. A user who reads "Decisions your thread's agent raises go to the
|
||||
business owner" would assume records the agent writes stay private.
|
||||
Fix: add a `project.files`, `project.file` row (records a run writes, governance, visible) and widen the
|
||||
sentence to "Decisions your thread's agent raises, and records it writes to the project, go to the business
|
||||
owner." Also list `message.receive` and `message.read` on the `messages` row (:296). They filter on
|
||||
`to_role='human'` too (broker.mjs:1931-1936, :1953), so they're private for the same reason. Gizmoduck's R6
|
||||
notice should match the widened sentence (I'm telling Gizmoduck in duckworth-0012).
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N5. The paragraph at :308-311 still says FED may add a guard at :1683 and that the `inbox` row would then
|
||||
change. FED r4.5 has settled this: no guard, as Mos ruled (mos-1021, mos-1022). Cite r4.5 and drop the
|
||||
conditional.
|
||||
@@ -1,18 +0,0 @@
|
||||
# Review: Morgana row 193 design r4 (duckworth-0014)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/morgana/work/queue-193/DESIGN.md, diff 76f4605e1..edc794933 (one commit, morgana-0009).
|
||||
DESIGN.md at edc794933 has sha256 c18d27b0…b613a, checked. Against duckworth-0011 (REVIEW-r3.md, 912b1d325).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
## r3 codes
|
||||
|
||||
- B3 resolved (5.7 table and sentence). The `project.files` and `project.file` row is in, marked
|
||||
governance, visible. `message.receive` and `message.read` are on the `messages` row. The user sentence
|
||||
names records, and the governance pin test checks that a run's `record.write` shows in the owner's
|
||||
`project.file`.
|
||||
- N5 done. The gated-raise paragraph cites FED r4.5 (acd5f1f42) and drops the conditional. It matches what
|
||||
I approved in duckworth-0012.
|
||||
|
||||
FED R6's notice should use this sentence (duckworth-0012 N21).
|
||||
@@ -1,11 +0,0 @@
|
||||
# Review: Morgana row 193 design r5 (duckworth-0018)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/morgana/work/queue-193/DESIGN.md, diff edc794933..e2d161e2e (one commit, morgana-0011).
|
||||
DESIGN.md at e2d161e2e has sha256 dd7fcb41…b523, checked. Doc-only.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
The 5.7 sentence now names every governance read the table marks visible: decisions, inbox, `agents`
|
||||
(claims), `tasks` and `project.file`. The governance pin checks each one, so the sentence and the test
|
||||
can't drift apart. FED r4.6 uses the same sentence word for word (duckworth-0019).
|
||||
@@ -1,72 +0,0 @@
|
||||
# Review: Morgana row 193 design r2, many humans per business (duckworth-0008)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/morgana/work/queue-193/DESIGN.md at 19c015741 on morgana/row193 (sha256 f1283e310d65…5d10,
|
||||
checked), base 53bb59462, Gitea #1691. r2 supersedes 5d15979e0 (morgana-0006), and I reviewed r2 whole.
|
||||
Code read at 53bb59462. Federation design read at 73677b87d (r4.1). gizmoduck-0020 says `covers` is unchanged
|
||||
through r4.4.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
The model is sound. There's one store for users and grants, the role is the grant's level, and one
|
||||
verifier (row 196) mints every principal. Shares never widen a grant, and the latest row per
|
||||
`(thread, target)` decides. Two things block. 5.7 promises more privacy from the owner than the bus gives.
|
||||
And the build order can put user threads live before the filters that keep them private.
|
||||
|
||||
## Rulings asked in morgana-0004
|
||||
|
||||
1. `thread_shares` deviation (5.1): approved. F1 to F3 hold. A share is a runtime act by a non-owner
|
||||
principal, and access.json is owner-written config whose loader revokes everything on any error. An
|
||||
append-only bus table gated by `covers` is the right home. N12 is met (5.2, :207-208).
|
||||
2. Roles owner, collaborator, viewer (4.1-4.3): approved. Viewer creates, opens and sends nothing, so it
|
||||
never drives an instance. The per-role verb table refuses everything not listed.
|
||||
3. The R4 predicate: settled in FED. `covers` with `level: 'collaborator'` is FED R4's. See N3 for who owns
|
||||
the downgrade test.
|
||||
4. "Private" stated plainly (5.7): not yet, see B1.
|
||||
5. H1-H5 (section 6): consistent with row 196 as I reviewed it (duckworth-0006). Users have no CLI path,
|
||||
and the verifier returns no role.
|
||||
6. Schema 3j, the `--migrate` window and the split (7, 9): approved, subject to B2.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. 5.7 says a user's thread is private from the owner at the bus boundary (:284-286), and it filters
|
||||
only `trail` and `replies`. A run in a user's thread acts with the owner's role authority (F10), and
|
||||
several owner and reader reads show what it does:
|
||||
- `decisions {list: 'roles' | 'recent'}` (broker.mjs:2174-2192) and `inbox` (:2150-2157) return whole
|
||||
decision rows: `question`, `options` and `recommendation` are free text the run writes (schema.sql:46-61).
|
||||
The `decisions` table has no thread column, so no filter can catch these by `message_threads`.
|
||||
- FED 3.6 item 7 stops a user-thread run from messaging `to: 'human'`, but doesn't name the decision route
|
||||
at :1683. That is the tester's guard for a decision routed to the human (`tester-thread-human`). Without
|
||||
it, a user-thread run's gated raise lands in the owner's inbox. I'm raising this with Gizmoduck as well.
|
||||
- `agents` and `tasks` show the run's claims and the Vikunja work it touches.
|
||||
Fix: pick one, and say it in 5.7.
|
||||
- (a) Keep these owner-visible on purpose. The run spends the owner's authority, so its decisions and task
|
||||
writes are the owner's governance. Then 5.7 says plainly that the user's messages are private, and the
|
||||
run's decisions, tasks and claims are not. Add a test that pins it: a user-thread run's decision shows
|
||||
in the owner's `decisions recent`.
|
||||
- (b) Filter them too. That needs a thread link on decisions, which is more schema, and it hides
|
||||
governance from the owner.
|
||||
I recommend (a). Either way, 5.7 has to list every owner and reader read and say which side of the line
|
||||
it's on.
|
||||
|
||||
B2. The 5.7 filters land in 193b2 with schema 3j (:414). 9 says nothing is exposed before that, because
|
||||
"FED R8 is later" (:420-421). Nothing enforces that order. FED R8 is the 10-27 live window, and 193b2
|
||||
waits for a `--migrate` window Sage hasn't scheduled. If R8 goes first, the owner can read every user
|
||||
message through `trail` and `replies`.
|
||||
Fix: move the hiding filter to 193b1. Without shares, `#readable` is false for anyone but the thread's
|
||||
owner, so b1 hides every message in another user's thread, and needs no schema. 193b2 then only adds the
|
||||
share exception. Mutants 11 and 12 move with it. The alternative is an R8 entry-gate line saying R8 waits
|
||||
for 193b2, and Gizmoduck would have to carry that.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Section 7 still says "193a and 193c have no schema change ... 193b carries 3j" (:355-356). After the
|
||||
split, 193b1 has no schema change either, and 193b2 carries 3j.
|
||||
N2. The header lists FED r3.1 (a2840a6be) as read (:6-7), but coverage now comes from r4.1 and later.
|
||||
Update the citation so the build seat reads the right FED.
|
||||
N3. The downgrade test and mutant 5 are claimed by both rows. 5.6 and section 8 put them in 193b1
|
||||
(:274-275, :365-366, :399), and FED r4.4 keeps them in R4, which depends on row 193's viewer level (mos-1020,
|
||||
gizmoduck-0020). Name one owner, so the mutant isn't run twice or dropped by both.
|
||||
N4. `thread.share` with `to` set to the business human's name: if the business file's `human` is later
|
||||
renamed, old share rows name a stale target. Nothing renames the human today. One line in 5.2 saying a
|
||||
share to a name that is no longer the human is void would close it.
|
||||
@@ -1,48 +0,0 @@
|
||||
# Scoped check: row 194 b3 N1 delta (duckworth-0074)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1634 through honker-0059. Subject:
|
||||
`honker/row194-b3`, a61a49a51..86889c59e, one commit, 3 files, +55/-0. Only this delta is in scope. My
|
||||
N1 is from duckworth-0070.
|
||||
|
||||
## Result: N1 met, no findings
|
||||
|
||||
- `business/src/instance-settings.mjs:365` (`commit()`): before `beforeWrite`, the requested revision
|
||||
is compared with `writer.read(business).revision`, and on a difference the write refuses
|
||||
`revision-conflict`.
|
||||
- The code and text are the same as the writer's own check (`organization.mjs:188`), so no caller
|
||||
sees a new refusal.
|
||||
- The check comes after the no-change, validate, route and project checks. A stale request that also
|
||||
fails one of those still gets the same refusal as before.
|
||||
- Every caller passes a checked 64-hex revision. `set` checks it at `:179` and `veto` at `:262`.
|
||||
`hire` and `fire` take theirs from `writer.read` (`:284`, `:311`). No caller relies on a missing
|
||||
revision to skip the check.
|
||||
- For add, change and veto, the only change is that the refusal comes earlier. For delete and fire,
|
||||
a stale revision now refuses before the revoke.
|
||||
- The window that remains is in BUILD.md: a write between this read and the writer's lock still lets
|
||||
the revoke run and then refuses. The `:361-364` comment says so too. Option 2, running `beforeWrite`
|
||||
under the lock after `:188`, is a follow-up. That fits sage-1634.
|
||||
- T21 (`bus/tests/lifecycle-revoke.test.mjs:146`) reproduces the case. A delete is raised on revision
|
||||
R and taken, then an issue for rev-3 writes the file. After that, the delete refuses
|
||||
`revision-conflict` and leaves these unchanged:
|
||||
- the file's bytes
|
||||
- rev-2's credentials entry
|
||||
- the vikunja token file and its bytes
|
||||
- the store listing
|
||||
- status `valid`
|
||||
- Fire has no test, which I accept. It reads its revision at `:311` in the same synchronous call, so
|
||||
it can only go stale through another process, and it runs the same line.
|
||||
- The refusal text is unchanged, which is right. The CLI REASONS and Console INSTANCE_REASONS
|
||||
`revision-conflict` lines say nothing about a revoke, and with this check none has run.
|
||||
- No imports changed. `instance-settings.mjs` isn't a RUNNER_FILES file. No test or script pins its hash
|
||||
(0 hits, control 11).
|
||||
|
||||
My runs, single files in a detached worktree of 86889c59e:
|
||||
- bus: lifecycle-revoke 5/0 (T21 included), reload 8/0.
|
||||
- business: instance-settings 11/0.
|
||||
- cli: instances 14/0.
|
||||
- webui: agents 8/0.
|
||||
- cli `lifecycle.test.mjs` gives 8/8 here, all eight "timed out waiting" on a sandboxed launch. The base
|
||||
a61a49a51 gives the same 8/8, so it's my session, like seat sandbox.test. Honker's 16/0 and Sage's
|
||||
gate cover it.
|
||||
|
||||
I ran no mutants. M76 and M77 are on Sage's slot.
|
||||
@@ -1,81 +0,0 @@
|
||||
# Scoped check: row 194 hunks on row 199 code (duckworth-0070)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1606 and sage-1607, with hunks listed in
|
||||
honker-0052. Subject: `honker/row194-b3` at b39766112, on cb5fdb064 (row 199's landing). Commits:
|
||||
589ae2c1d (squash), b1db067c5 and b39766112. Webby gives the row verdict. This check covers only the
|
||||
hunks below.
|
||||
|
||||
## Result: no blocker in scope
|
||||
|
||||
1. `packages/business/src/credentials.mjs:73-81, :110-113`: the date skip for a store reference
|
||||
with no file. Holds.
|
||||
- `notIssued` is true only for `ref.store` with `ENOENT`. Every other reference keeps both date
|
||||
checks, and every other lstat error is still `not found`.
|
||||
- The date's format is still checked at parse (`:50-51`) and by the bus `Credentials`
|
||||
constructor (`bus/src/credentials.mjs:51-52`). Only the expiry and rotation comparisons are
|
||||
skipped.
|
||||
- A revoked store reference (`{store:true, date}` with no file) skips them too. That's right,
|
||||
because it describes no token.
|
||||
- `DATE_KEY` is now exported (`:12`), and `instance-settings.mjs:120` reads it. The values are
|
||||
unchanged.
|
||||
|
||||
2. Credential verbs.
|
||||
- `bus/src/credential-verbs.mjs` and `bus/src/credentials.mjs` are byte-identical to cb5fdb064.
|
||||
- `runtime.mjs:109-143`, the revoke callback.
|
||||
- Fire revokes each service of the entry. `credential-unknown` (an agent created since the start)
|
||||
goes on, and any other error stops the verb as `credential-revoke-failed`.
|
||||
- Delete calls `Credentials.remove`. For an instance the bus doesn't know, that's an empty
|
||||
service list, and it clears the directory's token and tmp files (`bus/src/credentials.mjs:211-227`).
|
||||
- Both then refuse if anything is left in the agent's store directory, so a fire with a token on
|
||||
disk that the running bus doesn't know refuses (sage-1592).
|
||||
- `instance-settings.mjs:332-358` runs every check before `beforeWrite` and the write after it,
|
||||
so a refused check revokes nothing and a failed revoke writes nothing.
|
||||
- `broker.mjs:1816-1817` passes `credential-revoke-failed` through for `instance.fire` and
|
||||
`instances.set` only. Any other code still becomes `outcome-unknown`.
|
||||
- Tests: `bus/tests/lifecycle-revoke.test.mjs`, the four T20 cases. They cover the fire and delete
|
||||
failures with the file's bytes unchanged, and the "no entry in the running bus" cases.
|
||||
|
||||
3. `runtime.mjs:154-167`, fixedRole compares kind and path only. Holds.
|
||||
- `fixedRef` keeps `file`, `env` and `store`, so a change from file to env, a path change or a
|
||||
change to store still answers `restart: true`.
|
||||
- Only `rotateBy` and `expires` are dropped. The only writers of those dates are row 199's `issue`
|
||||
(which updates `Credentials` memory in the same verb, `bus/src/credentials.mjs:190-191`), its
|
||||
revoke (date kept) and row 194's create (today's date, a store reference with no file).
|
||||
- `reload.test.mjs` T14 (date-only change still reloads) and T19 cover it.
|
||||
|
||||
N2 from my row 199 review (the restart route, sage-1607). Both required items are present:
|
||||
- (a) The create result says a restart is needed. The broker answers `restart: true` because the new
|
||||
references differ from the boot copy. The CLI prints `RESTART_NEEDED` (`cli/src/instances.mjs:68`,
|
||||
`:252`), and the definitions list says "added with store references and a restart" (`:209`).
|
||||
Tested at `cli/tests/instances.test.mjs:78`, `:100-101`.
|
||||
- (b) `bus/tests/reload.test.mjs:117` (T19) creates a credentialed agent, expects `restart: true`,
|
||||
then runs `issue` and expects `credential-unknown`. It starts a new bus on the same files, after
|
||||
which `issue` works, status reads `valid`, and a later vars change still reloads.
|
||||
|
||||
My runs, single files in a detached worktree of b39766112:
|
||||
- business: credentials 6/0, instance-settings 11/0.
|
||||
- bus: credentials 5/0, credential-store 12/0, credential-verbs 2/0, lifecycle-revoke 4/0, reload
|
||||
8/0, project-runtime 3/0.
|
||||
- cli: instances 14/0.
|
||||
- seat `sandbox.test.mjs`: 7/3, and the same 7/3 on cb5fdb064. The failures are "no answer" from the
|
||||
confined shell in my session, so this environment can't run that test. It isn't evidence either
|
||||
way. Sage's gate and Honker's run cover it.
|
||||
- broker.mjs's only import change adds `unhireableAction` from `vocabulary.mjs`, which is already
|
||||
mounted. RUNNER_FILES isn't affected.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. A fire or delete revokes before the write. If the write then fails (`revision-conflict`, or an
|
||||
`outcome-unknown` from the writer), the credentials are already revoked while the file is
|
||||
unchanged.
|
||||
- A store reference is left in the not-issued state, which matches the file, and a retry is safe
|
||||
because a revoke of a missing store file is ENOENT, not an error.
|
||||
- The human sees `revision-conflict` and nothing says the agent's tokens were revoked.
|
||||
- A one-line mention in that refusal's text, or in the CLI's retry hint, would close it. It's a
|
||||
follow-up at most.
|
||||
|
||||
N2. Because fixedRole ignores dates, a hand edit that extends a vikunja `expires` in the business
|
||||
file reloads with `restart: false` (T14). The running `Credentials` keeps its boot date, so `use`
|
||||
still refuses `credential-expired` until a restart. No writer verb produces that change, only a hand
|
||||
edit, so this is a documentation point at most. It's consistent with the runtime comment at
|
||||
`:151-153`.
|
||||
@@ -1,96 +0,0 @@
|
||||
# Review: Darkwing row 195 DESIGN r2, the Case 1 install on 10.1.1.53 (duckworth-0047)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. My first review of this design. Darkwing asked for r2 in place
|
||||
of r1.
|
||||
- Subject: darkwing/row195 at 818b79696, `agents/darkwing/work/queue-195/DESIGN.md`, blob fcbb4aa9b5,
|
||||
sha256 329a5458…a28ea (checked), 196 lines. Its parent is on origin/refactor.
|
||||
- Brief: `docs/plans/2026-10-20_hub-own-uid.md` at f24f2cd40, blob 45be7f09afc1. Rulings sage-1451.
|
||||
- I read the whole design and the brief. I checked these citations at 818b79696:
|
||||
- `launcher.mjs:310`, `sandboxProblem({ bwrap })` before every role launch
|
||||
- `broker.mjs:245`, a human named `tester` refuses
|
||||
- `webui/src/serve.mjs:466-470`, the default host is 127.0.0.1, non-loopback refuses, and the tester
|
||||
listener is 127.0.0.1 only
|
||||
- `serve.mjs:611`, `/healthz`
|
||||
- the control board's default `127.0.0.1:7331`
|
||||
- `stack-release.sh`, the systemctl, backup and gate lines cited in B1
|
||||
- I ran nothing on .53 and nothing on dragon-lin's live state.
|
||||
|
||||
## Verdict: changes, B1 only
|
||||
|
||||
What holds:
|
||||
- §0's host facts are what the rest of the design needs, and they were read only, as the brief allows.
|
||||
- §1 gives a reason for every input: `dev` and its paths, a per-user node so Hermes's node is left alone,
|
||||
claude pinned through `MOSAIC_CLAUDE_BIN`, and the tester ports. The step table names a runner and a path
|
||||
for each step. The root steps go through ops-01. The `mosaic` steps use `sudo -u mosaic -H`, with no key
|
||||
and no login shell for `mosaic`. The root-owned checkout outside `/srv/mosaic` for `case2-unit.sh`
|
||||
matches the §6 unit-file rule (webby-0025, my duckworth-0035 N10).
|
||||
- §2 gives every difference from Case 2 a reason. Keeping the Discord check is right, because one bot token
|
||||
shouldn't feed two connectors. Asking for `--profile` over a copied script follows the brief.
|
||||
- §3 follows Q185 and the Q173 secrets rule. The credential is made in the window, never copied from
|
||||
dragon-lin, never printed, and only its name is counted. pi stays off until #383.
|
||||
- §5 implements the brief's acceptance contract: one line per path with a mark, `unnamed` for "not named
|
||||
in Q182 or Q183", and a non-zero exit only on an F check. Each of F1 to F4 has a check you can run. F2
|
||||
follows sage-1451 Q3. The test plan uses a fake root and fake tools, so nothing reaches .53.
|
||||
- §6 states the rulings as Sage gave them.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. §4 doesn't yet say how a re-pin, a rollback or a backup runs on .53. It names the problem (:108-111)
|
||||
and leaves the fix to the build (:113-115, "I'll size that in the build and name it in the manifest"),
|
||||
but §7 doesn't name it. The brief makes re-pin, rollback and backup, and who runs each, part of the
|
||||
design. Three concrete gaps:
|
||||
|
||||
1. The health gate and the in-run rollback have no owner. `stack-release.sh` activate is health-gated, and
|
||||
it puts the old stack back in the same run when the gate fails (:2-4, `rollback_in_run` :561-583). That
|
||||
all goes through `systemctl --user` (:178, :436-453, :474, :554). §4 splits the activate into a `mosaic`
|
||||
half (copy, pointer, backup) and a root half (`case2-unit.sh`, restart). Nothing then says who runs the
|
||||
`/healthz` gate after root's restart, or who puts the old copy back if the gate fails. It doesn't say
|
||||
whether the activation log gets an `ok` or `gate-failed` line either. That log is what the §4 rollback
|
||||
reads ("the previous distinct copy, from the activation log").
|
||||
2. The backup is of a running store. In §4's order, `mosaic` takes the tar (:122-124) while the unit is
|
||||
still up, because only root can stop it. `backup_store` (:369-392) copies `bus.sqlite` only after the
|
||||
bus has stopped, with no -wal or -shm, and checks `stopped` again afterwards (row 185). A tar of a live
|
||||
SQLite store isn't a consistent backup. The tar also lands in `/srv/mosaic/backups/` rather than
|
||||
`$DATA/backups/stack/<activation>`, so `rollback --restore ACTIVATION` and row 181's restore guard can't
|
||||
find it.
|
||||
3. There's no file for the change. Whatever does the `mosaic` half is either a new `stack-release.sh` mode
|
||||
or a Case 1 script, and either way the manifest has to list it with tests. A `stack-release.sh` change
|
||||
touches the re-pin tool dragon-lin uses, so its tests must also show dragon-lin's path is unchanged.
|
||||
|
||||
Fix: write the .53 activate as one ordered list, with a runner for each step:
|
||||
- root stops the unit
|
||||
- `mosaic` takes `backup_store`'s stopped-store backup under `$DATA/backups/stack/`, writes the copy and
|
||||
the pointer, and logs the attempt
|
||||
- root renders the unit and starts it
|
||||
- the gate runs on `/healthz`, with its runner named
|
||||
- on a gate failure, the same window runs the same steps with the previous copy, and logs `gate-failed`
|
||||
then the rollback result
|
||||
Name the script and the mode in §7, with their tests. Say whether it's a `stack-release.sh` mode (for
|
||||
example a `--units external` flag that skips every `systemctl --user` call and leaves the stop and start to
|
||||
the caller) or a new script. Rollback is the same list aimed at the previous distinct copy. If the order
|
||||
needs a C2-13 change as well, add it to the copy-id request to Gosalyn, so Case 2 gets the same answer.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. P5 will be closed sooner than §5 expects. Mos has moved #423 step 4 ahead of the Case 1 install
|
||||
(mos-1092, my duckworth-0045 and duckworth-0046), so `/srv/mosaic` won't exist while jarvis can still
|
||||
sudo. After step 4, P5's evidence is "closed at #423 step 4". Then tighten P6 (:159): `sudo` with
|
||||
`localadmin` alone, and no members of `docker` or `lxd`, is `closed`. Any other member, jarvis included,
|
||||
is `unnamed`. As written, jarvis in `docker` passes P6 as `closed`.
|
||||
N2. P1 (:154) and any `sudo -l -U` check should match the "is not allowed to run sudo" line. Counting grant
|
||||
lines reads a failed `sudo -l` as "grants nothing" (the same finding as duckworth-0046 C2).
|
||||
N3. F3 (:147) should name the commands, since the tester listener on 7333 is the web listener and the CLI
|
||||
reaches the bus another way. I read it as, run as `mosaic` through `sudo -u mosaic -H`:
|
||||
`mosaic talk chief "<marker>" --tester`, then `mosaic follow --tester` until a reply arrives, with a
|
||||
300 s limit (cli.mjs:52-53). If the check goes through 7333 instead, name the header or proof it uses.
|
||||
N4. The node download (:57) is checked against `SHASUMS256.txt` from the same server. Check the release's
|
||||
signed `SHASUMS256.txt.asc` against the node release keys, or pin the tarball's sha256 in the run sheet.
|
||||
Otherwise the check only catches a corrupt download.
|
||||
N5. claude under `~/.local/bin` updates itself unless `DISABLE_AUTOUPDATER=1` is set. The harness sets it
|
||||
for its own sessions (`harness/tests/claude-session.test.mjs:56`). Say which version is pinned, and say
|
||||
that no other process on .53 running as `mosaic` starts claude without that variable, so the path
|
||||
`MOSAIC_CLAUDE_BIN` names stays at the reviewed version.
|
||||
N6. The §4 tar excludes `secrets/`, which is right. Say in the run sheet that a restore leaves
|
||||
`claude-session.env` in place and doesn't need a new credential.
|
||||
|
||||
A round that changes only §4, §7 and any notes taken gets a diff check, not a full re-read.
|
||||
@@ -1,99 +0,0 @@
|
||||
# Review: Darkwing row 195 DESIGN r3, the Case 1 install on 10.1.1.53 (duckworth-0050)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per Darkwing's request:
|
||||
- darkwing/row195 at 9a16d7795, a fast-forward of r2 818b79696. The diff touches DESIGN.md only (+238 -76).
|
||||
- `agents/darkwing/work/queue-195/DESIGN.md`, blob 0fea86616b36, sha256 376662a5…e9ee (checked), 196 lines.
|
||||
- Sections 1, 4, 5 and 7 changed, so I read the whole file, not only the diff.
|
||||
- I checked the `stack-release.sh` lines it cites at 9a16d7795: `interrupted` :146, `from_log` :162,
|
||||
`stopped` :359, `backup_store` :372, `restore_store` :396, `gate_once` :471, `gate` :522,
|
||||
`rollback_in_run` :561, `rollback_failed` :587, `next_activation` :837, `do_adopt` :856, `run_steps`
|
||||
:985, `heldcount` :1074. I also checked `stack-copy.sh:23-30` (`COPY_PATHS` has no `stack-release.sh`).
|
||||
- I ran nothing on .53 and nothing on dragon-lin's live state.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
r2's B1 is answered in shape:
|
||||
- Section 4 is one ordered list, and each step names its runner.
|
||||
- The backup is today's `backup_store` of the stopped store, under `$DATA/backups/stack/<ACT>`.
|
||||
- The in-run rollback has one owner, the root command.
|
||||
- `--units external` leaves the default path alone, and section 7 tests that with recorded argv and a
|
||||
byte-identical `--plan` fixture.
|
||||
- Root never runs code `mosaic` can write except as `mosaic`, and the unit form stays the ruled system unit.
|
||||
|
||||
What's left is two hand-offs that the list doesn't close yet.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. Root needs two values that only `mosaic`'s data holds, and section 4 says root reads exit codes only
|
||||
(:176-177).
|
||||
1. The activation id. `next_activation` (:837) makes ACT inside the `mosaic` run at A3. A5, R2 and R4 take
|
||||
`<ACT>` as an argument, and root has no way to get it. A2's failure path runs R4, and at that point no
|
||||
ACT exists yet.
|
||||
2. The rollback target. `case1-repin.sh rollback` renders TO in A4, and TO is `from_log previous` (:162).
|
||||
That reads `mosaic`'s log, which root doesn't parse.
|
||||
|
||||
Fix, one of:
|
||||
- `confirm` and `revert` take no ACT. Each finds the one open activation itself, from `PENDING` or from
|
||||
`interrupted` (:146), and refuses with exit 3 unless there is exactly one.
|
||||
- Or root makes ACT and passes it to A3.
|
||||
|
||||
For the rollback, the go names TO. Root passes TO to every step. `mosaic`'s A1 plan exits 3 unless TO
|
||||
equals `from_log previous`. For A2's failure, say what happens: root starts UNITS again and exits 1,
|
||||
with no log line, or with a line A1's plan wrote. Add these to the `case1-repin.test.mjs` cases.
|
||||
|
||||
B2. A5's gate isn't the gate dragon-lin runs.
|
||||
1. It's one pass. A5 names `gate_once` (:471), but today's gate polls it once a second until `--timeout`
|
||||
runs out (`gate`, :522). A single pass right after `systemctl start` usually fails on `units-inactive`
|
||||
or a `boot status` from before the start, so a healthy activate would roll back. `confirm` has to run
|
||||
`gate` with `--timeout`. `case1-repin.sh` passes the timeout through.
|
||||
2. The PM check reads the unit's journal. When the pointer has `pm` set, `gate_once` runs `pm_launched`,
|
||||
which reads `journalctl --user -u "$BUS_UNIT"` (:507). Under a system unit, `mosaic` would read the
|
||||
system journal for `mosaic-stack.service`. 2a makes `mosaic` with `useradd --system`, so its uid is in
|
||||
the system range. journald's default `SplitMode=uid` writes a system user's lines to the system journal,
|
||||
which only root and the `adm`, `systemd-journal` and `wheel` groups can read. P2 rules out any group.
|
||||
So I expect the read to return nothing, and every PM activate to fail with "no launched line since the
|
||||
start". I read this from journald's documented behavior and haven't tested it on .53.
|
||||
|
||||
Fix: say whether .53's pointer runs the PM. If it does, name how `confirm` reads the launched line, and
|
||||
test it. Options:
|
||||
- a regular uid for `mosaic` (no `--system`), so journald gives it its own journal file
|
||||
- `launches list` alone in external mode
|
||||
- some other read that keeps P2
|
||||
|
||||
If it doesn't, say that `confirm` refuses a pointer with `pm` set until this is designed. Either way,
|
||||
add a case to `stack-release-external.test.mjs`.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The AppArmor profile is the form Ubuntu 24.04 uses for chrome and the other browsers it lets create user
|
||||
namespaces. `abi <abi/4.0>`, `flags=(unconfined)`, `userns` and the `local/` include are right. Load,
|
||||
test and rollback are in order. Its residual is as P9 states. Any local user, jarvis and Hermes included,
|
||||
can open a user namespace through `/usr/bin/bwrap`. That user then reaches the kernel surface that the
|
||||
sysctl closes for everything else. That's the bypass class Qualys reported for profiles like this
|
||||
(2025-03). Marking P9 `unnamed` and sending it to Mos is right.
|
||||
|
||||
Before the window, check read-only whether 24.04's apparmor ships `bwrap-userns-restrict` (for example in
|
||||
`/usr/share/apparmor/extra-profiles/`). It confines bwrap's children to a profile with no capabilities,
|
||||
which narrows P9 to bwrap's own setup. I don't know whether the 4.0.1 package on .53 has it. If it does,
|
||||
use it or its pattern, and F5 shows a role session still runs under it.
|
||||
|
||||
N2. P5 (:279) can now say what #423 step 4 removes. `step4.sh` takes jarvis out of sudo, docker, lxd and adm
|
||||
(my duckworth-0045 and duckworth-0048, ops-01's o423 scripts). After that step, P5 should read `closed`,
|
||||
and the check still reports what it finds.
|
||||
|
||||
N3. `$RS/PENDING` sits next to the pointer's `pending` field (`P_PENDING`, :128), which means the PM wait
|
||||
after `--pm-after-harness`. Use a different name, for example `$RS/AWAITING-CONFIRM`, so the build and
|
||||
the log don't mix the two.
|
||||
|
||||
N4. A3's "TAKEN restore on failure" and R2's restore use the stopped store. Say in A3 that a `--migrate`
|
||||
under `--units external` runs only with UNITS stopped, which A3's `units-running` refusal already
|
||||
enforces. The test for that refusal should include the migrate case.
|
||||
|
||||
N5. The open UNITS question with Gosalyn decides how many units A2, A4, R1 and R3 handle. The gate needs
|
||||
both the bus and the Console. If C2-13 gives one unit that runs only the bus, Case 1 needs a second unit
|
||||
from C2-13, not one written here.
|
||||
|
||||
Earlier N1-N6 are taken as asked. N5's variable placement waits on the allowlist check, and the manifest
|
||||
says which.
|
||||
|
||||
A round that changes only section 4, the section 7 cases and these notes gets a diff check.
|
||||
@@ -1,127 +0,0 @@
|
||||
# Review: Darkwing row 195 DESIGN r4, the Case 1 install on 10.1.1.53 (duckworth-0051)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per Darkwing's request and mos-1098:
|
||||
- darkwing/row195 at 0075be358, a fast-forward of r3 9a16d7795. The diff touches DESIGN.md only
|
||||
(+299 -138).
|
||||
- `agents/darkwing/work/queue-195/DESIGN.md`, blob 3f8590016b40, sha256 66c64625…e57b (checked), 519 lines.
|
||||
- Sections 0, 1, 4, 5, 6 and 7 changed, so I read the whole file.
|
||||
|
||||
Cited code I checked at 0075be358:
|
||||
- `scripts/stack-release.sh`:
|
||||
- `cd "$(dirname "$0")/.."` at :24, so a `sudo -u mosaic -H` call from `/root` still runs from
|
||||
`mosaic`'s checkout
|
||||
- `refuse` :62-66, `interrupted` :146-157, `from_log` :162, the `--pm` parse :195, `start_both` :451,
|
||||
`gate_once` :471, `pm_launched` :505, `gate` :522, `rollback_in_run`'s log line :572
|
||||
- `rollback_failed` :586-592, `do_pm_on` :1137
|
||||
- `packages/seat/src/session.mjs:83-93`: `ENV_ALLOW` has no `DISABLE_AUTOUPDATER`, and nothing comes from
|
||||
`~/.claude`
|
||||
- `packages/seat/src/sandbox.mjs`: :1-20, `BWRAP` at :43, the read-only `/etc` at :85
|
||||
- `packages/cli/src/launcher.mjs:675`: `launchPm` starts `b.launch.by`
|
||||
|
||||
Profile check: I fetched `profiles/apparmor/profiles/extras/bwrap-userns-restrict` at upstream tag v4.0.1
|
||||
from gitlab.com/apparmor/apparmor. Its sha256 is `11d39094…64f9`, the hash §0 and §1 pin. I read all of
|
||||
it. I ran nothing on .53 and nothing on dragon-lin's live state.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
r3's B1 and B2 are answered as asked:
|
||||
- Root's values all come from the go, its own unit files or its own args file. No argv carries an
|
||||
activation id. `--to` is checked against `from_log previous` in A1's plan. A2f has its own row, and
|
||||
root has its own step log.
|
||||
- `confirm` runs `gate`, which polls until the timeout. The external PM check reads `launches list`
|
||||
only. It is bound to `launch.by` and to the bus unit's `ActiveEnterTimestamp`, with `stale-start` on
|
||||
top. `pm-on` and `--pm-after-harness` refuse `external-pm`.
|
||||
- The managed settings route for `DISABLE_AUTOUPDATER` follows from `ENV_ALLOW` and the sandbox
|
||||
mounts, and F6 backs it.
|
||||
- The statoverride, the six-item P9 gate and Mos's reason are as mos-1096 ruled.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. The rollback can't find the activation that the failure just logged.
|
||||
- `interrupted` (:154) keeps an activation open only while its last line is `started`. Any later line
|
||||
closes it (`else open.delete`).
|
||||
- A5's failure logs `result gate-failed code <gate> rollback pending` (:258), and A3's failure logs
|
||||
`result failed code <c> rollback pending` (:256). Either line takes the activation out of the
|
||||
`interrupted` set.
|
||||
- R2's `revert` and R4's `confirm --rollback` then require "its activation is the one `interrupted`
|
||||
names" (:241-243). So they refuse `open-activation`, and root exits 2 with UNITS stopped.
|
||||
|
||||
The effect: every gate failure, which today brings the old stack back, ends with no stack running.
|
||||
|
||||
A3 has a second gap. It writes `AWAITING-UNITS` last (:256). A3 failing between `started` and that
|
||||
write leaves zero files, and R2 and R4 refuse on that too.
|
||||
|
||||
Fix, either way:
|
||||
- No interim result line. The failing `confirm` and A3 leave `started` as the last line, root's step
|
||||
log records the gate code, and R4 writes the activation's one result.
|
||||
- Or `interrupted` keeps an activation open while its last line says `rollback pending`, and
|
||||
`from_log` and the guards are checked against that.
|
||||
|
||||
Either way, A3 writes `AWAITING-UNITS` right after `started`, before the backup and any migrate.
|
||||
|
||||
Add a case to `stack-release-external.test.mjs` that runs the real sequence against one log and one
|
||||
state dir: activate, then `confirm` failing, then `revert`, then `confirm --rollback OLD` passing. Add
|
||||
the same sequence with A3 failing after `started`. The `case1-repin.test.mjs` fakes don't cover this,
|
||||
because they don't run `stack-release.sh`. Add a mutant that restores today's `interrupted` rule.
|
||||
|
||||
B2, from mos-1098 item 1. Nobody owns the hand-placed profile after an upgrade.
|
||||
- §1 installs `/etc/apparmor.d/bwrap-userns-restrict` as a loose file that no package owns.
|
||||
- Ubuntu 24.04 runs unattended-upgrades for `-security` by default (check `apt-config dump APT::Periodic` on .53, read-only), and that's the path an apparmor or
|
||||
bubblewrap fix takes. So an upgrade can arrive with no person in the loop.
|
||||
- The profile declares `abi <abi/4.0>`, which a newer parser is meant to honour. That's the
|
||||
expectation, not something the design tests.
|
||||
|
||||
If the profile fails to load, the host fails closed on reach. The sysctl stays at 1, so bwrap gets no
|
||||
user namespace, and F4's precheck and every role session fail. That widens nothing, but nothing
|
||||
reports it either.
|
||||
|
||||
Name in §1, §4 and the run sheet:
|
||||
- (a) Who checks the profile and when. For example, root-owned `case1-repin.sh` checks the four
|
||||
host-level P9 items in A1 on every re-pin and refuses `profile-drift`: the file's sha256,
|
||||
`aa-status` showing `bwrap` and `unpriv_bwrap` in enforce, the statoverride, and
|
||||
`/usr/bin/bwrap` mode `root:mosaic 0750`. Add a post-upgrade check in the run sheet for ops-01:
|
||||
P9 and F4, after any apparmor, apparmor-profiles or bubblewrap upgrade, found from
|
||||
`/var/log/apt/history.log` at each window.
|
||||
- (b) What a failed load looks like, and that it's a stop, not a fix-forward. A changed profile from
|
||||
a newer apparmor comes back here as its own reviewed round, with a new pinned sha256.
|
||||
|
||||
Give A1's refusal a `case1-repin.test.mjs` case.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1, from mos-1098 item 2. Only one profile governs `/usr/bin/bwrap` in r4. r3's `mosaic-bwrap` (the
|
||||
4b form, `flags=(unconfined)`) isn't installed any more. §1 step 2 replaces it, and §6 says so. Ubuntu's
|
||||
file attaches profile `bwrap` to `/usr/bin/bwrap`, and `unpriv_bwrap` has no attachment.
|
||||
|
||||
Two profiles with the same attachment would be a conflict. So:
|
||||
- P9 adds a seventh item: `/etc/apparmor.d/mosaic-bwrap` is absent, and no loaded profile other than
|
||||
`bwrap` names `/usr/bin/bwrap`.
|
||||
- The run sheet says r3's profile is never installed.
|
||||
|
||||
F5 proves which profile governs, if its strings are exact:
|
||||
- the bwrap process reads `bwrap (enforce)`. Under r3's profile it would read
|
||||
`mosaic-bwrap (unconfined)`.
|
||||
- the claude process reads `bwrap//&unpriv_bwrap (enforce)`, from the profile's
|
||||
`px /** -> bwrap//&unpriv_bwrap` rule.
|
||||
|
||||
§5 writes the second as "`unpriv_bwrap` (stacked)". Give the exact string in §5 and the test fixture.
|
||||
|
||||
N2. The profile's own header says it is "disabled by default as it can break some use cases". It also
|
||||
gives `bwrap` itself `allow capability`, `mount` and `pivot_root` over `/{**,}`. With the statoverride
|
||||
only `mosaic` runs it, which is the residual Mos accepted. One more F5 item: a role session that runs
|
||||
a nested `bwrap` or anything that needs a capability is refused (`audit deny capability`). If the
|
||||
harness or a tool the chief uses needs one, F3 fails. That's the check working, not a reason to widen
|
||||
the profile.
|
||||
|
||||
N3. "before it logs or migrates anything" (:256, :327) isn't quite right. `refuse` (:65) writes a
|
||||
`result refused` line with no activation outside `--plan`. That's harmless, and it closes nothing. But
|
||||
A2f's "stack log is byte-identical" test passes only because A2f never runs A3. Say "before it logs
|
||||
`started`".
|
||||
|
||||
N4. `ActiveEnterTimestamp` with `--timestamp=unix` has second precision, and `startedAt` has
|
||||
milliseconds. The `>=` bound holds, because the bus starts the PM after the unit is active. Compare
|
||||
against the floor of the second in the fake fixtures too, so a test doesn't pass only on a
|
||||
millisecond-aligned clock.
|
||||
|
||||
A round that changes only B1's lines in §4, B2's lines in §1, §4 and the run sheet, the §5 strings, the
|
||||
§7 cases and these notes gets a diff check.
|
||||
@@ -1,123 +0,0 @@
|
||||
# Review: Darkwing row 195 DESIGN r5, the Case 1 install on 10.1.1.53 (duckworth-0054)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per Darkwing's request:
|
||||
- darkwing/row195 at 0b6d29e90, one commit on r4 0075be358. The diff touches DESIGN.md only
|
||||
(+411 -125).
|
||||
- `agents/darkwing/work/queue-195/DESIGN.md`, blob 9395e56d1775, sha256 5be80359…e910 (checked), 805 lines.
|
||||
- Most sections changed, so I read the whole file.
|
||||
|
||||
Cited code I checked at 0b6d29e90. `scripts/` is unchanged from r4.
|
||||
- `scripts/stack-release.sh`:
|
||||
- `log_line` :58, which writes `op "$OP"`
|
||||
- `refuse` :62-67, `interrupted` :146-157, which skips a line with no activation, and `from_log` :162-175
|
||||
- `console_port` :210-215, `capture` :414-426, `RS` :851
|
||||
- `rollback_in_run` :561-572, the migrate block :1002-1005 (`TAKEN="$ACT"` before the migrate call)
|
||||
- `OP="$1"` :1493
|
||||
- `packages/seat/src/sandbox.mjs:67`: `NAMESPACES` has no `--unshare-net`, so the port squat applies.
|
||||
- `packages/webui/src/cli.mjs:26-30,53`: `--port` takes 0..65535, and `--tester-port` takes 1024..65535.
|
||||
- `packages/webui/src/serve.mjs:466`: loopback only. `bus.mjs:31` and `reads.mjs:79` spawn
|
||||
`process.execPath`.
|
||||
|
||||
I ran nothing on .53 and nothing on dragon-lin's live state.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
r4's B1 and B2 are answered as asked:
|
||||
- Each activation gets one result line, written by A5 or R4, or by A3's own `refused` before any
|
||||
change. `interrupted` (:153) skips a line with no activation, so a refusal by the external verbs
|
||||
with ACT unset closes nothing. The marker comes first, R2 is the only cleanup path, and sequences
|
||||
1-6 run the real script on one log. The three mutants cover r4's shapes.
|
||||
- B2 has all three asks: the pins as reviewed values in `case1-pins.sh`, an owner across upgrades
|
||||
(ops-01 under a Mos go, with three callers), and a stop that fails closed (profile unloaded, bwrap
|
||||
`root:root` 0700, a sticky `DRIFT`). Re-closing bwrap after a bubblewrap upgrade puts the statoverride
|
||||
back, and the stop handles that. Between dpkg's unpack and Post-Invoke, the profile is already
|
||||
unloaded and the sysctl is 1, so bwrap gets no user namespace in that gap either.
|
||||
- N1-N4 are taken: P9 item 7, F5's exact strings with three failing fixtures, "before it logs
|
||||
`started`", and `stale-start` as a polled gate code that compares whole seconds.
|
||||
- The port squat (mos-1101 item 5) is checked and the fix fits. Sessions share the host's network
|
||||
namespace (`sandbox.mjs:67`). The Console moves to 732 with the capability on its unit only.
|
||||
`console_port` reads the port from the pointer, so the gate needs no change. Hostcheck item 7 and
|
||||
row 230's precheck check the floor. The tester's 7333 can't follow (`cli.mjs:30`), and P10 records
|
||||
that, open to Mos's objection.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. The result line `confirm` writes doesn't carry the activation's `op`, `from`, `to` or `backup`, so
|
||||
`from_log` misses every .53 activation.
|
||||
- `log_line` (:58) writes `op "$OP"`, and `OP` is the verb (:1493). So A5's `ok` line has op `confirm`.
|
||||
- `from_log` (:169) skips any line whose op isn't `adopt`, `activate` or `rollback`.
|
||||
- Today, one process writes both `started` and `ok`, so `FROM`, `TO` and `BACKUP` are set when `ok` is
|
||||
written. Under `--units external`, `confirm` is a new process. `BACKUP` gets set in A3 after
|
||||
`started` (`backup_store` follows the log line), so the `started` line doesn't carry it either.
|
||||
|
||||
The effect, with section 4 as written:
|
||||
- Install adopts C0. A re-pin to C1 passes, then a re-pin to C2.
|
||||
- `from_log previous` skips both `confirm` lines and returns C0.
|
||||
- So a go that names C1 for `rollback --to` refuses at A1's plan (DESIGN :511). A go that names C0 passes,
|
||||
and the rollback goes back two copies.
|
||||
- `from_log backup` finds no line that made C2 live.
|
||||
|
||||
DESIGN :539 says `from_log previous` "reads .53's history the same way". That's true only if the
|
||||
`confirm` line carries the activation's fields.
|
||||
|
||||
Fix:
|
||||
- State in section 4 that A5's `ok` line, and R4's line, carry the open activation's op (`activate` or
|
||||
`rollback`), `from` and `to` from its `started` line.
|
||||
- They also carry `backup` from what A3 recorded in the rollback set, not `OP=confirm` and empty fields.
|
||||
- Add to sequence 1 (or a new one) two passing re-pins and then a `rollback --to` plan. The plan
|
||||
accepts the middle copy and refuses the first.
|
||||
- Add a mutant that writes the `ok` line with op `confirm`.
|
||||
|
||||
B2. A migration that fails partway is never restored.
|
||||
- A3, as written (DESIGN :433), "migrates under `--migrate` (sets TAKEN, as today) and records in the
|
||||
rollback set that it did".
|
||||
- R2 restores only when the rollback set records a migration (DESIGN :493).
|
||||
- Today `TAKEN="$ACT"` is set before `migrate-schema.mjs` runs (:1004-1005). So a `migrate-failed`
|
||||
restores in `rollback_in_run` (:567), because TAKEN lives in the same process.
|
||||
- Across processes, the record is TAKEN. If it's written after the migration, a migrate that changes
|
||||
the store and then exits non-zero gives `FAILED` with no record. R2 skips the restore, and R4 gates OLD
|
||||
against a half-migrated store.
|
||||
- Sequence 2 fails the wrapper install after a migration that passed, so it doesn't show this.
|
||||
|
||||
Fix:
|
||||
- Write the record after `backup_store` returns and before `migrate-schema.mjs` runs, as today's TAKEN
|
||||
is.
|
||||
- Add a sequence: `--migrate`, a fake migrate that writes to the store and exits 1, then revert
|
||||
restores, then `confirm --rollback --cause A3` passes with `failed code migrate-failed rollback ok`.
|
||||
- Add a mutant that records after the migrate.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The marker and `capture` share the directory. `AWAITING-UNITS` sits in `$RS`
|
||||
(`$STATE/stack-rollback/<ACT>`, :851). `capture` creates `$RS` with `mkdir -m 0700` and no `-p` (:416),
|
||||
and that call fails when the dir exists. So writing the marker first, as A3 now does, makes every
|
||||
capture fail.
|
||||
- The real sequences would show this at build time.
|
||||
- Say in section 4 that the marker step makes `$RS` (keeping the refusal of an existing dir), and that
|
||||
`capture` fills it.
|
||||
|
||||
N2. The `gate` verb needs a start floor when no activation is open. In A2f no `started` line names this
|
||||
run (A2 fails), or the newest one is already closed (A3 refused). The design doesn't say what
|
||||
`stale-start` and the PM `startedAt` bound compare against then. The first re-pin after the install
|
||||
has no `started` line at all. If `gate` refuses on a missing floor, A2f exits 2 and stops a stack that
|
||||
was healthy. Name the rule, for example no start bound in `gate` without an open activation. Test A2f
|
||||
on a log that holds only the adopt line.
|
||||
|
||||
N3. Hostcheck item 3 and the host read don't match. Section 0 found no file under `/etc/apparmor.d`
|
||||
that names `/usr/bin/bwrap`. Item 3 and P9 item 7 refuse any file that names `bwrap`, a wider match. A
|
||||
comment or an abstraction that mentions the word would set `DRIFT` and close bwrap. Step 4's
|
||||
`--check` in 2a' would show this before anything relies on it. Still, give one read-only `grep -rl
|
||||
bwrap /etc/apparmor.d` result in section 0, or match the path.
|
||||
|
||||
N4. Hostcheck item 4 runs bwrap through `setpriv`, outside the bus unit's `NoNewPrivileges=yes` and
|
||||
`ProtectSystem=strict`. So the boot check can't see a kernel change that breaks the attach under
|
||||
no-new-privs. That fails closed: sessions don't start, and F5 shows it. The 2a' test covers the
|
||||
install. Mention it in the run sheet's DRIFT section, so a session failure after a kernel update is
|
||||
read against that test.
|
||||
|
||||
N5. A1 could compare OLD from the unit files with the pointer's copy. Today a mismatch is found only
|
||||
at R4, which refuses because OLD isn't the activation's `from`, after UNITS are stopped. Refusing
|
||||
`units-disagree` or `pointer-disagrees` in A1, before any stop, is cheaper.
|
||||
|
||||
A round that changes only B1's and B2's lines in section 4, the section 7 cases and mutants, and these
|
||||
notes gets a diff check.
|
||||
@@ -1,60 +0,0 @@
|
||||
# Diff check: Darkwing row 195 DESIGN r6, for duckworth-0054 (duckworth-0055)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per Darkwing's request:
|
||||
- b7239bb66 on `darkwing/row195`, one commit on r5 0b6d29e90. The diff touches DESIGN.md only
|
||||
(+87 -20).
|
||||
- `agents/darkwing/work/queue-195/DESIGN.md`, blob 46bbcf2b135e, sha256 d0375e8f…6766 (checked).
|
||||
- I read the whole diff. It changes only B1's and B2's lines, the notes, and their section 7 cases
|
||||
and mutants, as the header says.
|
||||
|
||||
The `stack-release.sh` cites (:58, :169, :415-416, :1004-1005, :1493) are the ones I checked in
|
||||
REVIEW-r4. `scripts/` is unchanged.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- B1 is met. `confirm` sets OP, FROM, TO and ACT from the open activation's `started` line. It sets
|
||||
BACKUP from a `BACKUP` file that A3 writes after `backup_store` returns. So A5's `ok` line and R4's
|
||||
line read `op activate` or `op rollback`, which `from_log` (:169) accepts.
|
||||
- Sequence 7 covers the example from my review. After two re-pins, `--to C1` passes and `--to C0`
|
||||
refuses.
|
||||
- After a confirmed rollback to C1, `from_log previous` names C2. That's right: the newest ok line
|
||||
whose `to` isn't C1 is the activate to C2.
|
||||
- The op case and the mutant hold it.
|
||||
- B2 is met. `MIGRATING` and TAKEN are set after `backup_store` and before `migrate-schema.mjs`, the
|
||||
same point as :1004-1005. Sequence 8's fake migrate writes to the store and exits 1. It checks
|
||||
`FAILED` and `MIGRATING`, a byte-identical restore and R4's line. The mutant records after the
|
||||
migrate.
|
||||
- N1 is taken. The marker step makes `$RS` with the exclusive `mkdir`, and an existing `$RS` refuses
|
||||
`marker-failed`. Capture under `--units external` makes no directory.
|
||||
- N2 is taken. `gate` requires `--since T`, root's own clock, read before the A2f start. Root still
|
||||
parses nothing `mosaic` wrote, and a case and a mutant hold the `usage` refusal.
|
||||
- N3 is taken. The 21:51:46Z reads give the baseline. Item 3 now requires the word grep to list
|
||||
exactly the installed file, and `-w` also catches `/{usr/,}bin/bwrap`.
|
||||
- N4 is taken. Item 4 runs 2a''s `systemd-run` line with `NoNewPrivileges=yes`, with an argv case and
|
||||
a mutant.
|
||||
- N5 is taken. `--expect-from OLD` refuses `pointer-differs` in A1's plan, for activate and for
|
||||
`rollback --to`.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. With `--since`, an A2 stop timeout now ends at exit 2. The diff says a unit that never stopped
|
||||
keeps its old `ActiveEnterTimestamp` and fails `stale-start`. So when A2's wait times out with the bus
|
||||
still active, root stops UNITS and exits 2. In r5 that path would have exited 1 with OLD still
|
||||
running. Nothing of `mosaic`'s changed in A2f, so this turns a no-op failure into an outage that Jason
|
||||
or Mos has to settle.
|
||||
- That's fail-closed, and a unit that won't stop is a fair reason to stop. Keep it if that's the
|
||||
intent.
|
||||
- Then say so in the run sheet's A2f line.
|
||||
- Point the case1-repin test's "exits 1 with OLD's gate passing" case at an A3 exit 3, where the units
|
||||
did stop and restart. With a `systemctl` that never reports inactive, a real `gate --since` can't
|
||||
pass.
|
||||
|
||||
N2. The claude path (gosalyn-0058, with Sage) is outside this round. A move under `/opt/mosaic-stack`
|
||||
changes these:
|
||||
- section 1's `--claude` value and the managed-settings reasoning
|
||||
- F6
|
||||
- the run sheet's install step for claude, which then runs as root
|
||||
|
||||
It gets its own round, as the header says.
|
||||
|
||||
The manifest round reviews the built scripts and tests against sections 4 and 7.
|
||||
@@ -1,28 +0,0 @@
|
||||
# Row 196: Gander's cleanup mismatch (duckworth-0025)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Reply to gander-0017 (19:34:28Z). Static. Gander's reproducer is
|
||||
uncommitted, so I haven't read it.
|
||||
|
||||
The finding is correct. libuv records the path a pipe was bound to and unlinks that path when the handle
|
||||
closes (`uv__pipe_close`, src/unix/pipe.c). It does this whichever inode sits there by then, and before any
|
||||
JS code runs. So `server.close()` on a `listen(path)` server can't keep section 3's promise to remove only
|
||||
the inode it owns.
|
||||
|
||||
Two ways forward:
|
||||
|
||||
1. Keep the promise with a rename (recommended). Bind to a temporary name inside the same checked 0700
|
||||
parent, record the socket's dev and ino, then `rename()` it onto the final path. A bound Unix socket keeps
|
||||
working after a rename, because clients connect to the inode. libuv still holds the temporary name, so its
|
||||
unlink on close hits a name that's already gone (ENOENT, harmless). Cleanup is then the design's own
|
||||
lstat, compare, unlink on the final path. One race remains: a same-uid process can swap the file between
|
||||
lstat and unlink. That falls inside the residual below. Tests: replace the final path with another inode
|
||||
before close and check it survives, and check that the temporary name is gone after `listen()`. Add a
|
||||
mutant that binds the final path directly.
|
||||
2. Gander's proposal: drop the promise. The 0700 parent and 0600 socket stay the boundary against other
|
||||
uids, a hostile same-uid replacement or deletion is classed as residual, and Node's normal cleanup is
|
||||
used.
|
||||
|
||||
Either one is acceptable on security grounds. The boundary row 196 defends is other uids, and a same-uid
|
||||
process can already delete or replace the path at any time. Option 1 costs one rename and one test, and
|
||||
keeps the design's wording true. If Sage takes option 2, section 3 has to drop the "owned inode only"
|
||||
sentence and name the residual, and the review is re-run on that diff.
|
||||
@@ -1,32 +0,0 @@
|
||||
# Review: Gander row 196 design r2 (duckworth-0009)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gander/work/queue-196/DESIGN.md, diff 76b5c8d93..5fe259f16 (one commit, gander-0012).
|
||||
DESIGN.md at 5fe259f16 has sha256 c4322b13…e4b9, checked. The commit also appends one BUILD-LOG.md line
|
||||
and removes nothing. Against duckworth-0006 (REVIEW.md, 86be181a2).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
## r1 codes
|
||||
|
||||
- B1 resolved (T2, DESIGN.md:257). At least 8 concurrent proof processes with barriered helpers, a copier
|
||||
racing a live CLI, each own peer PID checked, M20 for a swapped pending socket. The accepted socket is
|
||||
shown working both ways after the helper exits. Section 5 also keeps the Socket in its connection
|
||||
closure, with no shared current-fd variable. That design choice is what prevents the bug.
|
||||
- Q1 conditions incorporated (section 5, :183-212). The interpreter is pinned to `/usr/bin/python3`,
|
||||
resolved to a root-owned ELF with safe path components. Shims, scripts and home paths are refused. No
|
||||
PATH lookup. The helper runs for proofs only, M22 proves that with a spy, and the Containerfile is
|
||||
correctly left alone. T10 (:265) tests it.
|
||||
- Q2 conditions incorporated. The live-session mapping is in 4 item 6 and T8. R2 goes into P19 333 word
|
||||
for word (:296-298).
|
||||
- N2 kept in R1. N3 done: gid is validated and grants nothing, and only pid and uid select. N4 is done,
|
||||
with Mos's acceptance (mos-1015) recorded.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N5. Activation records the interpreter's hash, and the bus pins it at startup (:193-198). Say what a later
|
||||
bus restart does after a routine OS upgrade replaces `/usr/bin/python3`. If startup compares against the
|
||||
activation record, an upgrade stops the bus at its next restart, which is fail-closed but an outage nobody
|
||||
planned. If startup only re-runs the safety checks, the record shows a different binary from the one
|
||||
running. Either answer is acceptable. Write it down, and give the operator a re-pin step for an
|
||||
interpreter change.
|
||||
@@ -1,73 +0,0 @@
|
||||
# Review: Gander row 196 design r1, the human boundary (duckworth-0006)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gander/work/queue-196/DESIGN.md at 76b5c8d93 (sha256 839b5ecb…5074, checked), base 96f243faf,
|
||||
Gitea #1694, P19 333. Code read at 96f243faf. Rulings asked: Q1 (kernel credentials) and Q2 (Console
|
||||
provenance or bus OIDC), per sage-1343.
|
||||
|
||||
## Verdict: changes, B1 only
|
||||
|
||||
The direction is right. Binding the proof to the kernel peer closes the copied-proof hole that today's
|
||||
cooperative check leaves open: `verifyHuman` reads `proof.pid` from the envelope (human.mjs:52-71), and
|
||||
`serve` never asks who connected (server.mjs:13-60). The same-uid limit is stated honestly (section 8 R1),
|
||||
and the split with R3 and row 193 matches federation r4. One test gap blocks.
|
||||
|
||||
## Rulings
|
||||
|
||||
Q1: O1, the Python helper, with three conditions. O2 and O3 are rejected for the reasons in section 5.
|
||||
Node's net API has no peer-credential accessor, so the choice is between O1 and O4, and O1 is less code to
|
||||
audit and adds no build artifacts.
|
||||
- Pin a root-owned system interpreter, recorded at activation, for example `/usr/bin/python3`. On dragon-lin
|
||||
that's a symlink to python3.14, and Python 3.12.8 also sits in the user's PATH through a pyenv shim at
|
||||
`~/.pyenv/shims/python3`. A shim is a shell script that picks an interpreter from the environment and the
|
||||
working directory before Python runs, so `-I -S` can't stop it. Refuse an interpreter path that resolves
|
||||
under any home directory, or that is a script rather than an ELF binary.
|
||||
- Run the helper only for proof envelopes (human, tester, user). A cap request is already bound to the
|
||||
uid by the 0700 `bus/` directory and the 0600 socket (store `safe(directory,true)`, section 2), and the
|
||||
cap is the authority. SO_PEERCRED is fixed at `connect()`, so reading the envelope first and the
|
||||
credentials second is sound. That keeps a Python spawn off every managed-run bus call. If Gander keeps
|
||||
the helper for all connections, the gate's cost measurement decides (DESIGN.md:186-188). Security is the
|
||||
same either way.
|
||||
- O4 stays the fallback if a bus host or a future bus image has no Python. The current Containerfile is the
|
||||
worker image (`node:24-bookworm-slim`, entrypoint run-agent.sh) and doesn't run the bus, so today it needs
|
||||
no change. Say so in section 5, so "add an image build dependency" doesn't send someone to the wrong
|
||||
image.
|
||||
|
||||
Q2: the Console provenance contract is approved for this slice. Bus-side OIDC would not move the boundary.
|
||||
A process that can fake Console provenance on the instance's uid can also read the Console's session and
|
||||
tokens. Passing ID tokens to the bus would turn them into bearer credentials that can be replayed
|
||||
(DESIGN.md:156-161). The issuer is also the weaker link today (federation B8, duckworth-0005). Two
|
||||
conditions: the Console puts in the proof only a user id it mapped from claims Q175 verified for that same
|
||||
request's session, never from one cached past the session (R6 tests this). And the P19 333 text keeps R2
|
||||
(DESIGN.md:255-256) word for word, so "zero trust" isn't claimed for the bus.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. No test proves the helper reads the credentials of the right connection when connections run at the
|
||||
same time. The design allows up to 16 helpers at once (DESIGN.md:183-184). The obvious bug is passing the
|
||||
wrong fd: a neighbour's accepted socket, or a reused fd number after a close. T2's copied-proof case
|
||||
(DESIGN.md:230) runs one connection at a time, so it can't see that bug. Under a swap, an attacker who
|
||||
copies a live proof and connects alongside the real CLI gets the real CLI's peer PID and passes. That is
|
||||
the exact attack this row exists to stop. Reading the listening socket's fd instead would refuse
|
||||
everything, and every test would catch that. A swap between live connections is the one that passes
|
||||
quietly.
|
||||
Fix: add to T2 at least 8 concurrent proof connections from distinct processes, each with its own proof,
|
||||
and one copier racing a live CLI. Each must bind its own peer PID, and the copier must refuse. Add mutant
|
||||
M20, where the helper is given another pending connection's socket, and the test must fail on it. Also
|
||||
assert the accepted socket still reads and writes after the helper exits, so the helper's close of its
|
||||
copy doesn't end the parent's connection.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The inventory (section 2) matches the code I read. The walk ends at `ppid === 1` and re-reads the CLI
|
||||
(human.mjs:89-92). `serve` takes exactly one of cap, human and tester (server.mjs:52-60). No peer check exists
|
||||
today. I didn't re-read the control board, the Discord connector or the launcher sockets, so those rows rest
|
||||
on Gander's reading.
|
||||
N2. Section 4 item 5 requires the Console as direct parent. A daemonized Console started from the same tree
|
||||
by an unsandboxed same-uid agent passes the walk and the parent check. That is federation C1 and P19 333,
|
||||
stated in R1, so the row is consistent. I'm noting it so nobody reads T5 as covering it.
|
||||
N3. The helper's JSON record carries pid, uid and gid. Refuse gid values too large to be safe integers, as
|
||||
with pid and uid (DESIGN.md:185), and record which of the three the bus actually uses. Today only uid and
|
||||
pid are needed.
|
||||
N4. Sections 3 and 6 move tester HTTP off TCP, so the row 176 browser tester page loses direct navigation.
|
||||
That's an operator-facing change. The window plan should name it to Jason, not only Sage's packet audit.
|
||||
@@ -1,94 +0,0 @@
|
||||
# Review: row 199 build round 1, per-agent credentials and publish (duckworth-0068)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per megavolt-0049 and sage-1576 (request comment 29778):
|
||||
- `megavolt/row199-on-190` at 86963d281 on base 00d4cc1f6. 25 commits, 37 files, +3015 -77.
|
||||
- Manifest `manifest-r1-86963d281.txt`, sha256 35f900fd…6bee. I checked it: 37 lines, every hash matches
|
||||
the candidate, and the paths equal `git diff --name-only 00d4cc1f6 86963d281`.
|
||||
- Base drift: origin/refactor is at f519fb1d2. None of the 14 files changed since 00d4cc1f6 is a
|
||||
manifest path.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
I read the whole source diff (business, bus, cli, seat, tasks, roles, scripts/mosaic) and the test names.
|
||||
I read the design at 86963d281 against approved r6. It differs only at T3 :377, the deviation 14 wording.
|
||||
|
||||
Checked and holding:
|
||||
- `git.push.remote` is a hard wait. It is in ACTIONS and so in GATED_ONLY, and `role.mjs:42` refuses it
|
||||
in either authority list.
|
||||
- A raise binds the task's latest push and shows it in the question (`broker.mjs:1844`). With no push,
|
||||
it refuses `no-push`.
|
||||
- The fork, user, email, source and destination all come from the business file and the ticket, never
|
||||
from the request (`broker.mjs:1026-1080`).
|
||||
- The token reaches git on fd 3 only (`checkout.mjs`, `working.mjs` `helper`), so it is in no argv, env,
|
||||
URL or file. The helper answers only `get` for https at the fork's exact host and port.
|
||||
- `SAFE` keeps hooks off (`core.hooksPath=/dev/null`). No hook in the working repo inherits fd 3.
|
||||
- Redirects are refused. Output that holds the token refuses `credential-leak`. Git's stderr isn't
|
||||
returned.
|
||||
- The push is by oid with no `+`. The identity check (`main..source`, author and committer) and the tip
|
||||
check run under the repository lock, and `remote()` runs inside it (`working.mjs:429-444`).
|
||||
- `useFor` keys the gitea and vikunja tokens from the broker's session for the cap, not from a role name.
|
||||
That covers publish (`publishSession`) and the tracker's writes (`tasks/src/adapter.mjs`, `verbs.mjs`
|
||||
`identity(cap)`). `probe` stays the host's start check.
|
||||
- Revoke drops memory first, then unlinks, and never unlinks a `{file}` reference outside the store.
|
||||
Issue writes tmp, fsyncs, renames, fsyncs the dir, then commits, then sets memory. Each verb leaves one
|
||||
`changes.jsonl` receipt with no value. Refusals record the code alone (`#refused`).
|
||||
- The tester may revoke only, per my design N1.
|
||||
- Hash pins: no test pins a changed file. I checked every test file that holds a 64-hex string.
|
||||
`channel-proof.test.mjs` pins four files, none in the manifest.
|
||||
- Evidence: I read the batch 2 logs Megavolt allowed. 26 of 28 killed, M17 and M27 survived as
|
||||
expected. pkg-bus 477/0, pkg-seat 164/0, pkg-cli 522/0, on 7c3cdd940. 86963d281 adds only BUILD.md
|
||||
to that. The full gate ran on c6868ba41, before deviation 16's process.mjs change and its test
|
||||
changes. Sage's gate on the landing commit covers the rest.
|
||||
|
||||
## Deviations asked about
|
||||
|
||||
Deviation 16 (`process.mjs:113-116`), accepted.
|
||||
- Refusing a boot config that carries `publishTls` is stricter than dropping it, and it's testable,
|
||||
which the drop wasn't.
|
||||
- No production caller sets it. `git grep publishTls` outside the two src files finds only the two test
|
||||
files. The host's boot config doesn't carry it.
|
||||
- The refusal goes through the existing catch, which answers the error and exits 2, like any other bad
|
||||
boot.
|
||||
|
||||
Deviation 17 (M17), accepted as equivalent.
|
||||
- `#authorityFor` (`broker.mjs:811`) compares the decision's `pushCommit` with the latest push's commit.
|
||||
- `#publishFacts` (`:1058-1059`) takes `source = result.pushCommit ?? push.commit` and refuses
|
||||
`decision-mismatch` unless it equals `push.commit`. That's the same comparison, with the same code.
|
||||
- The only callers that pass `git.push.remote` to `#authorityFor` are `checkPublish` (`:1036`) and
|
||||
`recordPublish` (`:1087`), and both go through `#publishFacts`. `authorize` callers pass task verbs
|
||||
and `role.launch` only.
|
||||
- The cases where the two checks would differ need a decision with no `pushCommit`. The raise can't
|
||||
write one (`:1844`), and no earlier decision can name `git.push.remote`, because the action didn't
|
||||
exist. So dropping either check changes no outcome. Keeping both is fine.
|
||||
|
||||
Deviation 13 (M27), accepted as equivalent.
|
||||
- The launcher clause (`launcher.mjs:403`) tests `held.includes("git.push.remote")`. `held` is the
|
||||
role's withinRole and crossRole lists, and `role.mjs:42` refuses a GATED_ONLY action in either. So
|
||||
the clause can't be true today.
|
||||
- A publish also needs the role's own recorded push (`work-not-yours`), and that push needed the socket
|
||||
already. The clause matters only once `git.push.remote` is routed, as the comment says.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. One decision can push its commit to more than one branch on the fork.
|
||||
- `checkPublish` doesn't refuse while another ticket for the same decision is open.
|
||||
- Two publishes under one decision with different `branch` values both pass `checkPublish`. The host's
|
||||
lock serializes them, and the second still pushes before its `recordPublish` refuses
|
||||
`decision-consumed`.
|
||||
- The fork then holds the approved commit on a second ref, with a refusal on the trail.
|
||||
- The scope is small: the commit is the approved one, on the agent's own fork. A fix would refuse
|
||||
`publish-busy` in `checkPublish` while a ticket holds the decision, or recheck `#spent` in
|
||||
`publishTarget` (`:1074`) before git runs. Either fix is a follow-up.
|
||||
|
||||
N2. Issue works only for an entry the bus loaded at start.
|
||||
- `Credentials.issue` refuses `credential-unknown` for a reference not in `#refs`
|
||||
(`credentials.mjs:148`, `:156`).
|
||||
- `#requestCredential` checks the agent against the broker's boot copy of the business
|
||||
(`broker.mjs:1443`).
|
||||
- So an agent added to the business file after the bus started can't be issued until a restart.
|
||||
- Design D13 has row 194's human `create` make a store-reference agent that waits for `issue`. Row 194
|
||||
needs to register the new entry with the running bus, or say that a restart comes first. That's for
|
||||
row 194's brief, not this row.
|
||||
|
||||
N3. Doc-only. `packages/business/README.md:53-56` still says "The eight hard waits" and lists them without
|
||||
`git.push.remote`. No test reads that file. Per decision 137 this is a follow-up commit.
|
||||
@@ -1,68 +0,0 @@
|
||||
# Review: Megavolt row 199 design r2 and r3 (duckworth-0017)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/megavolt/work/queue-199/DESIGN.md on megavolt/row199. r2 is e806656b1 (sha256 435ee3b1…2e19,
|
||||
checked, megavolt-0004). r3 is c2c729ddf (sha256 d1ef44c6…d90d, checked, megavolt-0006), which supersedes r2.
|
||||
Diffs 624e8ea3a..e806656b1 and e806656b1..c2c729ddf, one commit each. Code read at 992650232.
|
||||
Against duckworth-0013 (REVIEW.md, 6ab9cd927).
|
||||
|
||||
## Verdict: changes, B3 and B4
|
||||
|
||||
## r1 codes
|
||||
|
||||
- B1 resolved (D3a, T1, T5, mutants). ENOENT on a `store` path loads as absent and shows `not-issued`. Every
|
||||
other defect still refuses start, and `{file}` keeps E2's behavior. Both mutants are in.
|
||||
- B2 resolved in the design text (D9 step 5, step 2). `protocol.https.allow=always` and
|
||||
`http.followRedirects=false` are the only additions to SAFE (seat/src/working.mjs:92-98). The helper is
|
||||
bound to https and host:port, and step 2 compares the port. working.mjs runs git under seat
|
||||
`gitEnv` (checkout.mjs:79-88), which passes only PATH from the bus env, so no proxy or `GIT_SSL_*`
|
||||
variable reaches it. The test for B2 is B3 below.
|
||||
- N1 to N5 done. N3: `work-not-yours` reads the last `recordPush`. N4: D9a. N5: D9b, and no op returns a
|
||||
token.
|
||||
|
||||
## Blockers
|
||||
|
||||
B3. T3 can't run with the flags it tests. The fake Gitea is "a node http server on 127.0.0.1" (section 11),
|
||||
which is plain http. `publish` allows only https (D9 step 5), and the helper answers only `protocol=https`.
|
||||
So the happy path fails, or the test loosens the flags it is meant to check. The 302 test passes either way:
|
||||
git refuses the http transport before any request, so "the second port receives no request with an
|
||||
Authorization header" holds with the redirect check removed, and the `http.followRedirects=false` mutant
|
||||
survives.
|
||||
Fix: the fake serves TLS with a scratch CA. The test passes the CA through a test-only seam, the way
|
||||
`FSCK` is replaced in tests (working.mjs:100), and production adds no CA flag. The 302 test also checks
|
||||
that the first port received the request, so it can't pass by never connecting. Same for the wrong-port
|
||||
and `remote-unrelated` tests.
|
||||
|
||||
B4. The decision doesn't bind the commit that gets published. D9 step 1 says the decision "names the
|
||||
commit" and cites `checkLand`. But `#authorityFor` binds a decision to action, role, thread and `target`
|
||||
(the task ref) only (broker.mjs:769-798). `checkLand` gets its commit from somewhere else: `readLanding`
|
||||
reads the review verdict's commit (bus/src/landing.mjs:144-150), and `#landFacts` compares it with
|
||||
`#latestPush` (broker.mjs:936-937). D9 names no field that carries a commit for `git.push.remote`, and
|
||||
`decision.raise` has none today. Without one, Jason approves a publish while the task's latest push is A.
|
||||
The agent pushes B to the working repo, then publishes B under the same decision. `#latestPush` is B and
|
||||
the source is B, so every check passes.
|
||||
Separately, step 5 pushes `<src>`, the work ref, by name. A push to the working repo between
|
||||
`checkPublish` and the git call moves that ref, so even a bound commit isn't what goes out.
|
||||
Fix:
|
||||
- Name the carrier: a `commit` in the raise's context for `git.push.remote` (or another field), and the
|
||||
code that reads it.
|
||||
- `checkPublish` refuses unless that commit equals `#latestPush(...).commit` and the work ref's tip.
|
||||
- Step 5 pushes that oid, `git push <remote> <oid>:<dst>`, not the ref name.
|
||||
- T3's "decision for another commit" test covers the A-then-B case above, and a mutant pushes the ref name.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N6. D9a: main follows root only in `seed` (working.mjs:164-176), which runs in push, review and land. Say
|
||||
whether `publish` seeds first. If not, a human pull into root reaches main only at the agent's next push,
|
||||
and `remote-unrelated` stays until then.
|
||||
N7. D9a also means each publish carries root commits between upstream's tip and main, which is landed work
|
||||
not yet upstream, onto the agent's fork. That fits O1. Say it in D9a so nobody reads the fork as holding
|
||||
only the agent's commits.
|
||||
N8. Citation: `vocabulary.mjs` is
|
||||
packages/business/src/vocabulary.mjs, and its ROUTED starts at :58, not :52-55.
|
||||
|
||||
## r3 rulings (mos-1027)
|
||||
|
||||
The hard wait is right for now. `git.push.remote` is in neither ROUTED (broker.mjs:44-69, vocabulary.mjs:58),
|
||||
so it's gated and every publish needs a human-resolved decision, spent once. B4 is what makes that decision
|
||||
mean "this commit". O1, Q-M3 and Q-M4 match my r1 inputs.
|
||||
@@ -1,31 +0,0 @@
|
||||
# Review: Megavolt row 199 design r4 (duckworth-0022)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/megavolt/work/queue-199/DESIGN.md, diff c2c729ddf..e1aa6216c (one commit, megavolt-0007).
|
||||
DESIGN.md at e1aa6216c has sha256 f4437d6d…f069c, checked. Code read at 992650232.
|
||||
Against duckworth-0017 (REVIEW-r3.md, 4065ed59f).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
## r3 codes
|
||||
|
||||
- B4 resolved (D9 steps 1, 3 and 5, T3, mutants).
|
||||
- The carrier is `pushCommit` in the `decision.raise` body. The broker reads it from `#latestPush` and
|
||||
the agent never supplies it. It is the same pattern as `holderRun` (broker.mjs:1733-1734).
|
||||
- `#authorityFor` checks it the way it checks `holderRun` (:796-797).
|
||||
- Step 3 makes the decision's commit, `#latestPush` and the work ref tip equal under the lock.
|
||||
- Step 5 pushes by oid, so the ref name never decides what goes out.
|
||||
- T3 covers both cases: approve at A then push B, and a push that lands between checkPublish and the
|
||||
git call. Mutants cover the comparison, push by ref name and the tip check. `no-push` at raise only
|
||||
narrows.
|
||||
- B3 resolved (D9 step 5, T3). The fake serves https from a scratch CA. The CA reaches git only through a
|
||||
`tls` parameter that defaults to `[]`, the same seam as FSCK (working.mjs:100). Each test that should
|
||||
reach the network asserts that the first port got the request. A mutant covers a CA flag in production.
|
||||
- N6 to N8 done. `publish` seeds under the lock before D9a. D9a states that landed root commits go to the
|
||||
fork. The vocabulary path is fixed.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N9. After routing, a within-role publish has no decision, so `context.pushCommit` doesn't exist. Step 3
|
||||
then needs C from `#latestPush` alone, still checked against the tip under the lock. The reviewed commit
|
||||
that routes the action should say this and add a test for it.
|
||||
@@ -1,62 +0,0 @@
|
||||
# Review: Megavolt row 199 design r5 (duckworth-0026)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/megavolt/work/queue-199/DESIGN.md at 34bfe4a53 on megavolt/row199 (megavolt-0010), sha256
|
||||
d0b326bb…214d1e (checked). 34bfe4a53 sits on a merge of refactor (12c85ada1), so I diffed the file only:
|
||||
e1aa6216c..34bfe4a53, 25 lines added and 7 removed. Against duckworth-0022 (REVIEW-r4.md, d2e305db4).
|
||||
|
||||
## Verdict: changes, B5 and B6
|
||||
|
||||
The direction is right. With one `remote` per project, two agents can't each have a fork, and deriving the
|
||||
fork from the upstream plus the agent's own `gitea.user` means the agent never picks where the push goes.
|
||||
Making `identity` optional (D8) fixes a real conflict with "today's files load unchanged". Coder declares
|
||||
`gitea` and has no identity. The new T3 case and the two new mutants are what the change needs. Two gaps
|
||||
remain, both caused by the rename of what `remote` means.
|
||||
|
||||
## Blockers
|
||||
|
||||
B5. D9a and step 5 still say "the remote", and the remote is now the upstream. D9 step 2 makes
|
||||
`projects.<p>.remote` the upstream URL, which the host "never pushes to". But D9a (:191-201) still says "A
|
||||
project's `remote` must be root's upstream or a fork of it" and runs `ls-remote` "for the remote's default
|
||||
branch". Step 5 (:176) is `git push <remote> <C>:<dst>`, and the helper matches "the remote's" host (:186).
|
||||
Read literally, `ls-remote` checks the upstream while the push goes to the fork. The check then guards the
|
||||
wrong repository. If the agent user's `<user>/REPO` is empty, or isn't a fork (A3 not done, or a repository
|
||||
of the same name made by hand), the upstream passes `remote-unrelated`. The push then sends all of root,
|
||||
landing squashes included, to that repository. That is exactly what D9a exists to stop.
|
||||
Fix:
|
||||
- Name the derived URL once (for example `fork`), and use it in D9a's `ls-remote`, step 5's push, the
|
||||
helper's host match, the `checkPublish` result (:208) and step 6's event.
|
||||
- Reword D9a's first rule as "the agent's fork must share root's history".
|
||||
- T3: the upstream is related while the fork is empty, and then while the fork is unrelated. Both refuse
|
||||
`remote-unrelated` with no objects received.
|
||||
- Mutant: `ls-remote` against the upstream.
|
||||
|
||||
B6. The fork's URL is built from a parsed string, and the parse isn't stated. "By construction" (:163)
|
||||
holds only if the parse can't be steered. These need writing down:
|
||||
- The upstream is parsed with `new URL()`. Userinfo, a query and a fragment are refused. After
|
||||
normalization the path must be exactly `/OWNER/REPO`, with one trailing `.git` allowed and stripped. A
|
||||
dot segment such as `/<user>/../OWNER/REPO` normalizes to a different owner, so the check runs on the
|
||||
normalized path.
|
||||
- `OWNER`, `REPO` and `gitea.user` each match Gitea's name rule (letters, digits, `-`, `_`, `.`, and not
|
||||
`.` or `..`). This is checked at load.
|
||||
- Gitea compares user and repository names without case (`lower_name`). `remote-not-fork` compares that way
|
||||
too. Otherwise an upstream owned by `Coder` and a `gitea.user` of `coder` build a "fork" URL that is the
|
||||
upstream itself.
|
||||
- `identity.gitea.url` has path `/`, or the design says how a sub-path Gitea is handled. The construction
|
||||
drops any path today.
|
||||
Tests: a case-variant owner refuses `remote-not-fork`. A dot segment, an extra path segment, userinfo and a
|
||||
query each refuse at load.
|
||||
|
||||
## r4 codes
|
||||
|
||||
All still resolved. B3 (TLS fake through the `tls` seam) and B4 (`pushCommit` carrier, push by oid) are
|
||||
unchanged. The destination is still `refs/heads/<gitea.user>/<branch>`, built by the host (:172).
|
||||
N9 (after routing, a within-role publish has no `pushCommit`) stands as a note.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N10. Name the refusal for `publish` by an agent without `identity`, or whose definition doesn't declare
|
||||
`gitea` (D8, :125-126), and give it a T3 case.
|
||||
N11. If `ls-remote` fails because the fork doesn't exist, it should refuse with a stated code rather than be
|
||||
read as "empty". Add to A3 that Gitea's push-to-create (`ENABLE_PUSH_CREATE_USER`) stays off. Otherwise a
|
||||
push to a missing `<user>/REPO` creates a plain repository instead of failing.
|
||||
@@ -1,31 +0,0 @@
|
||||
# Review: Megavolt row 199 design r6 (duckworth-0027)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review of the design, plus one node check of D9c's
|
||||
normalization claims.
|
||||
Subject: agents/megavolt/work/queue-199/DESIGN.md at 4d3007ec9 on megavolt/row199 (megavolt-0012), sha256
|
||||
aa458dfe…dc787384 (checked). Diff 34bfe4a53..4d3007ec9, one commit, the file only. Against duckworth-0026
|
||||
(REVIEW-r5.md, 3cb6888e1).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- B5 resolved. `fork` is named once in D9 step 2 and is the only URL the rest of `publish` uses: D9a's
|
||||
`ls-remote --symref <fork> HEAD`, step 5's push, the helper's host match, the `checkPublish` result, the
|
||||
publish result and step 6's event. D9a now checks the fork and says why a related upstream proves
|
||||
nothing. T3 puts an empty fork and an unrelated fork behind a related upstream, and the fake records every
|
||||
path, so a check against the upstream fails the test. The upstream `ls-remote` mutant is in.
|
||||
- B6 resolved (D9c). I ran the equality check in node 26 (`url.origin + url.pathname === input`). These
|
||||
each fail it: a dot segment, `%2e%2e`, an upper-case host, `:443`, backslashes, userinfo, a query and a
|
||||
fragment. A trailing slash passes it, and so does `%41`. The two-segment rule catches the first and the
|
||||
name rule (no `%`) catches the second, so both refuse. The owner and the repository are read from the
|
||||
normalized path, so the owner that gets checked is the owner that gets used. The `.git` strip followed by
|
||||
the name rule refuses `.git` and `..git`. `remote-not-fork` compares without case. A sub-path Gitea refuses
|
||||
at load. The three new mutants cover the three ways this could regress.
|
||||
- N10 resolved. Load refuses a `publish` grant without `identity` or without `gitea`, with a fixed message,
|
||||
and has a T3 case for each.
|
||||
- N11 resolved. A non-zero `ls-remote` refuses `fork-unavailable` and its stderr isn't returned. A3 keeps
|
||||
push-to-create off for users and orgs, and the mutant that reads failure as empty is in.
|
||||
- r4 codes hold. The `pushCommit` binding, the push by oid, the `tls` seam and the
|
||||
`refs/heads/<gitea.user>/<branch>` destination are unchanged. N9 is carried to BUILD.md for the routing
|
||||
commit, which is the right place for it.
|
||||
|
||||
Code can start on this revision.
|
||||
@@ -1,69 +0,0 @@
|
||||
# Review: Megavolt row 199 design r1, per-agent credentials and remote push (duckworth-0013)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/megavolt/work/queue-199/DESIGN.md at 624e8ea3a on megavolt/row199 (sha256 3eb2df7c…1cd4,
|
||||
checked), base 992650232, Gitea #1697. Code read at 992650232.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
The shape is right. The path is derived from the cap, never named in a request. No `gitea` or `vikunja`
|
||||
token enters a sandbox. The destination ref is built by the host. `publish` is no-force. E1 to E10 match the
|
||||
code where I checked them. The launcher and its git socket run in the bus process (cli.mjs:504-506), so
|
||||
`useFor` from `publish` doesn't move a token across processes. Two things block.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. A `store` reference with no file stops the whole bus at start. D13 creates an agent "with `store`
|
||||
references and no files" and says `launch` then refuses `credential-unavailable`. `revoke` (section 6)
|
||||
unlinks the file and leaves the reference. But D1 keeps E2's reader, and that reader throws
|
||||
`credential-file` when the file is missing (credentials.mjs:38-43). It runs inside `startBroker`'s try
|
||||
(runtime.mjs:89), so the bus refuses to start. The fault doesn't show at the verb. It shows at the next
|
||||
restart, for every agent of the business. Row 194's fire calls revoke before it edits the business file
|
||||
(D13), so a failed edit after a good revoke leaves exactly this state.
|
||||
Fix: say what a `store` reference with no file means. I suggest: ENOENT on a `store` path loads as an
|
||||
absent entry, `status` shows `not-issued`, and `useFor` refuses `credential-unavailable`. Every other defect
|
||||
still refuses start: wrong owner or mode, a symlink, bad format, a dir problem. `{file}` and `{env}` stay as
|
||||
they are. T1 and T5 gain a bus restart after create-without-issue and after revoke. Add a mutant that makes
|
||||
ENOENT on a `store` path fatal again, and one that makes ENOENT on a `{file}` path silent.
|
||||
|
||||
B2. D9 step 5 doesn't bind the token to the remote's host. A one-shot helper that hands out the token on
|
||||
any `get` gives it to whatever host git asks about. Git follows a redirect on the first request
|
||||
(`http.followRedirects` defaults to `initial`) and then asks the helper for credentials for the new host.
|
||||
So a redirect from the remote, or from anything that answers for its name, sends the agent's
|
||||
`write:repository` token off the Gitea host. Separately, "E8's safe env" can't push to https at all:
|
||||
working.mjs:95 sets `protocol.allow=never` with only `file` allowed.
|
||||
Fix, in D9 step 5:
|
||||
- `publish` sets `protocol.https.allow=always` and nothing else beyond SAFE.
|
||||
- It sets `http.followRedirects=false`.
|
||||
- The helper answers `get` only when `protocol` is `https` and `host`, port included, equals the remote's.
|
||||
It answers nothing for `store` and `erase`.
|
||||
- Step 2's host check compares the port too.
|
||||
- T3 gains a fake remote that 302s to a second 127.0.0.1 port, where no basic auth may arrive.
|
||||
- Add a mutant whose helper answers any host.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Section 6 lets the tester run every lifecycle verb on the dev instance (Q173). Agents act as tester,
|
||||
so `issue` and `rotate` under the tester mean an agent session holds a live token on stdin. That cuts
|
||||
against section 7 ("Agents never mint accounts or tokens") and the no-credential-values rule. I suggest
|
||||
the tester gets `status` and `revoke` only, and `issue` and `rotate` stay with Jason. This is input to
|
||||
Q-M3.
|
||||
N2. The `issue` flag has to follow the kind. The reader takes `rotateBy` for `gitea` and `expires` for
|
||||
`vikunja` and refuses any other key (credentials.mjs:24-33). Say `--rotate-by` is gitea-only and
|
||||
`--expires` is vikunja-only.
|
||||
N3. The work ref is per task, `work/vikunja-<project>-<task>` (working.mjs:110-114), not per agent. "The
|
||||
agent's own work ref" (D9 step 3) holds only through step 4's identity check. Say so, or also check that
|
||||
the last `recordPush` for that ref was the caller's.
|
||||
N4. `publish` sends the working repo's `main` history from the project root along with the work. If the
|
||||
remote doesn't share root's history, the first publish uploads all of root, landing squashes included. Say
|
||||
that a project's `remote` must be root's upstream, and have R1 or T3 show a first publish against an empty
|
||||
fork.
|
||||
N5. E5 cites `git-socket.mjs:398-407`. It is `packages/cli/src/git-socket.mjs`. Name the host op that
|
||||
`publish` uses to reach `useFor`, since the socket today reaches the broker only through `host.op`.
|
||||
|
||||
## Q-M inputs for Mos
|
||||
|
||||
- Q-M1: O1 (forks). Gitea enforces it, and no per-agent rule can drift. O2 depends on probe P1, which hasn't run.
|
||||
- Q-M2: no objection. It's a reviewed `roles/` change, with nothing automatic.
|
||||
- Q-M3: human-only, plus N1's narrowing for the tester.
|
||||
- Q-M4: agreed. Per-agent model tokens need the proxy row.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Review: Gizmoduck row 201 FED R1 manifest, round 1 (duckworth-0033)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: gizmoduck/fed-r1 at 44e36fc4c, candidate 80fe31071,
|
||||
base ae3c36f24 (gizmoduck-0032). Against the FED R1 brief (docs/plans/2026-10-20_federation-r1-r2-r5.md at
|
||||
a2af7ab49) and DESIGN.md 3.5 at a1daa3c07.
|
||||
- Manifest digests checked, all three match: access.mjs 74ac7e9e…dc4f4, index.mjs 7ee59b6e…3d20,
|
||||
access.test.mjs 3bec097f…b238. 44e36fc4c only deletes a `__pycache__` .pyc.
|
||||
- I read access.mjs in full, the test file in full, BUILD.md, and util.mjs:66-89 (`readJsonFile`).
|
||||
- In a scratch git worktree at 80fe31071, `node --test packages/business/tests/access.test.mjs` passed 24 of
|
||||
24 with exit 0. I removed the worktree afterwards. I ran no suites and no mutants.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
Each brief test item maps to a test:
|
||||
|
||||
| Brief item | Test |
|
||||
|---|---|
|
||||
| symlink, non-regular file | access.test.mjs:216, :219 |
|
||||
| another owner | :224 |
|
||||
| group or other write | :225-228 |
|
||||
| expiry | :178-186, and covers :259-266 |
|
||||
| `*` | :154-156, :165 |
|
||||
| subjects (deviation 2) | :132-135 |
|
||||
| `human`, `tester`, any business's human | :111-115 |
|
||||
| any level other than collaborator | :171-176 |
|
||||
| project not a UUID | :151-153 |
|
||||
| covers: `*` and listed ids, unlisted project, instance or business, expired, level filter | :244-274 |
|
||||
|
||||
The file checks run on one descriptor opened with `O_NOFOLLOW | O_NONBLOCK`. `fstat` comes before the read
|
||||
(util.mjs:66-89), so there's no check-then-read race, and a FIFO is refused instead of blocking. Mode
|
||||
`& 0o022` and the uid compare match 3.5's first rule. The interface (`checkAccess`, `loadAccess`, `covers`,
|
||||
`LEVELS`, `accessFilePath`, and the context with `ssoSubjects` required) is the one Morgana confirmed
|
||||
(morgana-0014, 08bb4d827) and Sage accepted (sage-1399).
|
||||
|
||||
Deviation 1, a past `expires` loads and `covers` returns false: right. 3.5 says such a grant "is treated
|
||||
as absent", not refused, and sage-1399 confirms it.
|
||||
|
||||
Deviation 2: your reading is the one I meant. The brief's phrase is about the 3.5 subject rules: a
|
||||
`(provider, sub)` pair appears once in the file and never in `console.sso.subjects`. :132-135 cover a pair
|
||||
repeated within a user, a pair shared across users, an owner subject, and the same sub under another
|
||||
provider. The other reading, a grant naming a user that isn't in the file, is covered by your added rule at
|
||||
:145.
|
||||
|
||||
What remains is Sage's gate: the business suite, every `scripts/test-*.sh`, and M01-M44 each killed.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. `covers` with no `level` lets any level cover (access.mjs:177-185, test :271). That matches 3.5 ("when
|
||||
one is asked for"), and today it is harmless because `LEVELS` holds only collaborator. Once 193a adds
|
||||
`viewer`, any broker path that calls `covers` without `level` for a write verb lets a viewer grant send.
|
||||
For 193a (Morgana) and R3: the verb gate always passes `level`, and add a test that a viewer grant refuses
|
||||
`thread.send` and `thread.create`. Making `level` required would be an interface change, so that would go
|
||||
to Sage first.
|
||||
N2. `covers` relies on the grant having passed `checkAccess`. `projects[0] === "*"` is correct only
|
||||
because `*` must stand alone (:154-156). The doc comment says "a checked grant". R5 and 193a should only
|
||||
pass grants from `checkAccess` or `loadAccess` output, never ones they built themselves.
|
||||
@@ -1,91 +0,0 @@
|
||||
# Review: Quackerjack row 202 FED R5 manifest, round 1 (duckworth-0040)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: quackerjack/fed-r5, build 030809e15, manifest
|
||||
agents/quackerjack/work/queue-202/MANIFEST.md at 959bfb275, per quackerjack-0004. Against the FED R5 brief
|
||||
(docs/plans/2026-10-20_federation-r1-r2-r5.md) and DESIGN.md 3.5 at b93f098f0.
|
||||
- The six manifest hashes match the blobs at 030809e15. 959bfb275 adds MANIFEST.md only.
|
||||
- I read cli/src/access.mjs in full, the cli.mjs and scripts/mosaic diffs, the README section, BUILD.md,
|
||||
test 10, broker.mjs:375-378 and :2318-2322, client.mjs:20-45 and transport.mjs `refuseInsideAgent`.
|
||||
- In a scratch git worktree at 030809e15, `node --test tests/access.test.mjs` from packages/cli passed 18
|
||||
of 18 with exit 0.
|
||||
- One probe of my own, in scratch: an access.json at mode 0664, then `list` and `revoke` through
|
||||
`accessCommand`. Both throw a `BusinessError`, not a `CliError` (B1). Nothing was written and the lock
|
||||
was removed.
|
||||
- I removed the worktree and the probe. I ran no suites and no mutants.
|
||||
|
||||
## Verdict: changes, B1 only
|
||||
|
||||
Your four points:
|
||||
|
||||
1. Human proof: sound. `proveHuman` makes one `project.list` call through `humanTransport` before any read
|
||||
of the file, for all three verbs. The broker runs `#human` on it (broker.mjs:2319), which needs
|
||||
`s.human`, `via` `cli` and `outsideAgent` (:377). `refuseInsideAgent` runs first, and `--tester` refuses
|
||||
before the bus is asked. One correction to the reasoning is in N1.
|
||||
2. Lock: it does what BUILD.md says. `O_EXCL` create at 0600, the pid written after, removed in a
|
||||
`finally`, and a reclaim only on ESRCH. EPERM reads as alive, and an empty or non-pid lock refuses. So
|
||||
every doubt fails closed. I agree the reclaim race (limit 1) is acceptable for a human-only verb. It
|
||||
needs a crash plus two writers at once, and it's Sage's call. See N2 and N3.
|
||||
3. Write: `checkAccess` runs on the whole new document before `writeAtomic`. The temp file is created
|
||||
`wx` at 0600 in the same directory, written, fsynced and renamed. The revision is compared inside the
|
||||
lock. See N4.
|
||||
4. Bus finding: agreed, it's outside R5. A closed socket maps to `outcome-unknown` at client.mjs:32-33.
|
||||
Pinning it in test 10 is the right call. When row 196 changes it, this test fails and must be updated
|
||||
in that row. See N5.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. A bad access file, or a bad business file, escapes as an uncaught `BusinessError`. `readCurrent`
|
||||
calls `loadAccess` (access.mjs:78) for `list`, and inside the lock for `grant` and `revoke` (:203). Only
|
||||
the `checkAccess` call at :236-241 converts `BusinessError` to a `CliError`. `run` (cli.mjs:613-614)
|
||||
rethrows anything that isn't a `CliError`, so node prints a stack trace and exits 1. My probe: an
|
||||
access.json at mode 0664 gives `BusinessError: access file must not be writable by group or other` from
|
||||
both `list` and `revoke`. The same happens for a file owned by another uid, and for a hand edit that
|
||||
breaks any 3.5 rule. DESIGN.md 3.5 says "Jason may also edit the file by hand", so this is an expected
|
||||
path, not a corner case. `accessContextFor` (:44-57) has the same gap for `loadBusiness`. It also runs in
|
||||
cli.mjs before `accessCommand`, so a broken business file reaches an agent caller ahead of the agent
|
||||
refusal.
|
||||
Fix:
|
||||
- Convert `BusinessError` to `CliError(`refused: ${message}`, 2)` around `readCurrent` too. One wrapper
|
||||
round the whole verb body does it.
|
||||
- Build the context after `refuseInsideAgent`, e.g. pass `contextFor: () => accessContextFor(...)` and call
|
||||
it inside `accessCommand`.
|
||||
- Add one test: an access file at 0664 refuses `grant` with exit 2, and the file, the receipt and the lock
|
||||
are unchanged.
|
||||
The manifest's line "BusinessError is caught and re-thrown as exit 2" is then true.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The header comment (access.mjs:5-6) and README say the bus refusing a non-human shell is why "no agent
|
||||
and no tester can write". For a tester that's not quite right. `project.list` exempts testers
|
||||
(broker.mjs:2319, `if (!s.tester) this.#human(s)`). Testers are kept out because the CLI refuses
|
||||
`--tester` and always uses `humanTransport`. Say so in the comment. For R4, record that `access.reload`
|
||||
must call `#human` with no tester exemption, because that verb is the proof once R4 swaps it in.
|
||||
N2. The empty-lock window can go away. `bus host` already writes the pid to a temp file and hard-links it
|
||||
in as `host.lock` (packages/cli/README.md:297-299), so the lock never exists without a pid. With that
|
||||
pattern, the "empty lock, remove by hand" case disappears.
|
||||
N3. In the reclaim race, the loser's `finally` `unlinkSync(lock)` can remove the winner's lock. If its own
|
||||
lock was already taken, it throws ENOENT after the rename has happened, and no receipt is written.
|
||||
Using `rmSync(lock, { force: true })` or checking the pid before unlinking narrows that. It's part of
|
||||
limit 1, so Sage decides.
|
||||
N4. `writeAtomic` doesn't fsync the directory after `renameSync`, so a power cut can lose the rename. A
|
||||
failed write leaves the `.access-*.tmp` file behind. The receipt is appended after the lock is released
|
||||
(:246), so two writers' receipt lines can land in either order, and a crash between rename and append
|
||||
leaves a write with no receipt. Appending inside the lock, after the rename, keeps the log in revision
|
||||
order.
|
||||
N5. Test 10's `outcome-unknown` tells a human that the outcome is unknown when nothing was written. Until
|
||||
row 196 lands, the README could add: a refused proof shows as `outcome-unknown`, and no write happened.
|
||||
N6. R1's `loadAccess` checks that the file's uid matches the caller's, and the rename makes the caller
|
||||
the owner. So `mosaic access` has to run as the instance's uid, e.g. `mosaic` on .54 (Q181). Run as
|
||||
anyone else, every read refuses. The README should say which user runs it.
|
||||
N7. No verb adds or removes a `users` entry. `grant` needs the user already in the file (R1's rule), so a
|
||||
new user is a hand edit. "To cut a user off, revoke" removes that user's grants at one `--business` only.
|
||||
Say both in the help text. DESIGN.md 3.7 says "Removing the users from access.json acts at once", and the
|
||||
help should not read as if `revoke` does that.
|
||||
N8. When row 204 (193a) lands, `--level viewer` is accepted through `LEVELS` with no R5 change. The usage
|
||||
line, the help and the README name only `collaborator`. The row that lands second updates the text and
|
||||
adds a viewer grant test (my duckworth-0037 N1, carried in Morgana's BUILD.md).
|
||||
N9. `accessContextFor` calls `p.subjects.map` with no guard, so a provider with no `subjects` throws a
|
||||
`TypeError`. That goes away once R5 rebases onto row 192 and reads `console` through `checkConsole`, as
|
||||
BUILD.md says.
|
||||
|
||||
A round that changes only B1 and any notes taken gets a diff check, not a full re-read.
|
||||
@@ -1,46 +0,0 @@
|
||||
# Review: Quackerjack row 202 FED R5, round 2 (duckworth-0041)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: candidate 48a3fb96e on quackerjack/fed-r5, manifest
|
||||
agents/quackerjack/work/queue-202/MANIFEST.md at 7fd265033, per quackerjack-0005. Round 1 was
|
||||
REVIEW-r1.md (duckworth-0040, changes B1, on 030809e15).
|
||||
- The six manifest hashes match the blobs at 48a3fb96e. scripts/mosaic is unchanged since round 1.
|
||||
- I read the full diff 030809e15..48a3fb96e for packages/cli (access.mjs, cli.mjs, the test file and the
|
||||
README) and BUILD.md.
|
||||
- In a scratch git worktree at 48a3fb96e, from packages/cli, `tests/access.test.mjs` alone passed 21 of
|
||||
21 with exit 0. I removed the worktree. I ran no suites and no mutants.
|
||||
- No test pins a hash of `packages/cli/src/access.mjs` or reads it by path.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- B1 resolved. `accessCommand` runs `refuseInsideAgent`, then wraps the whole verb body, so a
|
||||
`BusinessError` from `loadAccess`, `loadBusiness` or `checkAccess` becomes `CliError("refused: …", 3)`.
|
||||
The exit code is 3 under Sage's ruling (sage-1437). The new test sets a 0664 file and asserts exit 3,
|
||||
the path in the message, and the file, receipt and lock unchanged.
|
||||
- The context is now `contextFor()`, called after the `--tester` refusal and the human proof. The new
|
||||
test makes `contextFor` throw and the file 0664, and both the agent and the `--tester` caller get their
|
||||
own refusal first.
|
||||
- The lock no longer reclaims. A dead-pid, empty or non-pid lock refuses and names the path, and the
|
||||
dead-pid case names the pid. With no reclaim, the `finally` unlink only ever removes the caller's own
|
||||
lock, so my N3 is gone too. That trades the race for a manual clear after a crash, which is the
|
||||
fail-closed choice, and Sage made it (sage-1429).
|
||||
- `writeAtomic` removes its temp file on any failure up to the rename. The test injects a throwing
|
||||
`renameSync` through `deps.fs` and checks the old bytes, no `.access-*.tmp`, no lock and no receipt.
|
||||
M11, the direct-write mutant, is now killable.
|
||||
- `deps.fs` undefined falls back to the real functions through the default parameter, in both
|
||||
`writeAtomic` and `withLock`.
|
||||
- N6 taken in the help text and the README.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Exit codes are now mixed. Load and check refusals are 3. `--level takes …`, `project-not-found` and
|
||||
"no grant … to revoke" stay 2. If that's intended, say so in the README. If not, it's a small follow-up.
|
||||
Sage's call.
|
||||
N2. The receipt is still appended after the lock is released (round 1 N4, second part). Receipt lines
|
||||
can land out of revision order when two writes run back to back. A crash between the rename and the
|
||||
append leaves a write with no receipt. Not required by the brief.
|
||||
N3. `writeAtomic`'s catch unlinks `tmp` even when `openSync(tmp, "wx")` failed with EEXIST, i.e. a temp
|
||||
name that belongs to someone else. With 8 random bytes this won't happen in practice.
|
||||
N4. Round 1 N1, N5, N7, N8 and N9 carry as you said. N1 is already in the R4 brief line of the
|
||||
manifest.
|
||||
|
||||
What remains is Sage's gate: the cli suite, every `scripts/test-*.sh`, and M1-M13 each killed.
|
||||
@@ -1,42 +0,0 @@
|
||||
# Review: Morgana row 204 (193a) manifest, round 1 (duckworth-0037)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: morgana/row193a at 0caf6e2f6, base 44e36fc4c (row 201
|
||||
FED R1 candidate tree), per morgana-0018. Against design `agents/morgana/work/queue-193/DESIGN.md` r5 4.2,
|
||||
my R1 N1 (duckworth-0033) and sage-1420.
|
||||
- Manifest digest d38de8c9…f75a checked, and its three file hashes match the tree: access.mjs 0c1d9a12…b2ae,
|
||||
access.test.mjs 11dc4dd3…698c, access-levels.test.mjs a0656806…d52a.
|
||||
- I read the full diff 44e36fc4c..0caf6e2f6, BUILD.md, mutants.py and mutants.log.
|
||||
- In a scratch git worktree at 0caf6e2f6, from packages/business, each file alone with the tap reporter:
|
||||
access-levels.test.mjs 5 of 5, access.test.mjs 26 of 26, both exit 0. I removed the worktree. I ran no
|
||||
suites and no mutants.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- `LEVELS` is `["collaborator", "viewer"]`, most to least, and the comment at access.mjs:13-15 says the
|
||||
order matters.
|
||||
- `covers` (access.mjs:180-188) throws a `TypeError` for a missing or unknown `level` after the `now`
|
||||
check. That resolves my R1 N1 the way sage-1420 item 2 ruled.
|
||||
- The least-level compare is right. A grant covers when its index is at or below the asked index, so a
|
||||
viewer grant (1) fails a collaborator ask (0), and any grant passes a viewer ask.
|
||||
- The `!LEVELS.includes(grant.level)` guard at :188 is needed. Without it an unknown grant level gives
|
||||
`indexOf` -1, which would pass every ask. Mutant A6 drops it and access.test.mjs test 25 catches that.
|
||||
- `checkAccess`'s refusal path is unchanged apart from the list in its message. "Viewer", "viewer " and
|
||||
`["viewer"]` refuse.
|
||||
- The tests carry the design's matrix, the missing-level throw, an unknown grant level, and a viewer grant
|
||||
that still needs business, project, instance and expiry.
|
||||
- No other package calls `covers` (grep over packages at 0caf6e2f6), and no test pins a hash of either
|
||||
source or test path.
|
||||
|
||||
What remains is Sage's gate: the business suite, every `scripts/test-*.sh`, and A1-A6 each killed on the
|
||||
rebased tree.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. R5 (row 202, quackerjack/fed-r5 at 959bfb275) imports `LEVELS` for `access grant --level`. Once both
|
||||
land, `--level viewer` is accepted with no R5 change. R5's usage line and README still name only
|
||||
`collaborator`. Whichever row lands second should update that text and add one grant test with `viewer`.
|
||||
R5 doesn't call `covers`.
|
||||
N2. On the rebased tree, R1's M43 anchor no longer exists, as BUILD.md says. If Sage's gate for 193a
|
||||
reruns R1's mutants, M43 needs the new anchor or it aborts on an anchor count of 0.
|
||||
N3. BUILD.md's mutant table labels A5b "no level means viewer", while mutants.py and the log say "means
|
||||
any". The two are the same mutant, since every grant covers a viewer ask. Doc-only.
|
||||
@@ -1,91 +0,0 @@
|
||||
# Review: Gizmoduck row 210 round 1 code, host.run with sudo through the credential store (duckworth-0052)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0052, which moved it from gizmoduck-0049:
|
||||
- 12ffe0b56 on `gizmoduck/row210-on-199-r2`, base 07141966e (row 199's tip, megavolt-0027).
|
||||
- `git range-diff addee664a..8ec923585 07141966e..12ffe0b56`: the DESIGN commits and the code commit
|
||||
c7958740a are `=`. 12ffe0b56 adds only BUILD.md and manifest lines.
|
||||
- `candidate-manifest.sha256` (sha256 82b701e7…): 20 files, all OK in my worktree. It lists exactly the
|
||||
non-packet files in `07141966e..12ffe0b56`.
|
||||
|
||||
What I read in full: `bus/src/host-run.mjs`, `business/src/hosts.mjs`, and the diffs of `broker.mjs`,
|
||||
`bus/src/credentials.mjs`, `credential-verbs.mjs`, `runtime.mjs`, `server.mjs`, `bus/src/business.mjs`,
|
||||
`business/src/business.mjs`, `credentials.mjs`, `resolve.mjs`, `role.mjs` and `vocabulary.mjs`. I read
|
||||
T9 and the test titles against DESIGN r3 D3-D9 and section 9.
|
||||
|
||||
## Checks I ran
|
||||
|
||||
- The 17 touched test files, each alone, in a scratch worktree at 12ffe0b56 with TMPDIR under my own scratch
|
||||
dir: 134 pass, 0 fail, from an awk sum over the TAP log. This matches gizmoduck-0052.
|
||||
- `host-run.test.mjs` alone again: 18 tests, 0 skipped. The scratch sshd ran.
|
||||
- After the runs, no sshd, ssh or ssh-agent of mine was left, and no `h-` dir was left.
|
||||
- Hash pins: no test line pairs a changed file's name with a 64-hex value.
|
||||
- I didn't run the suites or the 47 mutants. They need Sage's slot.
|
||||
|
||||
## Verdict: changes, B1
|
||||
|
||||
The design holds in the code:
|
||||
- D4 binding. `hostRun` is `{host, argvSha256, stdinSha256}` at the raise (broker.mjs `hostRun`). It's
|
||||
compared whole in `#authorityFor`. The broker appends the question text, and the 4096 cap applies to all
|
||||
of it. T9 covers each mismatch, consumed, unapproved, and the full stdin in the question.
|
||||
- D5 order. Busy is checked before `#consumeAuthority`, so a busy refusal leaves the decision usable
|
||||
(deviation 5). From the broker's `busy` check to `#inflight.set` in `use()` there's no await, so two
|
||||
calls can't both pass.
|
||||
- D6. The quoting is POSIX single-quote, with `sudo -n --` first. T10's argv list covers `$( )`, a
|
||||
backtick, `;`, a newline and glob characters. The prefix match is literal. `refusedCommand` takes the
|
||||
basename. The digits-and-dots and hex-label refusals follow my r3 note.
|
||||
- D7. ssh and ssh-agent each lead a group under `setpriv --pdeathsig KILL`. The options are all on argv
|
||||
with `-F /dev/null`, and known_hosts is pinned per call. Field 22 of `/proc/<pid>/stat` is counted after
|
||||
the last `)`. The sweep keeps a dir whose bus pid and start time are alive, kills only a live recorded
|
||||
pid whose cmdline names the dir, and refuses start on any stray entry.
|
||||
- D8 and D9. The needles cover the seed at the three base64 offsets and in hex. Revoke aborts the
|
||||
controller, and `use()` answers `credential-unavailable` on an aborted signal.
|
||||
- The kinds are closed through `credentialKind`. `resolveInstance` drops `host.*` from the session, and
|
||||
the bus puts it back on the bus side only (deviation 1).
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. The ssh tests skip and pass when sshd is missing, but their skip text says the gate fails.
|
||||
- `host-run.test.mjs:22`: `skip = existsSync(SSHD) ? false : 'no /usr/bin/sshd: the gate fails on this skip'`.
|
||||
14 of the 18 tests carry `{ skip }`. That's T4 to T22 and three of the four T27 cases.
|
||||
- `node --test` exits 0 on skipped tests, and no `scripts/test-*.sh` counts skips. I grepped for `skip`. The
|
||||
only hits are each script's own docker-unavailable skip lines.
|
||||
- So on a gate machine without `/usr/bin/sshd`, the bus suite reports green and runs none of the D4-D9
|
||||
executor tests. That's the false-pass shape, and the text claims the opposite.
|
||||
|
||||
Fix, either way:
|
||||
- Make a missing sshd a failure, for example a first test that asserts `existsSync(SSHD)`, with no skip on
|
||||
the rest.
|
||||
- Or keep the skip, change the text so it doesn't claim a failure, and have the gate check `# skipped 0`
|
||||
for this file.
|
||||
|
||||
Add a mutant or a one-off check with `SSHD` pointed at a missing path, so the gate's answer is shown.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. A `host.sudo` raise checks only that the host is in the registry. It doesn't check that the raising
|
||||
role has a grant for that host, or that the argv matches a prefix. T9 raises and approves for `web2`
|
||||
(`host-run.test.mjs:235`), which that role isn't granted (`:209-211` refuses `host-not-granted`). Jason can
|
||||
be asked to approve a call that can never run. That widens nothing, because the call checks the grant.
|
||||
Checking `roles[s.role].hosts[target]` and `commandAllowed` at the raise would keep those questions out
|
||||
of his queue. Refuse with the same codes as the call.
|
||||
|
||||
N2. `addKey` (`host-run.mjs:84-92`) has no time limit and doesn't listen to the abort signal. If ssh-add
|
||||
never exits, `runHost` never returns. `#inflight` keeps the entry, because its delete is in `use()`'s
|
||||
`finally`. Every later call for that agent and host then refuses `host-busy` until the bus restarts.
|
||||
Revoke aborts the controller but can't clear the entry. ssh-add talks to a local agent on a private
|
||||
socket, so this is unlikely. Give it the `agentWaitMs` limit, or kill its group on abort.
|
||||
|
||||
N3. `host-key-mismatch` (`:177`) is chosen from exit 255 plus the stderr text. A remote command that exits
|
||||
255 and prints that line gets the same answer, and its stdout is lost. Only a command Jason approved can
|
||||
do that, so this is a reporting matter. Say so in the D6 exit-255 residual, or match the text only at the
|
||||
start of stderr.
|
||||
|
||||
N4. Deviation 3's wider runner list and deviation 11's T3 at the broker are fine. M25's CRLF mutant is
|
||||
equivalent, as BUILD.md says. With `\r` refused at :27, the line regex (:29) refuses it anyway.
|
||||
|
||||
N5. `business/tests/helpers.mjs:14` makes `mosaic-business-*` dirs that no test removes. One run of the
|
||||
17 files left 377 under TMPDIR, and `hosts.test.mjs` alone left 111. The helper is older than this row.
|
||||
It's a follow-up, not this row's to fix.
|
||||
|
||||
A round that changes only `host-run.test.mjs`'s skip, the mutant or check for B1, and these notes gets a
|
||||
diff check.
|
||||
@@ -1,36 +0,0 @@
|
||||
# Diff check: Gizmoduck row 210 round 2 code, for duckworth-0052 (duckworth-0053)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0054:
|
||||
- 455964f23 on `gizmoduck/row210-on-199-r2`, one commit on 12ffe0b56. The base is still 07141966e.
|
||||
- The range is `12ffe0b56..455964f23`: 7 files, +163 -40. I read the whole code and test diff.
|
||||
- `candidate-manifest.sha256`: all 20 files OK in my worktree, and it lists exactly the non-packet files in
|
||||
`07141966e..455964f23`.
|
||||
|
||||
## Checks I ran
|
||||
|
||||
- The 17 touched test files, each alone, in a scratch worktree at 455964f23: 137 pass, 0 fail, 0 skipped,
|
||||
from an awk sum over the TAP log. `host-run.test.mjs` ran 21 tests, all passing, with none skipped.
|
||||
- B1 by hand. In my copy I set `SSHD` to `/nonexistent/sshd`, with a backup, and checked the sha on restore.
|
||||
The file exits 1 with 21 tests: 8 pass, 13 fail, 0 skipped. That matches gizmoduck-0054.
|
||||
- After the runs, no sshd or ssh-agent of mine was left.
|
||||
- I didn't run the suites or the 53 mutants. They need Sage's slot.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- B1 is met. The `{ skip }` guard is gone. A first test asserts each binary, and `sshd()` throws when
|
||||
`SSHD` is missing. A missing sshd now fails the file, and the gate sees it.
|
||||
- N1 is taken. The raise runs host.run's grant and prefix check (`broker.mjs:1964-1967`). T5-T8 count 0
|
||||
`host.sudo` decisions after both refusals. T9 grants `web2` through `coderWeb2`. The `hard-waits.test.mjs`
|
||||
fixture change follows from N1 and only adds a grant.
|
||||
- N2 is taken. `addKey` kills ssh-add's group at `limitMs` (`host-failed`) or on abort
|
||||
(`credential-unavailable`). `runHost` passes `agentWaitMs` and the call's signal. The test's silent socket
|
||||
sees both connections close.
|
||||
- N3 is taken. `-E <dir>/ssh.log` keeps ssh's own messages apart from the remote command's stderr, and the pin
|
||||
refusal reads only that log on exit 255. The new test covers both a remote `fail255` and a closed port.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N1. On an exit 255, `out.stderr += log` can take stderr up to 4096 characters past `outputMax`, and
|
||||
`truncated.stderr` stays false. That's bounded and only ssh's own text. Leave it or cap the sum.
|
||||
|
||||
The suites and the mutant batch, M36-M38 included, are Sage's gate.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Row 210 queue round 3: rebase check (duckworth-0078)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1658 and sage-1661 (request rev 2612/2613,
|
||||
Gitea comment 29849) and in gizmoduck-0094. Candidate: `gizmoduck/row210-r5`, tip 0485384e9 on
|
||||
ad95c888c, manifest sha256 688d38b96b3de80f7bbbca857130ef36f68ec65009a3df51c5084aef7cb96788. Scope is a
|
||||
rebase only (sage-1657). My round 2 approve (rev 2592) was on c43008749, on cb5fdb064.
|
||||
|
||||
## Verdict: approve, no findings
|
||||
|
||||
1. broker.mjs is a clean 3-way merge. I ran `git merge-file` myself with ours c43008749, base cb5fdb064
|
||||
and theirs ad95c888c. It gave 0 conflicts and sha256 610af0c4207bbd805a2a8826daa32b8cdb423937f793c32fd4241f6addfabfda,
|
||||
which equals r5's broker.mjs.
|
||||
- `git diff c43008749 0485384e9 -- broker.mjs` is exactly row 237's 3-line hunk, after :1055.
|
||||
- `git diff d7d943199 0485384e9` on broker.mjs equals `git diff cb5fdb064 c43008749`, ignoring
|
||||
index and hunk-header lines. So 210's broker change carries over unchanged.
|
||||
2. Across the 23 manifest paths, cb5fdb064..ad95c888c changes only broker.mjs. Every one of the 23
|
||||
hashes in the r5 manifest matches the r5 tree. The manifest differs from r4 only in the broker line,
|
||||
and its sha256 is 688d38b9…6788, which matches the request.
|
||||
3. The rebase is the nine r4 commits, all `=` in range-diff, plus one commit that touches only
|
||||
BUILD.md and the manifest. c43008749..0485384e9 adds no 210 path beyond the broker merge.
|
||||
DESIGN.md and mutants.py are unchanged.
|
||||
4. No interaction. 210's broker diff has no line with checkPublish, #publishes, publish-busy,
|
||||
recordPublish, #publishFacts, #checkAuthority or git.push. 237's check sits in checkPublish and adds
|
||||
no imports.
|
||||
|
||||
My runs, single files in a detached worktree of 0485384e9:
|
||||
- bus: publish 13/0, host-run 21/0, hard-waits 13/0, project-runtime 3/0.
|
||||
- cli: publish-socket 8/0, credentials 5/0.
|
||||
- business: hard-waits 5/0, hosts 7/0.
|
||||
- seat sandbox gave 7/3, and the base ad95c888c gives the same 7/3 in my session. It's environmental,
|
||||
as seen before. Gizmoduck reports 10/0.
|
||||
|
||||
Sage's mutants and full gate on this tree are still to come.
|
||||
@@ -1,103 +0,0 @@
|
||||
# Review: Gizmoduck row 210 (#1707) DESIGN.md r1, agent host access with sudo (duckworth-0042)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: agents/gizmoduck/work/queue-210/DESIGN.md at 7b10bc0ff
|
||||
on gizmoduck/row210, blob 1514a36d96 (checked), base refactor 9ba31b5b3, per gizmoduck-0041. Against the
|
||||
brief docs/plans/2026-10-20_host-credentials.md (9db578ff5), REQ-CRED-3 in Honker's 1.7 draft r2 (approved
|
||||
duckworth-0039), row 199 design r6 (4d3007ec9) and row 199's code on origin/megavolt/row199 at addee664a.
|
||||
- I checked E1-E11 at 9ba31b5b3: credentials.mjs:22, :84-87, :95-103, :104-117, host.mjs:31 and :270,
|
||||
broker.mjs:145-147, :790-797 and :1490-1532, server.mjs:87-95, business.mjs:89, vocabulary.mjs:101-103,
|
||||
sandbox.mjs:29-31, instance-settings.mjs:27. They hold. E12 is stale, see N1.
|
||||
- 199's `useFor` at addee664a (credentials.mjs:228-256) passes a BusError through and maps anything else to
|
||||
`service-failed`, so D5's own codes (`host-key-mismatch`, `credential-leak`) reach the caller as designed.
|
||||
- I ran nothing. The probe in section 2 is the author's, and I didn't repeat it.
|
||||
|
||||
## Verdict: changes, B1-B4
|
||||
|
||||
What holds:
|
||||
- The four grant layers, checked on every call, meet REQ-CRED-3's "a grant exists for that agent and that
|
||||
host before the credential is used". The agent never names another agent: the key comes from the cap
|
||||
through `useFor`.
|
||||
- No agent and not the chief can write layers 2-4. `hosts` is outside ORGANIZATION_KEYS and INSTANCE_KEYS,
|
||||
and `instances.set` refuses credentialed definitions.
|
||||
- `host.sudo` lands as a hard wait under REQ-WAIT-3, and T25 enforces REQ-WAIT-2.
|
||||
- The key handoff keeps the key off disk, argv and env (T19). `-F /dev/null`, a bus-written known_hosts
|
||||
under `HostKeyAlias` and `StrictHostKeyChecking=yes` give no trust on first use. Forwarding is off and
|
||||
`restrict` is on the host side.
|
||||
- The command is built from argv with POSIX single quotes and `sudo -n --`, never taken as a string.
|
||||
- The executor runs in the bus child as a broker.sock verb. The 10 s serial IPC (E5) is a sound reason not
|
||||
to use a host op. It matches where 199 put the push (199 BUILD.md deviation 11), see N1.
|
||||
- The brief's two named mutants are there: pin skip (M12, M13) and key leak (M15, M16).
|
||||
- Section 6 states the limits plainly, including that `"any"` is root and can outlive the grant.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. The event log doesn't say what ran as root. D5 step 6 records `action.allowed` with "host, argv hash,
|
||||
stdin size and hash", and T22 checks that the bodies carry hashes. After routing, a within-role call has no
|
||||
decision, so no record anywhere in the stack names the command, only its sha256. The existing verbs log the
|
||||
thing done in clear: `requestWeb` records the url or query (broker.mjs:1521), and 1.7's push line says
|
||||
"Every push is logged with the remote, the ref, the commit and, when one was given, the decision" (draft
|
||||
:105). argv has already passed `#secretCheck` (step 1), so it holds no known secret, and it's capped at
|
||||
32 KiB. The design gives no reason to hash it. Fix: record `host`, `user` and `argv` in clear in the
|
||||
`requested` event, and keep output as sizes and hashes. Change T22 to assert argv is present and stdout,
|
||||
stderr and the key are not, and M24's partner: a mutant that drops argv from the event.
|
||||
|
||||
B2. A pre-routing decision binds stdin that Jason never sees. D4 binds `stdinSha256`, and the question
|
||||
shows only "the host id and the argv". `stdin` can be 64 KiB (D6). So `argv: ["bash"]` with a script on
|
||||
stdin shows Jason the question "… bash", and he approves a script he can't read. Binding a hash he can't
|
||||
check doesn't give him a decision about it. Fix, either:
|
||||
- the question shows stdin in full when present (it passed `#secretCheck`), or
|
||||
- a gated call refuses non-empty stdin, and stdin is allowed only for a routed within-role call.
|
||||
Add a T9 case for the chosen rule and a mutant that drops it.
|
||||
|
||||
B3. If the bus child dies, the key stays loaded and the call can't be revoked. D7 step 5 cleans up in a
|
||||
`finally`, and T18 covers success, refusal, timeout and a thrown error. A bus child that is SIGKILLed or
|
||||
crashes never runs the `finally`. `ssh-agent -D` has no lifetime of its own, so it keeps the key loaded on
|
||||
its socket until the machine reboots. A detached ssh keeps running the remote command. After the restart
|
||||
the new bus's kill map is empty, so D9's "revoke also kills a `host.run` in flight" no longer holds for
|
||||
that call. 199's push leans on git's own time limit for the same case (deviation 11, "git's own time limit
|
||||
ends it"). ssh-agent has no such limit unless one is given. Fix:
|
||||
- `ssh-add -t <timeout + grace>`, so the agent drops the key on its own.
|
||||
- At bus start, sweep `<configDir>/secrets/run/`: kill what each leftover dir's recorded pids name (check
|
||||
they are still that ssh-agent or ssh first) and remove the dir.
|
||||
- Consider `setpriv --pdeathsig SIGKILL` around ssh-agent and ssh, which the sandbox already uses
|
||||
(sandbox.mjs:25-27). Probe it from node first.
|
||||
Add a test that SIGKILLs the bus child mid-call and then asserts the key is gone from the agent by the
|
||||
bound and the next start leaves no run dir. Add a mutant that drops `-t`.
|
||||
|
||||
B4. Nothing routes section 4 to Jason. REQ-CRED-3 says "Who grants a host credential, and how, is set by
|
||||
the #424 design, which Jason approves", and section 4 ends "The table above is that answer". Section 10's
|
||||
only Jason question, Q-J1, is about the first grants at the live window. My approval of the design doesn't
|
||||
stand in for his. Fix: add a Q-J0 to section 10, sent through Mos before the build lands: approve the
|
||||
section 4 table. Name where his answer is recorded.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. 199 is on origin now: origin/megavolt/row199 at addee664a carries code and BUILD.md. The header's "199
|
||||
has no code on origin yet" and E12 are stale. The bus child already spawns git there: runtime.mjs:245 calls
|
||||
`publishRemote`, which runs `runGit` (seat/src/checkout.mjs:95-138) as a detached group leader with
|
||||
SIGTERM, a grace period and SIGKILL to the group. Reuse that shape, or the helper, for ssh rather than
|
||||
writing a second one. Q-S2: deviation 11 keeps the token and the push in the bus child, and the host side
|
||||
only polls. D5's assumption holds, and since the agent reaches broker.sock directly, 210 needs no poll.
|
||||
Sage confirms.
|
||||
N2. Section 6 should say that a registry entry for the stack's own machine, or any host where the stack's
|
||||
`<configDir>` lives, gives the agent with `"any"` every agent's keys and tokens, not just root there.
|
||||
Consider refusing a loopback `address` at load.
|
||||
N3. D5 says every spawn is `detached: true`, so each gets its own group. D7 step 3 says ssh-agent runs "in
|
||||
the same process group". Say which group ssh-agent is in, and make sure the timeout and revoke kill reach
|
||||
both the agent and ssh.
|
||||
N4. Prefix lists plus stdin: `[["tee"]]` or `[["dd"]]` writes any file as root from stdin. D6 already says
|
||||
a prefix list limits mistakes, not a determined agent. Say in the doc that stdin goes to the command, so
|
||||
some prefixes are root on their own.
|
||||
N5. Pass the user with `-l <user>` and the address as its own argument rather than `<user>@<address>`.
|
||||
Then nothing depends on how ssh splits on `@`, or on an IPv6 literal.
|
||||
N6. D8 is defense in depth. The remote never holds the private key, and forwarding is off, so output can
|
||||
carry the key only if the agent already had it. It's still right to keep. Also note that a `host.*` entry
|
||||
in the shared token map makes today's `assertClean` loop over a multi-line value that never matches
|
||||
(E3), which is harmless.
|
||||
N7. My #423 r5 condition (duckworth-0035) applies. If ops-01 changes step 1 to fit this design, for
|
||||
example per-agent `from=…,restrict` lines for localadmin, that changed step 1 comes to me as a diff before
|
||||
it runs. That's the live part, not this row.
|
||||
N8. Q-S1 (route `host.sudo` in 1.7) is Sage's and Jason's call. Without it, B2's rule matters on every
|
||||
call.
|
||||
|
||||
A round with only B1-B4 and any notes taken gets a diff check, not a full re-read.
|
||||
@@ -1,66 +0,0 @@
|
||||
# Review: Gizmoduck row 210 (#1707) DESIGN.md r2, diff check (duckworth-0043)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: DESIGN.md at b726e10f4 on gizmoduck/row210, blob
|
||||
8c6c6a8c5e (checked), per gizmoduck-0044. Diff 7b10bc0ff..b726e10f4, +153 -60, one file. I read the
|
||||
whole diff, including sage-1450's D6 change, which goes past my B items. Round 1 was REVIEW-r1.md
|
||||
(duckworth-0042). I checked the new cites at 9ba31b5b3:
|
||||
- broker.mjs:126 `text()` caps a string at 4096, and :1654 runs `string(a.question)` on the raise.
|
||||
- sandbox.mjs:44 is `KILL_WITH_PARENT = ["/usr/bin/setpriv", "--pdeathsig", "KILL", "--"]`.
|
||||
- E12 and 199's cites at addee664a match what I read in round 1.
|
||||
I ran nothing.
|
||||
|
||||
## Verdict: approve on b726e10f4, with one condition (C1)
|
||||
|
||||
- B1 resolved. `requested` and `completed` carry host, address, user and argv in clear, after
|
||||
`#secretCheck`. stdin and output stay as size and sha256. T22 asserts both sides, and M30 drops argv.
|
||||
- B2 resolved. The broker appends host, user, JSON-quoted argv and the full stdin to the question itself,
|
||||
inside the existing 4096 cap. A raise that doesn't fit refuses `invalid-request`. T9 checks the stored
|
||||
question and the cap, and M31 omits stdin. With sage-1455 (every call a Jason decision under 1.7, per
|
||||
mos-1086), this was the rule that mattered most, and it now holds.
|
||||
- B3 resolved.
|
||||
- `setpriv --pdeathsig KILL --`, the sandbox's own prefix, wraps every spawn.
|
||||
- `ssh-add -t <timeout+30>` bounds the key even if the agent survives.
|
||||
- A pids file and a sweep before `broker.sock` listens. The sweep kills only a pid whose cmdline names
|
||||
the dir, and leaves alone a dir whose bus pid is alive, which also covers two buses sharing
|
||||
`<configDir>`.
|
||||
- T27 covers all three, and M32-M34 drop each one.
|
||||
- B4 resolved. Q-J0 is in section 10 in standalone form, routed through Mos before landing, and recorded in
|
||||
DECISIONS.md. See C1.
|
||||
- sage-1450's D6 holds together. There's no wildcard grant. Load refuses a string, empty, duplicate and
|
||||
glob-character `commands`, and a shell or runner as `argv[0]` by basename (so `/bin/bash` too). T26 and
|
||||
M6, M28 and M29 cover each one. Section 6's known limit names what closes it (`bwrap --unshare-net`) and
|
||||
doesn't claim the allowlist closes it.
|
||||
- Notes N1-N7 were taken as asked.
|
||||
|
||||
## Condition
|
||||
|
||||
C1. Q-J0 overstates revoke. Its line "You, Mos or the tester can revoke at once. The stack drops the key
|
||||
and stops a running command" goes to Jason word for word. Section 6 says "Revoke stops the stack's path,
|
||||
not remote processes. Killing ssh may leave a remote process running." He should approve on the accurate
|
||||
version. Replace the line with something like: "You, Mos or the tester can revoke at once. The stack drops
|
||||
the key and cuts its connection. A command already started on the machine may keep running." That's a
|
||||
one-line change that needs only my diff check, and it must be in before Mos sends Q-J0.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The runner list is open by design (D6 says so), and it's Sage's ruling. Commands that run another
|
||||
command as root as directly as `env` or `nohup` do: `systemd-run`, `runuser`, `chroot`, `nsenter`,
|
||||
`unshare`, `setpriv`, `capsh`, `doas`, `pkexec`, `flock`, `stdbuf`, `ionice`, `chrt`, `taskset`, `strace`,
|
||||
`script`, `watch`, and the interpreters (`python3`, `perl`, `ruby`, `node`, `awk`). Adding them costs one
|
||||
array line each. `systemd-run` is the one I'd add first.
|
||||
N2. The loopback check names `127.0.0.0/8`, `::1` and `localhost`. getaddrinfo also takes these to the
|
||||
local machine: `0.0.0.0` (and `0`), `::`, `::ffff:127.0.0.1`, short or numeric IPv4 forms such as
|
||||
`127.1` or `2130706433`, which pass the DNS-name regex, and `*.localhost`, which systemd-resolved maps to
|
||||
loopback. Refuse a name made only of digits and dots unless it's a full four-part IPv4 literal, and run
|
||||
the address check on `net.isIP` output. Section 6 already says load can't know every address of the
|
||||
stack's machine, so this narrows the gap without closing it.
|
||||
N3. The sweep trusts that a live pid is still the bus that wrote the file. A reused pid leaves that dir in
|
||||
place until the next start. The cmdline check already stops a wrong kill. Recording each pid's start time
|
||||
(`/proc/<pid>/stat` field 22) beside it removes the reuse case. Low stakes, because `-t` and pdeathsig
|
||||
have already dropped the key.
|
||||
N4. T26 is listed after T27, and M28/M29 after M35. Cosmetic.
|
||||
N5. On a gated call the 4096 cap counts the agent's own question, the JSON-quoted argv and the stdin, so
|
||||
stdin's real room is smaller than 4 KiB. D4 says this. The tool help should say it too.
|
||||
|
||||
Q-S3 (the host_run harness tool) is Sage's call. What remains: C1, then the build, manifest review and
|
||||
Sage's gate. The row lands after 199 and after the PRD version holding REQ-CRED-3.
|
||||
@@ -1,35 +0,0 @@
|
||||
# Review: Gizmoduck row 210 DESIGN r3, C1 diff check (duckworth-0044)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: gizmoduck/row210 at e021dfa26, DESIGN.md blob
|
||||
ece332e306 (checked), per gizmoduck-0047. e021dfa26 is a fast-forward of b726e10f4, and the diff touches
|
||||
DESIGN.md only (+51 -26). I read the whole diff.
|
||||
|
||||
## Verdict: C1 met, approve stands
|
||||
|
||||
C1. Q-J0 now says "The stack drops the key and cuts its connection. A command already started on the machine
|
||||
may keep running." That matches section 6 and D9's revoke line. The quote is still 10 lines, and the yes/no
|
||||
question is last. Dropping "on the machine" from the ops-01 sentence loses nothing. Mos can send Q-J0 as it
|
||||
stands at e021dfa26.
|
||||
|
||||
The rest of r3 matches the rulings and my r2 notes:
|
||||
- Q-S3 (sage-1468): the `host_run` row is gone from D10, and the scope line says no `packages/harness` file
|
||||
changes. My r2 N5 goes to Sage's follow-up row.
|
||||
- N1 (sage-1473): D6 carries the whole runner list, five interpreters, and the line that the list can't be
|
||||
complete and Jason's per-call decision is the control. T26 has one case per name.
|
||||
- N2: I checked `net.isIP` on node here. It returns 0 for `127.1`, `2130706433`, `0177.0.0.1`,
|
||||
`127.000.000.001` and `127.0.0.01`, and 4 for `10.1.1.53`. glibc resolves `0177.0.0.1` and `127.0.0.01`
|
||||
to 127.0.0.1. The rule "a name of only digits and dots refuses unless `net.isIP` reads it as IPv4"
|
||||
refuses all of them, octal included.
|
||||
- N3: the pids file records the start time (field 22), the sweep treats a mismatch as dead, and T27 and M34
|
||||
test it.
|
||||
- N4: M28 and M29 are in order.
|
||||
- The 1.7 approval line and the Q-S1 line agree with sage-1468.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. D6's interpreter list has `python3` but not `python`, `python3.12` or other versioned names, and has
|
||||
`awk` but not `gawk`, `mawk` or `nawk`. `sed` (GNU `e` command), `vi`/`vim`, `less` and `php` also start a
|
||||
shell. D6 already says the list isn't the control, so these are cheap additions to T26 if Gizmoduck wants
|
||||
them, not a requirement. A basename match on `python3*` and `*awk` would cover the versioned forms.
|
||||
|
||||
The build can start on 199's candidate.
|
||||
@@ -1,48 +0,0 @@
|
||||
# Review: row 210 round 4 diff, the B1 import fix (duckworth-0071)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Subject, per gizmoduck-0089 and sage-1619:
|
||||
`gizmoduck/row210-r3`, diff 3cf63efb4..c43008749, one commit, 9 files. Base cb5fdb064.
|
||||
|
||||
I approved rounds 1 and 2 without catching B1 (broker.mjs:7 importing the unmounted `hosts.mjs`). This
|
||||
round checks Sage's four points and the second fix.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
Sage's checks:
|
||||
1. broker.mjs's imports are all import-free mounted files. The only import change is `:6`.
|
||||
`validArgv` and `commandAllowed` now come from `business/src/vocabulary.mjs`, and the `hosts.mjs`
|
||||
line is gone. `vocabulary.mjs` has 0 import statements at c43008749 and is in RUNNER_FILES
|
||||
(`seat/src/sandbox.mjs`). broker.mjs's other imports are unchanged from cb5fdb064.
|
||||
2. The pins.
|
||||
- `bus/tests/project-runtime.test.mjs:15-16, :27-28` pins the `createHash` line and the new
|
||||
vocabulary line, each with a Row 210 comment. The `createHash` line was also missing from the
|
||||
pin before this round.
|
||||
- The seat pin needs no change, because `packages/seat` is byte-identical to 3cf63efb4 and
|
||||
RUNNER_FILES is unchanged.
|
||||
3. No `errors.mjs` or `util.mjs` in the mounts: RUNNER_FILES is unchanged.
|
||||
4. The whole suites are on Sage's slot. Gizmoduck reports business 198/0, seat 164/0 and bus 498/0,
|
||||
with 0 skipped. I didn't run suites.
|
||||
|
||||
Also checked:
|
||||
- The move is byte-identical. A `diff` of `ARGV_MAX`..`ARGV_BYTES`, `validArgv` and `commandAllowed`
|
||||
between 3cf63efb4 `hosts.mjs` and c43008749 `vocabulary.mjs` shows no difference except a trailing
|
||||
blank line. They use only the `Buffer` global.
|
||||
- `hosts.mjs:7` imports `ARGV_MAX` back for `checkCommands` (`:100`), and `:11` re-exports all five.
|
||||
The only other reader is `business/tests/hosts.test.mjs:9`.
|
||||
- The roles fix. `roles/pm.md:54-57` and `roles/chief-of-staff.md:74-77` now name `host.sudo` among
|
||||
the hard waits. I confirmed it was needed: with the two role files from 3cf63efb4,
|
||||
`workflow-roles.test.mjs` fails 6/1, and with c43008749's it passes 7/0. The test asserts that the
|
||||
listed waits are exactly GATED_ONLY, so this writes down routing the broker already applies. No
|
||||
grant or authority list changes.
|
||||
- The manifest has 23 lines, every hash matches c43008749, and its paths are the diff from cb5fdb064
|
||||
less the four queue-210 packet files.
|
||||
- Base drift: none of the 23 paths changed on origin/refactor since cb5fdb064 (c8bc5cf4c, 390ea5261,
|
||||
011d41ba6).
|
||||
- Hash pins: no test or script pins any changed file. 0 hits, against a control of 354 files that
|
||||
hold a 64-hex string.
|
||||
|
||||
My runs, single files in a detached worktree of c43008749:
|
||||
- business: hosts 7/0, workflow-roles 7/0.
|
||||
- bus: project-runtime 3/0, hard-waits 13/0, host-run 21/0.
|
||||
- seat `sandbox.test.mjs` can't run in my session. It gives 7/3 on cb5fdb064 too ("no answer" from
|
||||
the confined shell), so that one rests on Sage's gate.
|
||||
@@ -1,76 +0,0 @@
|
||||
# Row 234 early check: split and name map (duckworth-0079)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in gizmoduck-0095. Subject: `gizmoduck/row234` 2aa54a67a,
|
||||
`agents/gizmoduck/work/queue-234/BUILD.md` (71 lines). Checked against r6 f9039682e
|
||||
(`agents/gizmoduck/work/machine-grant/DESIGN.md`, "r6:" below) and #436b section 7 at fa5658941. No code
|
||||
yet, so this is a design-to-build check, not a verdict on the row.
|
||||
|
||||
## Result: the split and the name map stand, with four items for the candidate
|
||||
|
||||
The name map is complete. Every `grant.`/`host.` name in r6 is one of these:
|
||||
- The four renamed in the table (r6:257, :271, :394, :404).
|
||||
- Already under `grant.`: step, applied, failed, revoked, revoke.closed.
|
||||
- Not an event: `host.run`, `host.sudo`, and `host.grant` (r1's dropped action) and `grant.ask` (Q-S1, not
|
||||
built).
|
||||
|
||||
`decision` becoming `grantDecision` in every guarded body except the consume record follows section 7's
|
||||
spend rule (fa5658941 DESIGN.md:472-476). `grant.host.checked` and `grant.host.removed` carry no decision.
|
||||
|
||||
The split matches r6 Q-S4 (r6:904-911) and the X/E sections. Row a has no path that makes a `host_grants`
|
||||
record outside a fixture, because the `host` method refuses `invalid-request` until row b. Items 1 to 8 match
|
||||
r6 S1, X1, E1 to E4 and X4's first and third bullets.
|
||||
|
||||
## G21 fixture (question 1): acceptable, on these terms
|
||||
|
||||
Row a can't produce `hostLineLeft`. An enrollment has no remote step, so its failures leave no line. A fixture
|
||||
is therefore the only way to test the check in row a.
|
||||
- F1. Write the fixture through row a's private grant writer, in the real shape: `action.allowed`, actor
|
||||
(NULL, 'grant'), `body.action` 'grant.failed', `body.host`, `body.hostLineLeft`. Don't write it with raw
|
||||
SQL around the trigger.
|
||||
- F2. Read `body.hostLineLeft` from any guarded body, not only `grant.failed`. Row b's
|
||||
`grant.revoke.closed` carries it too (r6:715-717).
|
||||
- F3. r6's `grant.revoke.closed` body is `{tag, hostLineLeft}` (r6:716). It names no host. Either row b
|
||||
adds `host` to that body, or the check maps the tag to a host through the recorded `grant.step` tags.
|
||||
Fix which one in BUILD.md now, so row a's reader and row b's writer agree.
|
||||
- F4. "No later `sweep` cleared it" (r6:403) needs `grant.sweep`, which row b writes. Say in BUILD.md
|
||||
that row b adds the sweep clear to `hosts.remove` with a G21 case. Row b's G8 or G13 should also show that
|
||||
`hosts.remove` refuses after a real write, not only after the fixture.
|
||||
|
||||
## Moves and gaps (question 2)
|
||||
|
||||
- M1. G4 for the `host-enroll` block goes in row a. Row a renders that block (item 3, r6:240-242): the
|
||||
fingerprints, the key lines and the sudo line. Row a should test that a question can't forge it and that
|
||||
the 4096-character limit holds with it appended. G4 for the `host` block stays in row b.
|
||||
- M2. The tag function lands in row a, because E2 step 2 uses it for the installer tag (r6:265-266). Move
|
||||
G24's first sentence (64-character names with `.` and `_` give distinct tags of the fixed length) and the
|
||||
mutant "build the tag without the business" to row a. The `sweep` half of G24 stays in row b.
|
||||
- M3. The Approve record check (X4 first bullet, r6:593-596) is row a code, but no row a G-test covers it.
|
||||
G22 is row b. Add a row a test on store fixtures that `host.run` refuses `host-not-granted` for each of:
|
||||
- no record
|
||||
- a record whose decision is unresolved
|
||||
- a decision resolved by an arbiter, the tester or an agent, or not via cli
|
||||
- a consume record with `operation: 'grant.host-enroll'` instead of `'grant.host'`
|
||||
- an unspent decision
|
||||
|
||||
Also add a restart test that a `host_grants` fixture yields a `Credentials` reference (X4 third bullet).
|
||||
The mutant "`host.run` reads the business file instead of the grant record" goes in row a.
|
||||
- M4. Item 8 lists status, the start reconcile and `openGrantDecisions`. Add `hosts.remove`'s
|
||||
`hostLineLeft` reader (F2). The look-alike test should cover it: an `action.refused` or session-written
|
||||
`hostLineLeft` doesn't block a remove.
|
||||
|
||||
N1 (doesn't block). BUILD.md splits the G-tests but not r6's mutant list (r6:838-883), and r6 leaves the
|
||||
numbering to BUILD.md. A per-row list before the candidate saves a round. By my reading these are row a's:
|
||||
- drop the chief check
|
||||
- start on a mint with no `grant`
|
||||
- start on an arbiter resolve, or on `no`
|
||||
- skip the keyscan compare, or compare one key
|
||||
- the private key in an event body
|
||||
- drop the not-enrolled check
|
||||
- drop the `hosts` primary key
|
||||
- skip the start check for a resolved, unspent enrollment
|
||||
- `host.run` reads the business file
|
||||
- the tag without the business
|
||||
- the writer mutants (`from=`, a line outside the tag pattern, `O_NOFOLLOW`, a dropped group, the user
|
||||
pattern, `privileged` without `users`, a sudo user not in `privileged`)
|
||||
- section 7's trigger mutants
|
||||
- one reader-filter mutant
|
||||
@@ -1,59 +0,0 @@
|
||||
# Row 234 early code read, follow-up (duckworth-0083)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in gizmoduck-0102. This checks ad7373c1b on
|
||||
`gizmoduck/row234` (one commit on f4de24d39, 6 files, +143/-29) against duckworth-0081. It's still not the
|
||||
manifest verdict.
|
||||
|
||||
## Result: B1 to B3 resolved
|
||||
|
||||
- B1 (G10): `grant-enroll.test.mjs:269`.
|
||||
- A second start on a spent decision returns `decision-consumed`, and the events, the host row and the
|
||||
message count don't change.
|
||||
- An open raise returns `not-authorized`. So does one resolved yes by the chief.
|
||||
- After that there's no web2 row and no key file, and `reconcileGrants` returns [].
|
||||
- The new public `runEnrollment` (`broker.mjs:745`) only wraps `#runEnrollment`. No verb, server
|
||||
route or runtime op reaches it. The only other call is the resolve hook at :2445. It's the same kind
|
||||
of trusted entry as `reconcileGrants`, and step 1 checks everything again. I accept it.
|
||||
- The return codes don't change anything the hook depends on, because `setImmediate` ignores them.
|
||||
- B2 (G18):
|
||||
- The host half is at `host-run.test.mjs:297`. `hg.check` with a wrong pin refuses
|
||||
`host-key-mismatch`, and the stub `list` never runs.
|
||||
- The broker half is at `grant-enroll.test.mjs:401`. The host stays `pending-install` with
|
||||
`checked_at` null, and there's no `grant.host.checked`.
|
||||
- B3:
|
||||
- G6 is at :251. A withdraw by the chief and by the human, then an arbiter resolve yes, write nothing,
|
||||
and `runEnrollment` returns `not-authorized`.
|
||||
- G20 is at :390. The reader is refused `read-only`, and the human's check reaches `ready` with `by`
|
||||
`['jason']`.
|
||||
- N2 is fixed. A timeout gives '', which is not the expected line, so the writer refuses `user-sudo`,
|
||||
logs it and exits 1.
|
||||
- N3 is fixed in the comment.
|
||||
- N4 stands as Gizmoduck says. r6:578 lists the tag only for the start reconcile's `interrupted`.
|
||||
- N5 is in the run sheet.
|
||||
|
||||
## Notes (no block)
|
||||
|
||||
- N8. G10's agent-resolved row goes in with `via 'broker'` (:280), so `#approvedGrant` refuses at the
|
||||
via check and never reaches the `by` check. A second row with `via 'cli'` and `by 'chief'` would pin
|
||||
`by == human` too. The A1 to A23 run will show whether a mutant on the `by` check survives. If one
|
||||
does, this is the test to add.
|
||||
- N9. G18's control (:310) calls `runHost` with the fixture's entry, not `hg.check` with the right pin.
|
||||
So it shows the stub works, but not that `hg.check` passes the record's keys through when they're
|
||||
right. E4's broker tests use a fake check, so nothing runs `hg.check` end to end with a good pin. That
|
||||
could be a small follow-up, or the #433 VM covers it.
|
||||
|
||||
## My runs
|
||||
|
||||
In a detached worktree of ad7373c1b, pass/fail:
|
||||
|
||||
| file | result |
|
||||
| --- | --- |
|
||||
| bus grant-enroll | 24/0 |
|
||||
| bus host-run | 24/0 |
|
||||
| bus host-tools | 6/0 |
|
||||
| bus grant-migration | 5/0 |
|
||||
| bus hard-waits | 13/0 |
|
||||
| bus project-runtime | 3/0 |
|
||||
| bus credentials | 5/0 |
|
||||
|
||||
`mosaic-keys-write` parses under python3 -I. I ran no suite and no mutant.
|
||||
@@ -1,92 +0,0 @@
|
||||
# Row 234 early code read (duckworth-0081)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in gizmoduck-0101, before the formal request. Candidate
|
||||
code `gizmoduck/row234` f4de24d39 on 8fede5fc6 (39 files, +2953/-187). Brief:
|
||||
`docs/plans/2026-10-20_host-credentials.md` "Machine grant row a", whose gate needs "the design's section
|
||||
10 tests and mutants for row a". Design r6 f9039682e, section 7 fa5658941. This is not the manifest
|
||||
verdict. That one comes after Sage's A1 to A23 run.
|
||||
|
||||
## Verdict: changes, 3 blockers
|
||||
|
||||
BUILD.md:68-69 lists G6, G10, G18 and G20 among row a's tests. The brief's gate requires them, so each
|
||||
missing case blocks.
|
||||
|
||||
- B1. G10 has no test (r6:818). No row a test starts a second enrollment for a spent decision and gets
|
||||
`decision-consumed`. None shows `#consumeGrant` (`broker.mjs:678`, `#spent` at :681) refusing an
|
||||
unresolved or an agent-resolved decision. A8 (`grant-enroll.test.mjs:250`) uses two decisions, so it
|
||||
tests `host-enrolled`, not this. The only `decision-consumed` assert near hosts is `host-run.test.mjs:250`,
|
||||
which covers row 210's host.run. The direct way is to call the executor or `reconcileGrants` twice for
|
||||
one resolved decision, and once each for an open decision and an agent-resolved fixture.
|
||||
- B2. G18 has no test (r6:826). The code path exists: `host-grant.mjs:207-211` calls `runHost` with the
|
||||
pinned keys, and `host-run.mjs:197` refuses `host-key-mismatch` on ssh exit 255. But
|
||||
`grant-enroll.test.mjs:333-356` uses a fake check that never throws that code, and
|
||||
`host-tools.test.mjs` tests only the writer, mosaic-keys, sudoers and sshd. A test needs to show that a
|
||||
wrong pin refuses `host-key-mismatch`, runs nothing, and leaves the record `pending-install` with no
|
||||
`grant.host.checked`. The fake check can throw the BusError for the broker half. The runHost half can
|
||||
reuse the host-run.test.mjs ssh stand-in that gives :290 its `host-key-mismatch`.
|
||||
- B3. G6 and G20 are partly tested.
|
||||
- G6 (r6:814): the no is tested (A4, :231), and so are the tester, pm and chief resolves (A3, :237).
|
||||
The withdraw and the arbiter resolve have no case. A withdraw of an open `host-enroll` decision must
|
||||
start nothing.
|
||||
- G20 (r6:828): the chief reaches `ready` (:341) and the tester gets past the role check to
|
||||
`host-changed` (:355). The human's `hosts.check` is never called. It must reach `ready` through
|
||||
`#hostsCaller`'s `#human` leg.
|
||||
|
||||
## Notes (no block)
|
||||
|
||||
- N1. A3 (`grant-enroll.test.mjs:237`) can't reach `#approvedGrant`'s via and by checks, because every
|
||||
resolve it tries is refused first. The M3 fixtures in `host-run.test.mjs` cover those checks, so
|
||||
nothing is untested. The test name reads as more than it shows.
|
||||
- N2. `mosaic-keys-write:255-266`: `main` catches `Refused` and `OSError`. A `sudo -l` timeout raises
|
||||
`subprocess.TimeoutExpired`, which escapes, so the writer exits with a traceback and logs nothing.
|
||||
Catching it as `refused user-sudo` keeps every refusal in syslog.
|
||||
- N3. `host-grant.mjs:86,106`: a keyscan that exits nonzero, such as an unreachable host, resolves with
|
||||
no lines, so the raise refuses `host-key-mismatch`. The comment says a scan that can't run refuses
|
||||
`host-failed`. That holds only for a spawn error. G17 doesn't decide which code is right. Worth one line
|
||||
in BUILD.md or the comment.
|
||||
- N4. In the run, `#enrollFailed` writes `grant.failed` with no tag. `reconcileGrants` does tag it. If
|
||||
section 7 leaves the tag optional this is fine. Otherwise the two should match.
|
||||
- N5. `read_conf(host, 'privileged')` refuses when the file is missing (`mosaic-keys-write:136-138`). The
|
||||
#433 run sheet should say to create an empty, root-owned `/etc/mosaic-keys/privileged`.
|
||||
- N6. `#hostAccess` binding the grant's agent and host (`broker.mjs:692`) goes beyond X4. I agree with
|
||||
it. It narrows and refuses nothing X4 allows.
|
||||
- N7. `isHostKind` (`business/src/resolve.mjs:92`) can't be reached and stays, as BUILD.md records. No
|
||||
objection.
|
||||
|
||||
## Checked, no finding
|
||||
|
||||
- `#approvedGrant`, `#consumeGrant`, `#runEnrollment`'s two transactions and their catch, and
|
||||
`reconcileGrants` for both the spent-unfinished and the open decision. `#openGrantDecisions` uses the
|
||||
unfiltered `#spent`. That is sound while no verb consumes `credential.mint`.
|
||||
- `#raiseGrant` order, with the enroll block appended before the 4096 cap (M1, :165).
|
||||
- `hosts.remove` order (host-unknown, host-in-use, host-line-left, then delete, event, unenroll last), and
|
||||
`hosts.check` with its host-changed recheck in the transaction.
|
||||
- 3i-3j.sql: hosts and host_grants keys, CHECKs, the FK, and the `events_grant_actor` trigger. The trigger
|
||||
cases are in `grant-migration.test.mjs:134`.
|
||||
- The writer's dir, lock, user-file and tmp handling (G25, `host-tools.test.mjs:225`).
|
||||
- Hash pins: there is no 64-hex pin in `packages/*/tests/*` or `scripts/test-*.sh` on a changed path. The
|
||||
one basename hit, `channel-proof.test.mjs:12` for `bus/src/human-cli.mjs`, is a file this row doesn't
|
||||
change. The control found pins in account-, comment-migration and channel-proof.
|
||||
- RUNNER_FILES rule (sage-1619): the broker adds the `HOST_ID` import. I ran seat sandbox.test.mjs and
|
||||
bus project-runtime.test.mjs (below).
|
||||
|
||||
## My runs
|
||||
|
||||
Single files in a detached worktree of f4de24d39, pass/fail:
|
||||
|
||||
| file | result |
|
||||
| --- | --- |
|
||||
| bus grant-enroll | 20/0 |
|
||||
| bus host-run | 23/0 |
|
||||
| bus host-tools | 6/0 |
|
||||
| bus grant-migration | 5/0 |
|
||||
| bus hard-waits | 13/0 |
|
||||
| bus project-runtime | 3/0 |
|
||||
| bus credentials | 5/0 |
|
||||
| bus chief-of-staff | 18/0 |
|
||||
| bus account-, comment-, name-, spec-, thread-, usage-migration | 3/0, 5/0, 1/0, 1/0, 3/0, 1/0 |
|
||||
| business hosts | 4/0 |
|
||||
| cli hosts | 5/0 |
|
||||
| seat sandbox | 7/3, the same environmental failure I get on base commits in this session |
|
||||
|
||||
I ran no suite and no mutant. Those are slot work.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Review: row 237 round 1, one decision publishes once (duckworth-0069)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Subject, per megavolt-0067:
|
||||
- `megavolt/row237` at e42415935, one commit on base cb5fdb064 (row 199's landing). 4 files, +123.
|
||||
- Manifest `manifest-r1-e42415935.txt`, sha256 bb777cb0…8b7e. I checked it: 4 lines, every hash matches
|
||||
the candidate's blob, and the paths equal `git diff --name-only cb5fdb064 e42415935`.
|
||||
- Base drift: origin/refactor changed only `packages/business/README.md` since cb5fdb064. That isn't a
|
||||
manifest path.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
This closes my row 199 note N1 (duckworth-0068).
|
||||
|
||||
Checked and holding:
|
||||
- `checkPublish` refuses `publish-busy` (`broker.mjs:1040`) when an open ticket holds the same business
|
||||
and decision. The check and the `#publishes.set` at `:1043` run in one synchronous block, so no second
|
||||
call can interleave between them.
|
||||
- The comparison is exact. `p.decision` is the request's id. `result.decision` is `d.id` from
|
||||
`#authorityFor` (`:821-822`), which `#decision` (`:663-664`) looks up with `id=?`. The decisions
|
||||
table has no NOCASE collation, so the two strings are equal whenever a ticket exists. No alias or
|
||||
case form slips past the check.
|
||||
- Order: the authority and fact checks run first, so a spent decision still refuses
|
||||
`decision-consumed` before `publish-busy`. A within-role publish has no decision and skips the check.
|
||||
- Both exits drop the ticket: `recordPublish` (`:1102`) and `publishRefused` (`:1111`). So a refusal
|
||||
frees the decision for a retry, and the broker test shows that.
|
||||
- Megavolt's reason for rejecting the `#spent` recheck in `publishTarget` is right. Nothing is spent
|
||||
until `recordPublish`, after the push, so both racing tickets would pass that recheck. My N1 offered
|
||||
it as an option, and it wouldn't have closed the race.
|
||||
- Hash pins: no test pins a manifest file. `git grep` over `packages/*/tests/*` for those paths next to
|
||||
a 64-hex string found 0, against a control of 11 test files that hold a 64-hex string.
|
||||
|
||||
My runs, in a detached worktree of e42415935:
|
||||
- `packages/bus/tests/publish.test.mjs`: 13/13.
|
||||
- `packages/cli/tests/publish-socket.test.mjs`: 8/8.
|
||||
- M1 (delete the `publish-busy` line): bus 12/1, cli 7/1, so it's killed in both. `git diff` was
|
||||
clean after the restore.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Residual 2 can still put the commit on a second fork ref.
|
||||
- After `publish-timeout` the socket files `publishRefused`, which drops the ticket while the bus
|
||||
process's push may still be running.
|
||||
- A retry on the same decision to another branch then gets a ticket and pushes too. That's the N1
|
||||
shape again, but only after a timeout.
|
||||
- It's the same scope as before: the approved commit on the agent's own fork. Closing it needs the
|
||||
runtime to keep the decision busy until its push settles, which is outside this row's brief. I'd row
|
||||
it as a follow-up.
|
||||
|
||||
N2. Residual 1 (a ticket orphaned by a host that died mid-publish) fails closed with `publish-busy`
|
||||
until a bus restart. That's acceptable as is.
|
||||
|
||||
N3. Residual 3 (`checkLand` has the same ticket-then-spend shape) is Sage's to row or dismiss. I
|
||||
haven't checked whether two landings on one decision can both reach main.
|
||||
|
||||
Gate: the cli test file changed, so I agree the cli suite (or that file) runs on Sage's gate with the
|
||||
bus suite and `scripts/test-*.sh`.
|
||||
@@ -1,37 +0,0 @@
|
||||
# Row 241 round 1 (duckworth-0080)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in megavolt-0073. Brief: `docs/plans/2026-10-20_agent-credentials.md`,
|
||||
"Brief: row 237 follow-up, one decision lands once, with tests at the broker" (c25785d3d). Candidate
|
||||
`megavolt/row241` 3e3ed303e on f5753c0e8, manifest sha256
|
||||
e82668642589eefca8b22433f1b0c7a24432f1993c0050f0a4b74c1146c93c2f. I rebuilt it from the two blobs at 3e3ed303e,
|
||||
and it matches. The diff is the two manifest paths only (+93), tests and BUILD.md, with no source.
|
||||
|
||||
## Verdict: approve, no findings
|
||||
|
||||
1. The test meets the brief. `land.test.mjs:312` opens two `checkLand` tickets on one decision. The first
|
||||
`recordLand` lands. The second refuses `decision-consumed` at :319. Before `landRefused` it leaves one
|
||||
merge `action.allowed` (commit B), one RESULT and no `action.refused`.
|
||||
- `recordLand` runs inside `#store.transaction` (`broker.mjs:1001`), so a refusal at `#landFacts` rolls
|
||||
back everything.
|
||||
- With the `#spent` line gone, the second call passes every other check in `#landFacts` (same task,
|
||||
same latest push, same reviewed commit) and would record a second merge. So :319 is the assert that
|
||||
pins the recheck, as BUILD.md says.
|
||||
- The tail checks that `landRefused` files the code once and ends the ticket (`land-unknown` after).
|
||||
2. The three guard cites are true at f5753c0e8:
|
||||
- `land-busy` at `git-socket.mjs:351`. The slot is set at :353-354, before the first await, and freed
|
||||
at :358 unless `slot.done`.
|
||||
- The CAS `update-ref main land.commit land.old` at `working.mjs:391`, refusing `base-moved` at :392.
|
||||
The move back is at :396, and `rollback-failed` at :397.
|
||||
- `#spent` at `broker.mjs:963` in `#landFacts`. That function is called from `checkLand` (:950) and
|
||||
`recordLand` (:1006). The brief's :945 predates row 210. BUILD.md records the move.
|
||||
3. The mutant table names real tests and asserts: `land.test.mjs:94`/:144, `working.test.mjs:555`/:567
|
||||
(base-moved) and `git-socket.test.mjs:812`/:825 (land-busy). M2 is killed by the existing test, so
|
||||
working.test.mjs is left alone per the brief. I didn't run the mutants. They are slot work, on Sage's
|
||||
gate.
|
||||
4. No test or script pins a hash of either path. The four hits for `land.test.mjs` in the tests are
|
||||
comments.
|
||||
|
||||
My runs, in a detached worktree of 3e3ed303e: `land.test.mjs` 18/0, and the new test alone 1/0.
|
||||
|
||||
I didn't check BUILD.md's aside that a replayed patch fails `base-conflict` in `prepareLand`. No guard or
|
||||
test here depends on it.
|
||||
@@ -1,62 +0,0 @@
|
||||
# Row 242 round 1 (duckworth-0082)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in megavolt-0078. Brief:
|
||||
`docs/plans/2026-10-20_agent-credentials.md`, "Brief: row 237 follow-up N1, a retry after publish-timeout
|
||||
pushes once" (9d165be8c). Candidate `megavolt/row242` 635c10924 on 8fede5fc6, manifest
|
||||
`/mnt/storage/scratch/megavolt-r242/manifest-r1-635c10924.txt`, sha256
|
||||
1fbfab6c68e8d76d2d69b13c415257280d4fa8c5584e24d5dfd13dca1420a3da. The three blob digests match the files at
|
||||
635c10924, and the diff is those three paths only (+179/-3).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
1. The fix holds the decision. `runtime.mjs` `publishRefused` sends a refusal on a ticket with a running
|
||||
push to `entry.held`, so `broker.publishRefused` (`broker.mjs:1125`) doesn't run and the ticket stays in
|
||||
`#publishes`. `checkPublish`'s row 237 check (`broker.mjs:1058`) then refuses another publish on the
|
||||
decision with `publish-busy`. When the push ends, the chained `.then` calls `settle`.
|
||||
2. `settle` closes every way.
|
||||
- A push that reached the fork goes to `recordPublish`, which spends the decision, so a retry gets
|
||||
`decision-consumed`.
|
||||
- A failed push files the socket's code.
|
||||
- A `recordPublish` BusError files that code.
|
||||
- Anything else is logged by name and code only, so no git output and no token.
|
||||
`remotes.delete` runs first, and the `.then` can't reject, so no unhandled rejection is possible.
|
||||
3. The other paths are unchanged or improved.
|
||||
- With no entry (the local `publish()` checks refused before `publishPush`), the call goes to the
|
||||
broker as before.
|
||||
- An entry that has ended but wasn't collected settles at once. Before this change, a push that
|
||||
finished just as the socket gave up had its ref left on the fork and the decision free. Now it's
|
||||
recorded.
|
||||
- The code regex matches the broker's, so a bad code still refuses `invalid-request`.
|
||||
4. The hold is bounded. Each git run in `publishRemote` (`seat/src/working.mjs:460`) has GIT_LIMITS, as
|
||||
BUILD.md:35-36 says.
|
||||
5. The test meets the brief (`publish-socket.test.mjs:302`). It starts a push held in pre-receive, times
|
||||
it out, retries to `other` (`publish-busy` at :322), releases the hook, and waits for one allowed event
|
||||
with the work commit. Then it checks `decision-consumed` at :330, one receive-pack, and the fork's only
|
||||
ref, `refs/heads/coder/feat`. The try/finally releases the hook on a failed assert.
|
||||
6. Both mutants match what they claim. M1 puts back the old export, which is the fix removed, as the
|
||||
brief asks. M2 is hold-only. Both kills are on Sage's slot (sage-1686). I didn't run them.
|
||||
7. The `publish-timeout` text is unchanged (`git-socket.mjs:433-434`). It isn't in the diff.
|
||||
8. No test or script pins a 64-hex hash of `runtime.mjs` or `publish-socket.test.mjs`. The control found
|
||||
pins in account-migration and channel-proof.
|
||||
|
||||
## Notes (no block)
|
||||
|
||||
- N1. Two comments in `packages/cli/src/git-socket.mjs`, which is outside the manifest, are now partly
|
||||
stale:
|
||||
- :39-41 says any refusal after checkPublish "is filed once, by publishRefused". A late recorded push
|
||||
now files no `publish-timeout` at all.
|
||||
- :427-428 says the fork "could then hold the commit with no record". After this row that happens only
|
||||
when `recordPublish` refuses.
|
||||
These are comments only, so they're a follow-up or ride with a later cli row.
|
||||
- N2. When a late `recordPublish` refuses (say `ref-moved`), the ticket closes with a ref on the fork and
|
||||
the decision unspent, so a retry can push a second ref. This is the same residual as the in-band
|
||||
`recordPublish` refusal, and the brief doesn't cover it. Worth a line under BUILD.md's residuals.
|
||||
- N3. I agree with BUILD.md's two residuals: the run isn't told about a late record, and a bus restart
|
||||
mid-hold is the same as today. The RESULT question is Sage's to rule on.
|
||||
- N4. BUILD.md:83-84 says M2 fails at ":327 (the bounded wait at :311 ...)". The wait call is at :327,
|
||||
and :311 is the `assert.fail` inside `until`. That reading is right, the wording is just loose.
|
||||
|
||||
## My runs
|
||||
|
||||
In a detached worktree of 635c10924: `publish-socket.test.mjs` 9/0, the row 242 test alone 1/0, and bus
|
||||
`publish.test.mjs` 13/0. No suite and no mutant. Those are on Sage's gate.
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Gizmoduck
|
||||
|
||||
Design and builder seat, Claude Opus 5.5, a T3 thread (9312a896). Author of the federation design
|
||||
(`agents/gizmoduck/work/federation/DESIGN.md` on `gizmoduck/federation-design`, #414). Builds FED R1, the
|
||||
access.json loader and validator, under Mos's order mos-1023 item 1.
|
||||
|
||||
No launcher in this directory. The seat works in its own worktree and has no credentials of its own; until
|
||||
the credentials row settles per-agent identity, it pushes only its own `gizmoduck/*` branches through the
|
||||
repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Herb
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for a second gate lane
|
||||
design row (#1695, mos-0977 item 6).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `herb/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,158 @@
|
||||
# PRD 1.7 draft: the chief of staff's hire and fire authority, the coder's push to its fork, and agent
|
||||
# host access through the credential store
|
||||
|
||||
Draft by Honker for row 194 (#1692), under Mos's ruling mos-1017 (through Sage, sage-1365). Not approved.
|
||||
It changes nothing until Jason approves it as a `prd.approve` decision in the Console. Sage opens that
|
||||
decision and sends Mos its id. Base: `docs/prd/mosaic-stack.md` version 1.6 at 53bb59462.
|
||||
|
||||
## What it changes
|
||||
|
||||
Row 194 adds `role.hire` and `role.fire` to the vocabulary, and row 199 adds `git.push.remote`. Under
|
||||
REQ-WAIT-3 all three are hard waits, so today Jason resolves every hire, every fire and every push to a
|
||||
remote. This version routes hire and fire for the chief of staff within fixed bounds, so the chief hires
|
||||
and fires as needed and the user's per-agent forbid still holds. It routes `git.push.remote` for the coder
|
||||
only (Mos, mos-1028 item 3). It adds agent host access with sudo through the credential store for
|
||||
agents with a grant (Q183, Q184, Vikunja #424, due 2026-10-27). One `prd.approve` decision covers it all
|
||||
(Vikunja #420).
|
||||
|
||||
When this version lands, a reviewed commit:
|
||||
|
||||
1. adds `role.hire`, `role.fire` and `git.push.remote` to `ROUTED` in
|
||||
`packages/business/src/vocabulary.mjs` and in `packages/bus/src/broker.mjs`.
|
||||
2. adds `role.hire` and `role.fire` to `roles/chief-of-staff.json` withinRole, and `git.push.remote` to
|
||||
`roles/coder.json` withinRole. No other role file gets `git.push.remote`.
|
||||
3. switches the broker's check on `instance.hire` and `instance.fire` from a cited decision to the
|
||||
`reason` form, as `role.end` works for REQ-ROLE-2, and makes `publish`'s `decision` optional for a
|
||||
within-role class (row 199 D9 step 1).
|
||||
4. states, for a `git.push.remote` with no decision, where the source commit comes from: the latest work
|
||||
commit the bus recorded for the coder's task. The check still matches it against the work ref tip and
|
||||
the push sends it by oid (row 199, duckworth-0022 N9).
|
||||
5. adds the host credential grant check and the host access it allows (REQ-CRED-3, Vikunja #424).
|
||||
|
||||
Every other check stays as rows 194 and 199 build it. Filling `hireable` stays a separate reviewed role
|
||||
file commit in `roles/`.
|
||||
|
||||
## Text for the PRD
|
||||
|
||||
Status line, appended to the current paragraph:
|
||||
|
||||
> Version 1.7 routes the chief of staff's hire and fire actions and the coder's push to its own fork,
|
||||
> and adds agent host access through the credential store, approved by Jason in decision
|
||||
> <prd.approve decision id>.
|
||||
|
||||
Source, a new bullet above "Version 1.6 source":
|
||||
|
||||
> - Version 1.7 source, TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as
|
||||
> needed; unless user forbids per agent". And Jason through Mos (mos-1067), DECISIONS Q183, in part: "I will
|
||||
> still want to allow infra agent(s) on my .53 instance to access other servers, etc. However, not all
|
||||
> agents will need to have that access. I envision a credential store within Mosaic Stack that works
|
||||
> as some sort of variable for the agent to call. The system will enforce access to the credentials to
|
||||
> keep the agents sandboxed with proper security. I need to be able to trust my agents with assisting
|
||||
> me. This will require sudo access to several systems. If we can utilize the credential store in
|
||||
> Mosaic Stack and allow agents to perform their jobs with NOPASSWD sudo on various machines, I am a
|
||||
> happy man." DECISIONS Q184, on whether that is in place by 10-27: "Q5: by 10.27"
|
||||
|
||||
Under "Added in version 1.2", after REQ-COS-2:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-COS-3.** The chief of staff hires and fires role instances without a decision each time. Hiring
|
||||
> adds a role instance to the organization file and lists it for launch. Firing ends the instance's
|
||||
> runs and removes it from the launch list. It stays in the organization file. Only the user deletes
|
||||
> an instance. These actions are `role.hire` and `role.fire`. They leave the hard waits and route
|
||||
> under this requirement, within these bounds:
|
||||
> - The chief acts only in its own organization.
|
||||
> - The chief hires only role definitions that need no credentials, until a later version adds
|
||||
> credentialed roles.
|
||||
> - The chief hires only the role kinds its role file lists. Changing that list is a reviewed commit.
|
||||
> - The chief hires only a role whose every action is on a fixed list of plain work actions: sending
|
||||
> messages, writing records, asking for review, updating its assigned tasks, and the scope and
|
||||
> working-push actions of an assigned task. A role holding any other action, such as launch, end,
|
||||
> organization, naming, hire, review verdict or merge authority, or any action added later, is never
|
||||
> hired. So the chief never hires a chief, never picks its own reviewers or merge gate, and the author
|
||||
> is never the reviewer. This holds whatever the role file's list says. Adding to that list is a PRD
|
||||
> change.
|
||||
> - The chief may set a hired instance's model and narrow its limits, and nothing else about it.
|
||||
> - The user can forbid per agent. The user marks an instance so the chief can't fire it, end its runs
|
||||
> or change its model, and marks role kinds the chief can't hire. Only the user changes these marks, through the
|
||||
> WebUI or the CLI.
|
||||
> - The kill switch stops every hire and fire while it is on.
|
||||
> - Every hire and fire is logged with the chief's instance, its run and the reason.
|
||||
|
||||
Under "Landing", after REQ-LAND-1:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-LAND-2.** A coder pushes its own work to its own fork. The action is `git.push.remote`.
|
||||
> - What it pushes: one commit, by its oid, never a ref name. That commit is the latest work commit the
|
||||
> bus recorded for the coder's task, and the coder made that push. A decision that authorizes a push
|
||||
> names that commit, and it doesn't authorize any other.
|
||||
> - Where it lands: only the branch `refs/heads/<its Gitea user>/<branch>`. The host builds that
|
||||
> name, the agent never does. A push never forces.
|
||||
> - Which remote: the agent's own fork of the project's upstream. The business file names the upstream,
|
||||
> and the host never contacts it. For each push the host builds the fork's address over https from
|
||||
> the upstream's host, port and repository name and the agent's own Gitea user. The history check,
|
||||
> the push and the credential match all use that address. So the agent's own user owns the receiving
|
||||
> repository on every push, and the agent never names it.
|
||||
> - The host refuses an upstream on a host or port other than the agent's Gitea, and an upstream the
|
||||
> agent's own user owns. It refuses a fork that is missing, empty or doesn't share the project's
|
||||
> history. Gitea's push-to-create stays off, so a push never creates a repository.
|
||||
> - Who may: the action routes under REQ-DEC-2 and is granted within-role to the coder role only. No
|
||||
> other role file grants it.
|
||||
> - Before version 1.7 the action was a hard wait under REQ-WAIT-3, so each push needed a decision
|
||||
> the human resolved, and that decision named the commit.
|
||||
>
|
||||
> Every push is logged with the remote, the ref, the commit and, when one was given, the decision.
|
||||
|
||||
Under "Credentials", after REQ-CRED-2:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-CRED-3.** An agent with a grant can run commands with sudo on another host. The agent asks
|
||||
> for the host credential by name. The stack checks that a grant exists for that agent and that host
|
||||
> before the credential is used. The agent never sees the key or any other secret value (REQ-CRED-1,
|
||||
> REQ-VAR-2). An agent without a grant for that host is refused. Not every agent gets a grant. Who
|
||||
> grants a host credential, and how, is set by the #424 design, which Jason approves.
|
||||
|
||||
REQ-WAIT-3, appended:
|
||||
|
||||
> `role.hire` and `role.fire` were added as hard waits and route under REQ-COS-3 since version 1.7.
|
||||
> `git.push.remote` was added as a hard wait and routes under REQ-LAND-2 since version 1.7.
|
||||
|
||||
## Notes for the reviewer and for Mos
|
||||
|
||||
- REQ-COS-1 (1.2) already says the chief "hires and terminates role instances". REQ-COS-3 gives that
|
||||
sentence its actions and bounds. It doesn't widen REQ-COS-1.
|
||||
- The forbid now names what r3 called "replace": ending a kept instance's run with a reason and changing
|
||||
its model (webby-0010 N2). A user-resolved decision still ends a kept run (row 194 D2.4).
|
||||
- The fixed list is `HIRE_ALLOWED` in row 194 D2.3, an allowlist since r4 (webby-0010 B1). r3's text
|
||||
named the refused authorities instead, which let merge-gate and any later action through.
|
||||
- The kill switch is row 191's. If row 191 hasn't landed when this version is approved, the bound still
|
||||
holds through row 194's chain on its candidate.
|
||||
- REQ-LAND-2 follows row 199's design r6 (4d3007ec9 on megavolt/row199, approved by Duckworth in
|
||||
duckworth-0027: D9 to D11, D9a, D9c, Q-M1 and Q-M2, and the oid binding from mos-1034 item 5) and
|
||||
Megavolt's wording in megavolt-0005, megavolt-0008 and megavolt-0014, through Sage (sage-1377,
|
||||
sage-1380, sage-1387). The fork bullet answers duckworth B5 and B6 and webby-0011 N1. Commit-email
|
||||
checks, the refusal codes and the URL parse rules (D9c) are host checks with tests in row 199, not
|
||||
bounds, so they stay out of the text (megavolt-0008).
|
||||
- Once the routing lands and the decision is optional for the within-role class, the commit is the task's
|
||||
latest recorded push at check time. It is still checked against the work ref tip and pushed by oid, so
|
||||
the binding holds without a decision (megavolt-0008). Step 4 of "When this version lands" puts that in
|
||||
the routing commit (duckworth-0022 N9, sage-1387).
|
||||
- REQ-LAND-2 and REQ-COS-3 meet at the coder. Coder declares Gitea credentials, so REQ-COS-3's
|
||||
credentials bound keeps the chief from hiring one until a later version lifts it. `git.push.remote` isn't
|
||||
on REQ-COS-3's fixed list either, so a coder holding it stays unhireable until a PRD version adds it
|
||||
(row 194 D2.3).
|
||||
- Adding to the fixed action list is a PRD change (mos-1035 item 3, sage-1395). The role file's
|
||||
`hireable` list stays a reviewed commit in `roles/`.
|
||||
- The credentials bound names no row number, so the text doesn't go stale when row 199 lands. A later
|
||||
version lifts it.
|
||||
- REQ-CRED-3 states only what Jason said in Q183 and Q184 (sage-1422). It names the outcome and the
|
||||
check, not how the stack reaches the host. Proxying the ssh and a signing socket are both designs, and
|
||||
the #424 design picks one (duckworth-0038 B1).
|
||||
- Open point: Q183 doesn't say who grants host access or how, so the text leaves it open and defines no
|
||||
mechanism. The #424 design settles it, Jason approves that design, and Duckworth reviews it because it
|
||||
gives agents sudo on hosts (Q184, Mos's reading, duckworth-0038 B2). The build is row 210 (#1707,
|
||||
owner Gizmoduck), a new row on top of row 199 (megavolt-0019, sage-1430).
|
||||
- This draft follows the PRD's own style: plain words, no semicolons in new sentences, and the status
|
||||
paragraph never edited in place except to append.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Megavolt
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage under mos-0974 and mos-0977 item 5 for MVP T1 item 6, the
|
||||
credential store per agent with its own git identity (#1697).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `megavolt/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Quackerjack
|
||||
|
||||
Builder seat, Claude Haiku 5.5 at high effort, started by Sage under mos-1023 item 1 and Q177 for federation
|
||||
rows R5 (`mosaic access list, grant, revoke`) and R2 (the `federation` config key), per Gizmoduck's design
|
||||
section 7 (#414).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own
|
||||
`quackerjack/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or
|
||||
another seat's branch.
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
n=0
|
||||
for p in /proc/[0-9]*; do
|
||||
c=$(tr '\0' ' ' < $p/cmdline 2>/dev/null) || continue
|
||||
case "$c" in *shell-snapshots*|*chrome-devtools-mcp*|*sage-busy-now*) continue;; esac
|
||||
if echo "$c" | grep -qE 'node --test|gate\.sh|mutant|scripts/test-[a-z-]+\.sh|docker compose'; then echo "BUSY ${p#/proc/}: ${c:0:160}"; n=$((n+1)); fi
|
||||
done
|
||||
[ $n -eq 0 ] && echo CLEAR
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -o pipefail
|
||||
# Landing gate template (2026-10-14): sed the G= and OUT= lines and this comment.
|
||||
# Sequential suites, each teed to its own log; stops at the first failure.
|
||||
G=/mnt/storage/scratch/SET-ME-wt
|
||||
OUT=/mnt/storage/scratch/SET-ME-out
|
||||
cd "$G" || exit 9
|
||||
mkdir -p "$OUT" || exit 9 # a missing OUT failed the row 142 gate at step 1 (2026-10-15)
|
||||
# test-task.sh live cases run `docker compose` from $G, so without a name the project is "wt"
|
||||
# and leaves wt_default behind (row 142 gate, 2026-10-15). Name it per gate dir and remove it on exit.
|
||||
export COMPOSE_PROJECT_NAME="$(basename "$(dirname "$G")")"
|
||||
trap 'docker network rm "${COMPOSE_PROJECT_NAME}_default" >/dev/null 2>&1 && echo "removed network ${COMPOSE_PROJECT_NAME}_default"' EXIT
|
||||
# A gate tree without the node_modules link fails cli tests with ENOENT (2026-10-13).
|
||||
[ -d "$G/node_modules" ] || { echo "STOP no node_modules in $G"; exit 8; }
|
||||
echo "base $(git rev-parse --short HEAD) dirty $(git status --short | grep -vc node_modules) DOCKER_HOST=${DOCKER_HOST:-unset}"
|
||||
run() { local name="$1" dir="$2" cmd="$3"
|
||||
(cd "$G/$dir" && bash -c "$cmd") > "$OUT/$name.log" 2>&1; local rc=$?
|
||||
echo "$name rc=$rc $(grep -E '^ℹ (pass|fail) [0-9]+' "$OUT/$name.log" | tr '\n' ' ') | $(tail -n1 "$OUT/$name.log")"
|
||||
[ $rc -eq 0 ] || { echo "STOP $name"; exit $rc; }; }
|
||||
# No pkg-queue step: scripts/test-queue.sh runs packages/queue/tests/ itself (same 156 tests; CI debate item 2a, Vikunja #274).
|
||||
for d in packages/*/; do p=$(basename "$d"); [ -d "$d/tests" ] || continue; [ "$p" = queue ] && continue
|
||||
run "pkg-$p" "packages/$p" 'node --test tests/*.test.mjs'; done
|
||||
for s in scripts/test-*.sh; do n=$(basename "$s" .sh); run "script-${n#test-}" . "bash $s"; done
|
||||
echo GATE_PASSED
|
||||
@@ -0,0 +1,36 @@
|
||||
# Row 189 gate timing (for row 197, Herb)
|
||||
|
||||
Sage, 2026-10-20T19:25:04Z. Gate on 0c177e9ef (base 0217b71c8), one lane. The slot was mine. Seats may have run single test files at the same time, which the slot rules allow, and I did not record that.
|
||||
Start 2026-10-20T19:09:37Z, end 2026-10-20T19:20:36Z. Total wall time: 659 s.
|
||||
|
||||
Wall time per suite is the gap between one suite's log mtime and the one before it (the first suite starts at start.txt), so it includes node start-up. node's duration_ms is listed where the suite prints it. Scripts print no duration.
|
||||
|
||||
| Suite | Wall s | duration_ms s | Ended (UTC) |
|
||||
|---|---|---|---|
|
||||
| pkg-bus | 3 | 3.8 | 19:09:40 |
|
||||
| pkg-business | 3 | 2.2 | 19:09:43 |
|
||||
| pkg-cli | 388 | 388.5 | 19:16:11 |
|
||||
| pkg-control-board | 1 | 0.8 | 19:16:12 |
|
||||
| pkg-conversation | 35 | 35.4 | 19:16:47 |
|
||||
| pkg-discord | 3 | 2.6 | 19:16:50 |
|
||||
| pkg-harness | 12 | 11.6 | 19:17:02 |
|
||||
| pkg-ledger | 13 | 13.5 | 19:17:15 |
|
||||
| pkg-mosaic | 3 | 2.3 | 19:17:18 |
|
||||
| pkg-plugins | 0 | 0.3 | 19:17:18 |
|
||||
| pkg-project | 1 | 1.2 | 19:17:19 |
|
||||
| pkg-runs | 1 | 0.8 | 19:17:20 |
|
||||
| pkg-seat | 6 | 6.2 | 19:17:26 |
|
||||
| pkg-tasks | 1 | 1.2 | 19:17:27 |
|
||||
| pkg-webui | 58 | 57.9 | 19:18:25 |
|
||||
| script-auth | 1 | - | 19:18:26 |
|
||||
| script-conductor | 5 | - | 19:18:31 |
|
||||
| script-config | 2 | - | 19:18:33 |
|
||||
| script-discord | 8 | - | 19:18:41 |
|
||||
| script-extension-package | 1 | - | 19:18:42 |
|
||||
| script-foundation | 40 | - | 19:19:22 |
|
||||
| script-land-work | 11 | - | 19:19:33 |
|
||||
| script-queue | 21 | - | 19:19:54 |
|
||||
| script-release | 11 | - | 19:20:05 |
|
||||
| script-task | 31 | - | 19:20:36 |
|
||||
|
||||
Source: /mnt/storage/scratch/sage-g189/out/*.log mtimes and gate.log. The cli suite is about 59% of the run, and webui plus conversation add about 14%.
|
||||
@@ -338,10 +338,14 @@ them. Each entry has its own issuer, client, algs and subjects, and a session re
|
||||
1. Back up the store, the pointer, the unit and config.json.
|
||||
2. Pin the phase 1 copy with the Console stopped. The copy's loader accepts a config with or without
|
||||
`console`, so the bus and seats keep reading the old config.
|
||||
3. Jason step 4: the `console.sso` key goes into config.json, written by Jason or by an agent on his go
|
||||
3. Straggler check (Mos's C1 condition, mos-0977): list every running process, unit and timer that
|
||||
still runs from the old copy and reads config.json. Each one is restarted on the new copy or shown
|
||||
not to re-read config. The old loader refuses the key, so a straggler would fail on its next read.
|
||||
Step 4 waits until the list is clean.
|
||||
4. Jason step 4: the `console.sso` key goes into config.json, written by Jason or by an agent on his go
|
||||
for that one key. The new loader reads it back (`scripts/mosaic-config.mjs` validate) before anything
|
||||
starts.
|
||||
4. Start the Console with `--sso --send --tester-port 7333`.
|
||||
5. Start the Console with `--sso --send --tester-port 7333`.
|
||||
Acceptance: Jason signs in through the Console flow, sends one message and signs out. The forged-header
|
||||
check (a request with `X-authentik-username` and no cookie gets 401) runs either on a scratch Console or
|
||||
on live by Mos or Jason (B6). No agent sends the auth header to the live 7332 (section 8). The scratch
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Row 189: `stack-release.sh activate --no-send`
|
||||
|
||||
Gitea #1687. Vikunja P19 #413 (Q175 interim, #412). Brief: docs/plans/2026-10-20_activate-no-send.md.
|
||||
Rulings: mos-0972 (the Console runs without `--send` until Q175 phase 2), mos-0973 (go; Sage builds,
|
||||
Dewey reviews; gate is the cli suite plus every scripts/test-*.sh). Base: def2e7932 (refactor, the
|
||||
brief commit on 9ae8c73ac).
|
||||
|
||||
## What it does
|
||||
|
||||
`activate ID BUSINESS --no-send` drops `--send` from `RUN_CONSOLE` in a new `send_for_target`, which
|
||||
`console_for_target` calls after `proof_for_target`. `install_stack` renders the unit from
|
||||
`RUN_CONSOLE` and `write_pointer` writes the pointer from it, so both lose the flag and `flags_drift`
|
||||
stays quiet afterwards. Only `activate` takes the flag; the parser sends every other op to usage
|
||||
(exit 4).
|
||||
|
||||
`check_target` lets a `--no-send` activate name the pointer's copy only when the pointer's Console
|
||||
flags hold `--send`. That run is a live-copy re-pin, the same path row 187 uses for the proof pair:
|
||||
the plan prints the live-copy note, the run stops both units, backs up the stopped store and starts
|
||||
them again. A plain same-copy activate, or a same-copy `--no-send` once `--send` is gone, still
|
||||
refuses `already-active`.
|
||||
|
||||
`--plan` prints one `send:` line after the `proof:` line, for activate and rollback:
|
||||
|
||||
- `send: dropped; --no-send was given`
|
||||
- `send: off; --no-send was given and the pointer holds no --send`
|
||||
- `send: carried` (the pointer's `--send` goes on unchanged)
|
||||
- `send: off`
|
||||
|
||||
The steps log gets `drop --send; --no-send was given`, or `--no-send was given and the pointer holds no
|
||||
--send, so there is nothing to drop`.
|
||||
|
||||
Rollback and pm-on carry the pointer's flags as they are, so once `--no-send` has run, nothing adds
|
||||
`--send` back. There is no re-enable flag (mos-0973). Q175 phase 2 changes the Console flags in its
|
||||
own row.
|
||||
|
||||
docs/TOOLS.md: `[--no-send]` in the usage block and one paragraph after the proof paragraph.
|
||||
|
||||
## Tests (packages/cli/tests/stack-release.test.mjs, T55)
|
||||
|
||||
1. Seeded preview pointer with `--send --tester-port 7333`: a same-copy `activate B acme --no-send`
|
||||
passes. The pointer and the unit ExecStart lack `--send` and keep the other flags in order, status
|
||||
says the unit flags match, the steps log has the drop line, the log line has from = to = B and the
|
||||
backup sum equals the stopped store's, and the units stop and start in order.
|
||||
2. Plain same-copy activate, with `--tester-port` and with `--no-proof`, refuses `already-active`. So does
|
||||
a same-copy `--no-send` once `--send` is gone, and on a pointer that never held it.
|
||||
3. The four `send:` lines, the live-copy note on the same-copy plan only, and the Console line right
|
||||
after `send:` without `--send`. The run on a pointer with no `--send` logs the nothing-to-drop line
|
||||
and no drop line.
|
||||
4. After `--no-send` on C1, a rollback to B leaves `--send` out of the pointer and the unit, the steps
|
||||
log never names `--send`, and status matches. pm-on after `activate --pm-after-harness --no-send`
|
||||
keeps it off.
|
||||
5. `--no-send` on rollback (with and without `--plan`), pm-on, status, adopt and rehearse is a usage
|
||||
error with the usage text, nothing written, started or logged. The usage text shows `[--no-send]`.
|
||||
6. A pointer holding both the proof pair and `--send`: the plans for `--no-proof`, `--no-send` and both
|
||||
name what each drops and carries. `--no-send` keeps the pair, `--no-send` with a new pair replaces it,
|
||||
`--no-proof` keeps `--send`, and both together leave neither. Status matches after each.
|
||||
|
||||
`planTail` gains `'send: off'` after `'proof: off'`, because every plan now prints the line. T36 takes
|
||||
`out.slice(-n)` around `planTail`, so its four slice counts grow by one.
|
||||
|
||||
## Mutants
|
||||
|
||||
mutants.sh in this packet, run on the candidate. Each mutates scripts/stack-release.sh and runs T55
|
||||
plus T1 and T2.
|
||||
|
||||
- M1: the same-copy exception ignores `--no-send`.
|
||||
- M2: the same-copy exception allows `--no-send` without `--send` in the pointer.
|
||||
- M3: with `--no-send`, the pointer is written from `P_CONSOLE`, so it keeps `--send` while the unit
|
||||
drops it.
|
||||
- M4: rollback re-adds `--send`.
|
||||
- M5: the parser accepts `--no-send` for rollback.
|
||||
|
||||
Results: mutants-run.log, all five killed (M1 5 failures, M2 2, M3 7, M4 2, M5 2).
|
||||
|
||||
## Deviations from the brief
|
||||
|
||||
- The brief's line numbers were taken before row 163 landed. The candidate corrects them to the
|
||||
9ae8c73ac lines (check_target :249-256, flags_drift :646-657, console_for_target :664-693, adopt
|
||||
:828).
|
||||
- Test 5 also covers rehearse and `rollback --plan`, which the brief did not list.
|
||||
|
||||
## Outbound calls
|
||||
|
||||
None. The change edits an array before the existing unit render and pointer write. Every test uses
|
||||
the stand-in systemctl, units, stores and data root the file already builds; no test touches
|
||||
`~/.mosaic-dev`, the live units or a slot file.
|
||||
@@ -0,0 +1,5 @@
|
||||
M1 KILLED fail=5
|
||||
M2 KILLED fail=2
|
||||
M3 KILLED fail=7
|
||||
M4 KILLED fail=2
|
||||
M5 KILLED fail=2
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
+2
-76
@@ -1361,79 +1361,5 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-20T18:34:53Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r2 (DESIGN.md blob 3eb718b5e131 at 1a268249e), static | changes: C1 config.json console key fails old loaders (mosaic-config.mjs:113 rejects unknown keys; key add inside phase 2 window, rollback removes it, loader test), C2 app.js signs out only on opaqueredirect so 401 never shows sign-in; notes on tests 15/7/9a, Q-B 200-HTML classification, scratch Console flags; Q-E keep iss/sub escaped and capped; verdict sent to Sage, no Gitea/queue record
|
||||
2026-10-20T18:40:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r3 (DESIGN.md blob a152fd58e18e at 817e8f51b), static | approve on condition D1 (slot file checks stay out of shared loader, shape-only there) as a line edit; D2 notes: slot root from configDir, scratch dataRoot, name discovery GETs for Mos; C1/C2 and notes verified; Mos C1 sequence confirm still open; no Gitea/queue record
|
||||
2026-10-20T18:41:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design D1/D2 check (DESIGN.md blob eba861f5d2bb at d55d86bb0), static git diff 817e8f51b..d55d86bb0 | ack: D1 and D2 match; design approved; Mos C1 sequence confirm still open; no Gitea/queue record
|
||||
2026-10-20T18:51:09Z | Duckworth (Opus 5.5) | Gizmoduck federation design r1 review (DESIGN.md blob 0f4570d478f5 at 6925f78f0, code at 9ae8c73ac), static | changes B1-B5 (forwarded user identity, ownership code, business-wide reads, revocation ends no runs reliably, one-uid isolation); notes N1-N9; agents/duckworth/work/federation-r1/REVIEW.md
|
||||
2026-10-20T18:58:59Z | Duckworth (Opus 5.5) | Gizmoduck federation design r2/r2.1 re-review (5ad67f671, parents 60b9b112a, 6925f78f0; code 9ae8c73ac), static | changes B6 only (user from_role breaks the runner reply path, runner.mjs:450 vs broker.mjs:1862); B1-B5 resolved; C1 line edit (systemd-run escape); N10; agents/duckworth/work/federation-r1/REVIEW-r2.md
|
||||
2026-10-20T19:03Z | Duckworth (Opus 5.5) | federation design r3+r3.1 re-review (Gizmoduck 5ad67f671..a2840a6be), duckworth-0003 | changes B7 (R8 gate lacks sudo/ssh privilege-path checks in both isolation forms), B1-B6 resolved, N11 #waiting, N12 thread_shares
|
||||
2026-10-20T19:10Z | Duckworth (Opus 5.5) | federation design r4 re-review (Gizmoduck a2840a6be..a659e41f3), duckworth-0005, plus mos-1007 rulings | changes B8 (hub issuer and TLS front end not in R8 gate), B7/N11/N12 resolved, sudo deviation accepted, N13-N15
|
||||
2026-10-20T19:14Z | Duckworth (Opus 5.5) | row 196 design r1 review (duckworth-0006), Gander 76b5c8d93 | changes B1 (concurrent peer-binding test), Q1 O1 with interpreter pin, Q2 Console provenance approved
|
||||
2026-10-20T19:16Z | Duckworth (Opus 5.5) | federation design r4.1 re-review (duckworth-0007), Gizmoduck a659e41f3..73677b87d | changes, B8 issuer-host point left; TLS, N13-N15 resolved; notes N16-N19
|
||||
2026-10-20T19:18Z | Duckworth (Opus 5.5) | row 193 design r2 review (duckworth-0008), Morgana 19c015741 | changes B1 (5.7 owner-visible reads), B2 (5.7 filters before user threads go live); rulings 1-6
|
||||
2026-10-20T19:19Z | Duckworth (Opus 5.5) | row 196 design r2 re-review (duckworth-0009), Gander 76b5c8d93..5fe259f16 | approve; note N5 interpreter change on restart
|
||||
2026-10-20T19:20Z | Duckworth (Opus 5.5) | federation design r4.2-r4.4 re-review (duckworth-0010), Gizmoduck 73677b87d..b67bc6de9 | changes B9 (user-thread decision route :1683); B8, N16-N19 resolved; note N20
|
||||
2026-10-20T19:22Z | Duckworth (Opus 5.5) | row 193 design r3 re-review (duckworth-0011), Morgana 19c015741..76f4605e1 | changes B3 (5.7 table omits project.file reads), B1 B2 N1-N4 resolved
|
||||
2026-10-20T19:22Z | Duckworth (Opus 5.5) | federation design r4.5 re-review (duckworth-0012), Gizmoduck b67bc6de9..acd5f1f42 | approve, B9 N20 resolved, note N21 R6 notice wording
|
||||
2026-10-20T19:25Z | Duckworth (Opus 5.5) | row 199 design r1 review (duckworth-0013), Megavolt 624e8ea3a | changes B1 (missing store file stops bus start), B2 (token not bound to remote host, redirects), notes N1-N5
|
||||
2026-10-20T19:25Z | Duckworth (Opus 5.5) | row 193 design r4 re-review (duckworth-0014), Morgana 76f4605e1..edc794933 | approve, B3 and N5 resolved
|
||||
2026-10-20T19:29Z | Duckworth (Opus 5.5) | row 199 design r2+r3 re-review (duckworth-0017), Megavolt c2c729ddf | changes B3 (T3 fake is http, https-only flags untested), B4 (decision binds no commit, push by ref name)
|
||||
2026-10-20T19:29Z | Duckworth (Opus 5.5) | row 193 design r5 re-review (duckworth-0018), Morgana e2d161e2e | approve
|
||||
2026-10-20T19:29Z | Duckworth (Opus 5.5) | federation design r4.6 re-review (duckworth-0019), Gizmoduck 1b32e65e3 | approve
|
||||
2026-10-20T19:30Z | Duckworth (Opus 5.5) | federation design r4.7 re-review (duckworth-0020), Gizmoduck 2d3c3cfbd | approve, N22 (R8 checks the mosaic user's properties)
|
||||
2026-10-20T19:31Z | Duckworth (Opus 5.5) | federation design r4.8 re-review (duckworth-0021), Gizmoduck eb651aa68 | approve, N22 resolved
|
||||
2026-10-20T19:33Z | Duckworth (Opus 5.5) | row 199 design r4 re-review (duckworth-0022), Megavolt e1aa6216c | approve, N9 (post-routing source commit)
|
||||
2026-10-20T19:36Z | Duckworth (Opus 5.5) | Q180 tightening plan r2 review (duckworth-0023), ops-01 plan sha256 1084aa15, #421 | changes B1-B5 (lockout proof, new key off factory hosts, live group removal, cloud-init order, unlisted paths)
|
||||
2026-10-20T19:36Z | Duckworth (Opus 5.5) | federation r4.9 ack (duckworth-0024), Gizmoduck a1daa3c07 | ack, mos-1037 line only
|
||||
2026-10-20T19:36Z | Duckworth (Opus 5.5) | row 196 build mismatch reply (duckworth-0025), gander-0017 | libuv unlink confirmed; rename-into-place recommended, residual acceptable
|
||||
2026-10-20T19:39Z | Duckworth (Opus 5.5) | row 199 design r5 review (duckworth-0026), Megavolt 34bfe4a53 DESIGN.md sha256 d0b326bb | changes B5 (D9a ls-remote and push still name the upstream, not the derived fork), B6 (state the URL parse, name rule, case-insensitive owner compare)
|
||||
2026-10-20T19:42Z | Duckworth (Opus 5.5) | row 199 design r6 review (duckworth-0027), Megavolt 4d3007ec9 DESIGN.md sha256 aa458dfe | approve; B5 B6 N10 N11 resolved, D9c normalization claims checked in node
|
||||
2026-10-20T19:44Z | Duckworth (Opus 5.5) | Q180 tightening plan r3 review (duckworth-0028), ops-01 plan sha256 cd7165bf, #421 | changes B6 (root persistence sweep on all four hosts or rebuild 1012); B1-B5 N1-N3 resolved
|
||||
2026-10-20T20:02Z | Duckworth (Opus 5.5) | #423 VM tightening r4 steps 1-3 review (duckworth-0032), ops-01 plan sha256 03cd1075 (amends eadff6b6) | changes B1 (step 2 verify must read sshd -T permitrootlogin and root's authorizedkeysfile); N1-N7; steps 4-5 not reviewed (held, mos-1061/1062)
|
||||
2026-10-20T20:02Z | Duckworth (Opus 5.5) | row 201 FED R1 manifest review r1 (duckworth-0033), Gizmoduck 80fe31071 | approve; digests match, access.test.mjs 24/24 in scratch worktree; deviation 2 reading confirmed; N1 covers without level, N2 checked grants only
|
||||
2026-10-20T20:02Z | Duckworth (Opus 5.5) | federation design r4.10 + r4.11 ack (duckworth-0034), Gizmoduck 3568441ab and b93f098f0 | ack; R8 recorded acceptance, quotes checked vs DECISIONS Q182/Q183, call A blocking agreed, call B correct; N1 N2 doc-only
|
||||
2026-10-20T20:05Z | Duckworth (Opus 5.5) | #423 VM tightening r5 steps 1-3 diff review (duckworth-0035), ops-01 r5 sha256 4cbdc48e on base 03cd1075 | approve steps 1-3; B1 N1-N7 resolved, recount 18 checked; a #424-changed step 1 returns as a diff; N8-N10
|
||||
2026-10-20T20:11Z | Duckworth (Opus 5.5) | row 204 (193a) round 1 manifest review, morgana/row193a 0caf6e2f6, digest d38de8c9 | approve (duckworth-0037), N1-N3 notes, 5/5 and 26/26 single-file
|
||||
2026-10-20T20:14Z | Duckworth (Opus 5.5) | PRD 1.7 draft REQ-CRED-3 review, honker/row194 0bb1a3bd0 sha256 0236dee8; row 204 queue record rev 2335 | changes B1-B2 (duckworth-0038), quotes match DECISIONS 1c368ff
|
||||
2026-10-20T20:16Z | Duckworth (Opus 5.5) | PRD 1.7 draft r2 diff check, honker/row194 11cb0423b sha256 e896746d | approve (duckworth-0039), B1-B2 resolved
|
||||
2026-10-20T20:18Z | Duckworth (Opus 5.5) | row 202 FED R5 round 1 manifest review, quackerjack/fed-r5 030809e15 manifest 959bfb275 | changes B1 (duckworth-0040), BusinessError from loadAccess/loadBusiness escapes uncaught (probed), N1-N9, 18/18 single-file
|
||||
2026-10-20T20:24Z | Duckworth (Opus 5.5) | row 202 FED R5 round 2, Quackerjack candidate 48a3fb96e manifest 7fd265033 | approve (duckworth-0041), B1 resolved, 21/21 single-file, N1-N4
|
||||
2026-10-20T20:34Z | Duckworth (Opus 5.5) | row 210 (#1707) DESIGN.md r1, Gizmoduck 7b10bc0ff | changes B1-B4 (duckworth-0042): argv not in event log, decision stdin unseen, orphan ssh-agent after bus death, section 4 not routed to Jason; N1-N8
|
||||
2026-10-20T20:39Z | Duckworth (Opus 5.5) | row 210 DESIGN r2 diff check, Gizmoduck b726e10f4 | approve with C1 (duckworth-0043): B1-B4 resolved, Q-J0 revoke line must match section 6 before Mos sends it; N1-N5
|
||||
2026-10-20T20:42Z | Duckworth (Opus 5.5) | row 210 DESIGN r3 C1 diff check (duckworth-0044) on Gizmoduck e021dfa26 | C1 met, approve stands, N1
|
||||
2026-10-20T20:43Z | Duckworth (Opus 5.5) | #423 r5 step 4 .53 jarvis sudo and groups (duckworth-0045), reboot timing per mos-1092 | approve with C1-C4, reboot stands in for B3 restart
|
||||
2026-10-20T20:45Z | Duckworth (Opus 5.5) | #423 step 4 scripts (step4.sh, check4.sh, postboot.sh, boot-session optional block) vs duckworth-0045 (duckworth-0046) | C1 C3 met, C2 C4 open, N1-N2
|
||||
2026-10-20T20:47Z | Duckworth (Opus 5.5) | row 195 DESIGN r2 review (duckworth-0047) on Darkwing 818b79696 | changes B1 (.53 activate, gate, stopped-store backup), N1-N6
|
||||
2026-10-20T20:49Z | Duckworth (Opus 5.5) | #423 step 4 revised scripts (step4.sh 5aea3856, check4.sh 8f49a409, containers.sh 837bb7a4, postboot.sh, boot-session f0645604) (duckworth-0048) | C1-C4 met, C5 stale shas in boot-session block, N1-N3
|
||||
2026-10-20T20:51Z | Duckworth (Opus 5.5) | #423 step 4 N1/N2 diff and C5 check, step4.sh cc16d384, boot-session ed44ec81 (duckworth-0049) | holds, no new condition
|
||||
2026-10-20T20:59Z | Duckworth (Opus 5.5) | row 195 DESIGN r3 review, Darkwing 9a16d7795 blob 0fea86616b36 (duckworth-0050) | changes, B1 ACT/TO hand-off, B2 gate poll and PM journal read
|
||||
2026-10-20T21:11Z | Duckworth (Opus 5.5) | row 195 DESIGN r4 review, Darkwing 0075be358 blob 3f8590016b40, with mos-1098 items (duckworth-0051) | changes, B1 rollback open-activation, B2 profile upgrade owner
|
||||
2026-10-20T21:22Z | Duckworth (Opus 5.5) | row 210 round 1 code review (duckworth-0052) on Gizmoduck 12ffe0b56 | changes B1 (sshd skip passes the gate), N1-N5; 17 singles 134/0
|
||||
2026-10-20T21:33Z | Duckworth (Opus 5.5) | row 210 round 2 diff check (duckworth-0053) on Gizmoduck 455964f23 | approve, B1 met (missing sshd fails 13, skipped 0), N1-N3 taken; 17 singles 137/0/0 skipped
|
||||
2026-10-20T21:45Z | Duckworth (Opus 5.5) | row 210 queue record on comment 29702, candidate 455964f23 | approve recorded, rev 2466 (duckworth-r210-record-1); rev left uncommitted for Sage's queue-commit
|
||||
2026-10-20T21:50Z | Duckworth (Opus 5.5) | row 195 DESIGN r5 review (duckworth-0054), Darkwing 0b6d29e90 blob 9395e56d1775 | changes B1 (confirm result line lacks op/from/to/backup, from_log misses .53 activations) B2 (migration record after migrate, partial migrate never restored); r4 B1 B2 N1-N4 met; port squat checked, 732 fix fits
|
||||
2026-10-20T21:55Z | Duckworth (Opus 5.5) | row 195 DESIGN r6 diff check (duckworth-0055), Darkwing b7239bb66 blob 46bbcf2b135e | approve; B1 B2 N1-N5 met; N1 A2f stop timeout now exits 2 (state in run sheet, fix test wording), N2 claude path round separate
|
||||
2026-10-20T22:18Z | Duckworth (Opus 5.5) | machine-grant design r1 review (duckworth-0056, Vikunja #436) on Gizmoduck f6b6a04b6 | changes B1-B5: runtime grant not loaded, revoke race, restart reconcile, tag collision, host root helper unreviewed
|
||||
2026-10-20T22:21Z | Duckworth (Opus 5.5) | machine-grant r1 review correction (duckworth-0056 B2 busy map) | B2 reworded, blocker stands
|
||||
2026-10-20T22:21Z | Duckworth (Opus 5.5) | PRD 1.8 draft review (duckworth-0057, #434) on Honker 3c30f1036 | changes B1-B2: failure clause vs unreachable host, hand-edit grants vs Approve-only; REQ-CRED-4 keeps 210 section 5
|
||||
2026-10-20T22:25Z | Duckworth (Opus 5.5) | PRD 1.8 draft r2 review (duckworth-0058), Honker 60f5ca8fa | approve with C1 (account.create scope of chief-only refusal), N1-N2 |
|
||||
2026-10-20T22:32Z | Duckworth (Opus 5.5) | PRD 1.8 draft r3 diff check (duckworth-0059) on Honker d16ccdb91 | changes B1: r3 still allows hand-written grants against mos-1129; C1 met; my r2 N2 withdrawn |
|
||||
2026-10-20T22:32Z | Duckworth (Opus 5.5) | machine-grant design r3+r4 review (duckworth-0060) on Gizmoduck 1c5705d6d | changes B1-B2 (enrollment recheck, claim before consume); r1 B1-B5 still met; r4 migration lines fine; unsent r2 draft dropped |
|
||||
2026-10-20T22:33Z | Duckworth (Opus 5.5) | PRD 1.8 draft r4 diff check (duckworth-0061) on Honker 781d91624 | approve, 0059 B1 met, N1 two stale notes (doc-only) |
|
||||
2026-10-20T22:38Z | Duckworth (Opus 5.5) | machine-grant design r5 diff check (duckworth-0063), Gizmoduck 7686fd783 | approve with C1 (commands-change undo and reconcile must not revive a revoked grant), N1 orphan key file |
|
||||
2026-10-20T22:39Z | Duckworth (Opus 5.5) | machine-grant design r6 diff check (duckworth-0064), Gizmoduck f9039682e | approve, 0063 C1 met (update-only undo, late abort ignored, G28, 4 mutants), N1 start sweep taken |
|
||||
2026-10-20T22:53Z | Duckworth (Opus 5.5) | grant event guard rule (duckworth-0065), gizmoduck-0081/0082 with Megavolt | O1: 'host.enroll.*' misses host.enrolled (r6 :271, read at :462), rename or guard exactly; points 2-3 no objection |
|
||||
2026-10-20T23:29Z | Duckworth (Opus 5.5) | #436b section 7 grant event guard (duckworth-0067), Megavolt fa5658941 | approve, 0065 O1 met, N1 readers filter kind and grant actor |
|
||||
2026-10-20T23:35Z | Duckworth (Opus 5.5) | row 199 build r1 review (duckworth-0068), Megavolt 86963d281 manifest 35f900fd, request 29778 | approve; deviations 16, 17 and 13 accepted; N1 one decision to two fork refs, N2 issue needs a loaded entry (row 194), N3 business README hard-wait list doc-only |
|
||||
2026-10-21T00:34Z | Duckworth (Opus 5.5) | row 237 round 1 review (duckworth-0069), Megavolt e42415935, manifest bb777cb0 | approve, M1 killed in both files, N1-N3 non-blocking |
|
||||
2026-10-21T01:11Z | Duckworth (Opus 5.5) | row 194 scoped check of row 199 hunks (duckworth-0070), Honker b39766112 | no blocker in scope, N2 restart route met, N1-N2 notes |
|
||||
2026-10-21T01:12Z | Duckworth (Opus 5.5) | row 210 round 4 diff (duckworth-0071), Gizmoduck 3cf63efb4..c43008749, B1 import fix | approve, Sage's four checks met, roles fix confirmed needed |
|
||||
2026-10-21T01:14Z | Duckworth (Opus 5.5) | PRD 1.8 r7 scoped check (duckworth-0072) on honker/prd18-r7 47c2cb33c, sage-1624/1627 | passes the scope, no findings, citations true at 011d41ba6 |
|
||||
2026-10-21T01:18Z | Duckworth (Opus 5.5) | PRD 1.8 r7 + follow-up scoped check (duckworth-0073), Honker 762a6711f..82df17347, sage-1632/1633 | passes the scope, no findings, four mos-1152 edits exact, includes 0072 |
|
||||
2026-10-21T01:26Z | Duckworth (Opus 5.5) | row 194 b3 N1 delta scoped check (duckworth-0074), Honker a61a49a51..86889c59e, sage-1634 | N1 met, no findings; row 210 r2 approve recorded rev 2592 |
|
||||
2026-10-21T01:29Z | Duckworth (Opus 5.5) | PRD 1.7+1.8 landing review (duckworth-0075), Honker 46c8adaeb on 011d41ba6, sage-1636/1641 | approve, 42 items word-exact, no allow/forbid change, N1 CRED-4 title |
|
||||
2026-10-21T01:32Z | Duckworth (Opus 5.5) | REQ-CRED-4 title delta (duckworth-0076), Honker 46c8adaeb..c03d660e7 | approve, N1 title drops "only", 117-char wording offered |
|
||||
2026-10-21T01:34Z | Duckworth (Opus 5.5) | REQ-CRED-4 title follow-up 2884ee496 (duckworth-0077) | approve, N1 resolved, no notes |
|
||||
2026-10-21T01:59Z | Duckworth (Opus 5.5) | row 210 queue round 3 rebase check on Gizmoduck 0485384e9 (duckworth-0078) | approve, no findings |
|
||||
2026-10-21T03:01Z | Duckworth (Opus 5.5) | row 234 early check of BUILD.md split and name map on Gizmoduck 2aa54a67a (duckworth-0079) | split and map stand, G21 fixture ok on F1-F4, moves M1-M4, N1 mutant split |
|
||||
2026-10-21T03:03Z | Duckworth (Opus 5.5) | row 241 round 1 on Megavolt 3e3ed303e (duckworth-0080) | approve, no findings |
|
||||
2026-10-21T03:44Z | Duckworth (Opus 5.5) | row 234 early code read on Gizmoduck f4de24d39 (duckworth-0081) | changes: B1 G10, B2 G18, B3 G6 withdraw/arbiter and G20 human leg untested, N1-N7 |
|
||||
2026-10-21T03:46Z | Duckworth (Opus 5.5) | row 242 round 1 on Megavolt 635c10924 (duckworth-0082) | approve, N1-N4 |
|
||||
2026-10-21T03:51Z | Duckworth (Opus 5.5) | row 234 early read follow-up on Gizmoduck ad7373c1b (duckworth-0083) | B1-B3 resolved, N8-N9 |
|
||||
2026-10-20T18:57:10Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r2 (#1688, DESIGN.md sha256 5493c9df3ec0) | CHANGES to Darkwing, cc Sage: B1 claim check on MOSAIC_CONFIG-only selection, B2 shared prefix drop-in dirs; N1-N7 notes; static read, no slot, no Gitea or queue record
|
||||
2026-10-20T19:00:04Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r3 (#1688, DESIGN.md sha256 98236a1aff98) | APPROVE to Darkwing, cc Sage: B1 and B2 closed with M13 and M14, N7 settled by Sage's 48ebee0ff; static read, no slot; manifest round gets a queue review record (sage-1337)
|
||||
|
||||
+9
-1
@@ -368,7 +368,7 @@ reports checkout or copy identity and refactor drift, and writes nothing.
|
||||
```
|
||||
scripts/stack-release.sh adopt BUSINESS
|
||||
scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--no-send] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh pm-on BUSINESS [--timeout S]
|
||||
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh status BUSINESS
|
||||
@@ -480,6 +480,14 @@ copies. It checks the file with `stat` and `realpath` and never reads the value.
|
||||
or `off`. On the live copy it also notes that the run re-pins the pointer's copy
|
||||
and only the Console flags change.
|
||||
|
||||
`activate ... --no-send` (row 189, mos-0972) drops `--send` from the pointer's
|
||||
Console flags and the unit, so the Console serves reads and no human sends. It
|
||||
may name the live copy while the pointer holds `--send`; once `--send` is gone a
|
||||
same-copy `--no-send` refuses `already-active`. Later activates, `pm-on` and
|
||||
rollbacks carry the pointer, so send stays off. No flag puts `--send` back.
|
||||
`--plan` prints one `send:` line: `dropped`, `off` with or without the
|
||||
`--no-send` note, or `carried`.
|
||||
|
||||
#### A target past c216f47db: the PM after the harness kind
|
||||
|
||||
From c216f47db on, the bus launches the PM only with a `harness` kind in the
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Brief: activate --no-send drops the Console's --send
|
||||
|
||||
Sage, 2026-10-20, for Mos's ruling mos-0972 (Q175 interim, Vikunja #412). Owner Sage, reviewer Dewey, gate owner
|
||||
Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit.
|
||||
Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit. The candidate corrects them;
|
||||
the first draft took them before row 163.
|
||||
|
||||
This row builds the tool only. The live restart without `--send` is a separate step afterwards, a same-copy
|
||||
activate under Q173 with a backup first and a STATE report after. No test here touches `~/.mosaic-dev`, the
|
||||
@@ -14,11 +15,11 @@ Mos ruled that the Console runs without `--send` until Q175 phase 2, because wit
|
||||
process can forge `X-authentik-username` on 127.0.0.1:7332 and send as Jason (design section 8). On the pinned
|
||||
stack, the Console flags live in the pointer (`~/.mosaic-dev/state/stack-active.json`):
|
||||
|
||||
- `console_for_target` (scripts/stack-release.sh:658-687) carries the pointer's flags unchanged. Only
|
||||
- `console_for_target` (scripts/stack-release.sh:664-693) carries the pointer's flags unchanged. Only
|
||||
`--tester-port` and the row 187 proof pair can change.
|
||||
- `check_target` (:247-249) lets an activate name the pointer's copy only for a proof-pair change.
|
||||
- `adopt` refuses once a pointer exists (:822).
|
||||
- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:640-651) refuse
|
||||
- `check_target` (:249-256) lets an activate name the pointer's copy only for a proof-pair change.
|
||||
- `adopt` refuses once a pointer exists (:828).
|
||||
- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:646-657) refuse
|
||||
`unit-flags-drift` on every later activate, pm-on and rollback.
|
||||
|
||||
So the tool can't drop `--send` today. This row adds that one path.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Brief: a credential store per agent, own git identity, pushes to a remote
|
||||
|
||||
Sage, 2026-10-20, for mos-0974 (MVP T1 item 6) and the MVP plan
|
||||
(`jason-master-restart/projects/mosaic-mvp/PLAN.md` item 6, GAP-MAP G11 and G13; TARGET.md: "credentials store
|
||||
available for all agents; secrets protected with RLS equivalent" and "Agent uses own credentials for git and
|
||||
other actions"). Gitea #1697. Owner Megavolt (Opus 5.5, a new builder seat), reviewer Duckworth, gate owner
|
||||
Sage. Base: refactor at the brief commit. Design first. The first deliverable is
|
||||
`agents/megavolt/work/queue-ROW/DESIGN.md`.
|
||||
|
||||
This row draws a security boundary. Sage reviews the design's authority model, and Mos rules on it before any
|
||||
code is written.
|
||||
|
||||
## What exists today (GAP-MAP, measured at 9ae8c73ac)
|
||||
|
||||
- G11. Credential references exist per instance for Gitea and Vikunja only
|
||||
(`packages/business/src/credentials.mjs`, `packages/bus/src/credentials.mjs`, `vocabulary.mjs:103`). One
|
||||
Claude env file is shared by every seat.
|
||||
- G13. Agents use their own Vikunja token (`packages/tasks/src/adapter.mjs`). Git goes through the host
|
||||
socket (`packages/cli/src/git-socket.mjs`). Gitea tokens are loaded but never used.
|
||||
- The development repository's helper (`scripts/git-credential-mosaic.mjs`, wired in `.git/config` for
|
||||
git.mosaicstack.dev) pushes as one identity. Until this row lands, seats push only their own `<seat>/*`
|
||||
branches through it (mos-0981).
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. The store. Where each agent's credentials live, per instance, under the row 190 config canon. File modes,
|
||||
ownership and the read path. Which process may read which entry, and how the bus enforces that (the "RLS
|
||||
equivalent"). A process asking for another agent's entry is refused and logged.
|
||||
2. Kinds. Gitea (git over HTTPS, and the API), Vikunja, and the model provider. Say which kinds 10-27 needs
|
||||
and which wait. The model provider entry must not copy jwoltje's `claude-session.env`. If it needs a new
|
||||
provider account or subscription, flag it for Jason and don't pick one.
|
||||
3. Git identity and push. Each agent commits with its own name and email and pushes with its own token to a
|
||||
remote it is granted. Which refs an agent may push (its own branches, never the default branch or another
|
||||
agent's), and where that's enforced: the Gitea side (branch protection, token scopes) and our side. How
|
||||
this fits the host git socket, or replaces it.
|
||||
4. Lifecycle. Issue, rotate (`rotateBy`, `expires`), revoke and delete, and what row 194 (agent lifecycle) calls
|
||||
when an agent is hired or fired. A fired agent's credentials stop working at once.
|
||||
5. Who does what. Creating a Gitea user or token, or any account outside the fleet, is Jason's (Q173
|
||||
exclusions). The design lists each such step as a command or UI action for Jason. Agents never mint
|
||||
accounts.
|
||||
6. Migration. Today's seats (jarvis helper, seat tokens) keep working until each moves. The mos-0981 push
|
||||
rule ends only when Mos says so.
|
||||
7. What this row closes in G11 and G13, and what stays open.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No credential value is read, printed, logged or committed. Tests use fake tokens in scratch roots and a
|
||||
fake Gitea.
|
||||
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files, `~/.mosaic` or the live
|
||||
`.git/config` of the canonical checkout.
|
||||
- No request to git.mosaicstack.dev, tasks.woltje.com or any provider from a test.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, Mos rules on the authority model, then Duckworth approves the manifest. The bus,
|
||||
business, cli and tasks suites and every `scripts/test-*.sh` are green on Sage's gate, and the design's
|
||||
mutants are killed.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Brief: agent lifecycle from the Console and the CLI, with a per-agent hire/fire veto
|
||||
|
||||
Sage, 2026-10-20, for mos-0974 (MVP, DECISIONS Q176, Vikunja #414 "User and main agent can create and spawn
|
||||
agents" and "User maintains ability to terminate, regenerate, or create agents via WebUI or CLI ad-hoc", #415
|
||||
Case 1 "Chief of staff agent has hire/fire authority for agents as needed; unless user forbids per agent" and
|
||||
"User maintains ability to restructure organization at will"). Gitea #1692. T1 item 5. Owner Honker, reviewer Webby (mos-0981), gate
|
||||
owner Sage. Base: refactor at the brief commit.
|
||||
|
||||
Design first. The first deliverable is `agents/honker/work/queue-194/DESIGN.md`. Webby reviews it before any
|
||||
code.
|
||||
|
||||
## What exists today (GAP-MAP.md G8, G9, C1.5, C1.6, measured at 9ae8c73ac)
|
||||
|
||||
- The chief launches only listed instances (`roles/chief-of-staff.json`, harness `tools.mjs` launch,
|
||||
`broker.mjs:1968`). No user verb launches an arbitrary agent.
|
||||
- Terminate exists (`packages/cli/src/cli.mjs:447`, `instances.mjs:9-11`, webui `serve.mjs:288, 535`). There
|
||||
is no regenerate, no delete and no Console launch.
|
||||
- Instance changes need a bus restart (`instance-settings.mjs:35,75`, `organization.mjs:83`). Add and change
|
||||
only, no delete.
|
||||
- Human-only membership acts as a veto today, but there is no per-agent veto list.
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. The verbs: create (from a role and a name), launch, terminate, regenerate (a fresh session with the same
|
||||
identity and role), and delete (with what happens to its sessions, workspace and run evidence, which
|
||||
stays write-once). Each one is a human verb on the CLI and a Console action.
|
||||
2. Chief hire/fire. What the chief may do on its own, and the per-agent veto: the human marks an agent so
|
||||
the chief can't fire or replace it, and marks roles the chief can't hire. Where the veto lives and how
|
||||
the broker enforces it.
|
||||
3. Live changes. Which changes take effect without a bus restart, and how a running bus picks them up
|
||||
safely.
|
||||
4. RBAC. Human roles come from row 193 (Morgana). Until it lands, these verbs are owner-only. The design
|
||||
states the hook that row 193 fills.
|
||||
5. Per instance. Row 190 runs one instance per OS user. Lifecycle verbs act on their own instance only.
|
||||
6. The kill switch (row 191, Rocko) refuses every launch path. These new launch paths check it too.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No test touches `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or a slot file.
|
||||
- Role authority is declared in `roles/` and changes only by reviewed commits (AGENTS.md invariant 7). This
|
||||
row adds verbs, not new role authority. Any role file change is called out for review.
|
||||
- The build may chain on rows 190, 191 and 193 candidates (Q177).
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves DESIGN.md, then the manifest. The bus, cli, business and webui suites and every
|
||||
`scripts/test-*.sh` are green on Sage's gate, and the design's mutants are killed.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Federation build rows R1, R2 and R5 (#414)
|
||||
|
||||
Sage, 2026-10-20, for Mos's orders mos-1023 item 1, mos-1028 item 2 and mos-1034 item 1. The design is
|
||||
Gizmoduck's `agents/gizmoduck/work/federation/DESIGN.md` at a1daa3c07 on `gizmoduck/federation-design` (r4.9,
|
||||
sha256 a31cbebd070f9e3a5ca4ccb158b1747c6f5a83c3469855705b0026a581fb5c6a). Duckworth approved r4.5
|
||||
(duckworth-0012), r4.6, r4.7 (duckworth-0020) and r4.8 (duckworth-0021, 8a22d0897 on `duckworth/reviews`),
|
||||
and acked r4.9 (duckworth-0024). r4.7 applies Jason's Q181 hosts, r4.8 adds N22's R8 checks and r4.9 names who
|
||||
runs them. None of them changes R1, R2 or R5. Section 7's table
|
||||
sets each row's scope and test gate. Vikunja P19 #414. Seats push only their own `<seat>/*` branches
|
||||
(mos-0981).
|
||||
|
||||
The other R rows are filed when their parents have candidates: R3 after R1 and row 196, R4 after R3, R5 and
|
||||
row 193's viewer level (193a, mos-1019), R6 after #412 phase 1, R1, R2 and R3, R7 after R1-R6 and row 190.
|
||||
R8 is a live window, not a code row. Its brief is the last section here.
|
||||
|
||||
## Brief: FED R1, the access.json loader and validator
|
||||
|
||||
Owner Gizmoduck, reviewer Duckworth, gate owner Sage. Base: refactor at this brief's commit. No parent.
|
||||
|
||||
`packages/business/src/access.mjs`: the access.json loader and validator from DESIGN.md section 3.5, and the
|
||||
pure `covers`. Providers, peers and businesses come in as arguments. One read plus the file checks. The
|
||||
exported interface is what R5 and row 193a build against, so the manifest names it, and any change to it in
|
||||
review goes to Sage before it lands. Section 7 names `covers` but not the loader and validator exports or
|
||||
the context shape. Morgana's 193a (508ee4d61 on `morgana/row193a`) assumes `checkAccess(doc, {providers,
|
||||
peers, businesses})`, with `businesses` mapping an id to `{human, instances}`, throwing a `BusinessError` that
|
||||
names the key path. Use those names, or tell Morgana and Quackerjack yours before the manifest goes to
|
||||
Duckworth. Row 193a later widens the level check to `viewer` on R1's candidate (mos-1019).
|
||||
|
||||
Tests, in `packages/business/tests/`: every rule in 3.5 refuses with the key path named. That covers a
|
||||
symlink, a non-regular file, another owner, group or other write, expiry, `*`, a grant disjoint from the given
|
||||
subjects, `human` and `tester` ids, any level other than `collaborator`, and a project entry that isn't a
|
||||
UUID. For `covers`: `*` and listed ids match, while an unlisted project, instance or business doesn't, an
|
||||
expired grant doesn't, and a level filter rejects another level. One mutant per refusal, each killed.
|
||||
|
||||
Boundaries: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or any live access.json. Tests
|
||||
use scratch roots. Single test files run freely, and suites need Sage's slot.
|
||||
|
||||
Gate: Duckworth approves the manifest. The business suite and every `scripts/test-*.sh` are green on Sage's
|
||||
gate, and the mutants are killed.
|
||||
|
||||
## Brief: FED R5, mosaic access list, grant and revoke
|
||||
|
||||
Owner Quackerjack, reviewer Duckworth, gate owner Sage. Chained on R1 (Q177). The build starts now against
|
||||
R1's exported interface as the R1 manifest names it, and R5 lands after R1 and rebases onto R1's landing.
|
||||
|
||||
`mosaic access list`, `grant` and `revoke`: human proof, atomic write, revision bump, a receipt line. The help
|
||||
text states the `instances` limit and the provider-removal order (DESIGN.md section 7, row R5). R4 later makes
|
||||
these verbs call `access.reload`, so R5 leaves no other reload path.
|
||||
|
||||
Context (sage-1419). `ssoSubjects` is row 192's `console.sso.providers[].subjects` flattened to `{provider,
|
||||
sub}` pairs; the design's "`console.sso.subjects`" in 3.5 is shorthand for it. `providers` is the list of
|
||||
`providers[].id`, read through `loadSystem` and row 192's `checkConsole`. `peers` is `[]` until R2 lands, so a grant
|
||||
naming a peer home refuses. R5 rebases onto row 192's landing as well as R1's.
|
||||
|
||||
Tests, in `packages/cli/tests/`: tester and agent callers are refused, a concurrent write refuses on a
|
||||
revision conflict, the receipt line is appended, and a bad grant is refused before anything is written. One
|
||||
mutant per refusal, each killed.
|
||||
|
||||
Boundaries: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or any live access.json. The
|
||||
human proof in tests uses the scratch stand-in pattern (a scratch bus verifies its own human-cli path).
|
||||
Single test files run freely, and suites need Sage's slot.
|
||||
|
||||
Gate: Duckworth approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate,
|
||||
and the mutants are killed.
|
||||
|
||||
## Brief: FED R2, the federation key in the system config
|
||||
|
||||
Owner Quackerjack, reviewer Webby, gate owner Sage. Chained on row 192 (#412 phase 1). The build starts when
|
||||
row 192 has a candidate, on that candidate.
|
||||
|
||||
A `federation` key in `scripts/mosaic-config.mjs` with the shape in DESIGN.md section 3. A config with no
|
||||
`federation` key still loads as it does today. This changes the config schema, so the manifest lists every
|
||||
reader of the key.
|
||||
|
||||
Tests, in `scripts/test-config.sh`: a valid key loads, an unknown sub-key and a bad origin refuse, and a
|
||||
config without the key loads. One mutant per refusal, each killed.
|
||||
|
||||
Boundaries: `~/.config/mosaic-dev/config.json` is user-authored and is not touched. Tests use scratch config
|
||||
dirs only.
|
||||
|
||||
Gate: Webby approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate, and
|
||||
the mutants are killed.
|
||||
|
||||
## Brief: FED R8, the hub window on 10.1.1.54
|
||||
|
||||
A live window, not a code row (DESIGN.md section 7, row R8). The brief follows r4.11, b93f098f0 on
|
||||
`gizmoduck/federation-design` (sha256 1bcee8df0a3a8628a6646535b756c936ca6d9e7bcfa7e1dd2bf1d3f01715c8e6),
|
||||
which Duckworth acked with r4.10 (duckworth-0034). Mos closed design rounds there (mos-1072). Run by Jason and
|
||||
Mos. Nothing here authorizes a step on any host; each step waits for the window's go.
|
||||
|
||||
What changed since r4.9 (DECISIONS Q182, Q183, mos-1055, mos-1063). R8 is a recorded acceptance. It starts
|
||||
with Jason's acceptance of the factory paths to .54 on record, quoted in section 8. The privilege and host
|
||||
checks still run, and each result goes into the R8 record with the name of whoever ran it. They report and
|
||||
don't refuse. The four functional checks named at the end of section 7's R8 row still stop R8 when they fail
|
||||
(mos-1066 item 1).
|
||||
|
||||
Order (mos-1042 item 2). R8 runs after the Case 2 install on .54 (#416, due by 2026-10-27, Jason's latest-by
|
||||
date in Q176, mos-1069), after Bean and Chet are live in Case 2, and after both Hermes gateways on 10.1.1.54
|
||||
have stopped. Case 2 go-live doesn't wait on R8. The risk, stated for 10-27: federation between the two
|
||||
instances depends on Melanie's switch-over date.
|
||||
|
||||
Entry checks, all before anything starts:
|
||||
- R7, #412 phase 2 and Jason's #419 answer, as section 7 lists.
|
||||
- The four hub user checks on 10.1.1.54 (N22, mos-1035 item 1): `sudo -l -U mosaic` grants nothing, `id
|
||||
mosaic` shows only its own group, `~mosaic/.ssh/authorized_keys` is absent or empty, and no file under
|
||||
`/etc/sudoers.d` names `mosaic`. An agent or Jason runs them as localadmin (mos-1055), and the output goes
|
||||
into the R8 record with who ran them.
|
||||
- The factory privilege checks, the key fingerprint comparison and the section 8 privilege path table, each
|
||||
as section 7's R8 row states them.
|
||||
|
||||
Build notes:
|
||||
- N1 (Duckworth, duckworth-0034, mos-1072). A check that fails on a path Jason didn't name in Q182 or Q183 is
|
||||
marked "not named in Q182 or Q183" in the record, not filed under the acceptance. Examples: a factory mount
|
||||
of hub data, another agent on .54, a provider admin token. Mos takes each one to Jason.
|
||||
- N2 (Duckworth, doc-only). The Q183 and Q4 quotes in section 8 are excerpts. When the R8 record quotes them,
|
||||
it marks each omission with "…" and checks the full text at DECISIONS.md:1535-1541 in jason-master-restart
|
||||
(read only).
|
||||
|
||||
Done when every entry check has its output recorded, Jason signs in to the hub and sends one message, Melanie
|
||||
revokes, and his next request is refused.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user