Compare commits

...
Author SHA1 Message Date
jason.woltje b7239bb66e agents: row 195 Case 1 install design r6 (#1693): Duckworth r5 B1 (confirm's result lines carry the activation's op, from, to and the rollback set's BACKUP, so from_log reads .53's log; sequence 7), B2 (MIGRATING recorded after backup_store and before migrate-schema.mjs, as TAKEN today; sequence 8), N1 marker step makes $RS, N2 gate --since, N3 apparmor.d word grep baseline, N4 item 4 under systemd-run with NoNewPrivileges, N5 plan --expect-from OLD 2026-10-20 16:53:53 -05:00
jason.woltje 0b6d29e909 agents: row 195 Case 1 install design r5 (#1693): Duckworth r4 B1 (one result line per activation, FAILED file and --cause, marker written first, refusals log no activation, real-sequence tests), B2 per mos-1098/mos-1101 (case1-pins.sh and case1-hostcheck.sh, seven-item drift stop with apt hook and boot unit, ops-01 owner, bwrap closed on drift), N1-N4; Console on 127.0.0.1:732 with CAP_NET_BIND_SERVICE, tester 7333 as P10; chains on row 190 r3 and row 230 (C2-13a), node under /opt, four factory profile items for Gosalyn 2026-10-20 16:46:21 -05:00
jason.woltje 0075be3581 agents: row 195 Case 1 install design r4 (#1693): Duckworth B1 (root holds only the go's values and the unit files; confirm and revert find the one open activation, AWAITING-UNITS; rollback --to checked against from_log previous; A2 failure path; root step log), B2 (confirm runs gate with --timeout; .53 runs the PM; external PM check reads launches list with ActiveEnterTimestamp, no journal; pm-on refuses), N1-N5; mos-1096 bwrap-userns-restrict from the pinned deb, dpkg-statoverride root:mosaic 0750, P9 accepted by Mos; gosalyn-0046 two units and args file; managed settings for DISABLE_AUTOUPDATER; sage-1481 C2-13a 2026-10-20 16:08:53 -05:00
jason.woltje 9a16d7795d agents: row 195 Case 1 install design r3 (#1693): Duckworth B1, one ordered .53 activate with a runner per step (case1-repin.sh, stack-release --units external with confirm and revert), stopped-store backup under $DATA/backups/stack; N1-N6; mos-1091 4a-4c with the bwrap AppArmor profile; gosalyn-0034 C2-13 items 2026-10-20 15:56:54 -05:00
jason.woltje 818b79696d agents: row 195 Case 1 install design r2 (#1693, brief blob 45be7f09af, Sage rulings sage-1451) 2026-10-20 15:38:55 -05:00
jason.woltje 088087ac96 queue: revs 2248-2361+. Sage: rows 210 (#424 host access, Gizmoduck) and 211 (#426 force-push guard, Rocko) added and briefed; row 195 re-briefed to the Case 1 install on .53; review requests for 200 r1/r2 and 202; plus earlier Sage revs. Other seats' revs: Launchpad 2254, 2283-2284, 2286, 2311-2313, 2328-2329, 2344, 2347-2348 (rows 192/200); Honker 2255; Morgana 2257, 2320, 2326-2327 (row 204); Gander 2262; Herb 2265; Dewey 2268; Bushroot 2273; Megavolt 2276; Webby 2303, 2316, 2339, 2355 (row 192/200 verdicts); Gizmoduck 2304, 2318-2319 and the row 210 claim (rows 201/210); Quackerjack 2317, 2341-2342 (row 202); Duckworth 2323, 2335, 2353 (rows 201/204/202 verdicts); Darkwing 2354, 2357 (row 190 in-review) 2026-10-20 15:34:55 -05:00
jason.woltje a0fbefd48d plans: pre-push force-push hook and failing busy check brief (Vikunja #426, mos-1078, mos-1083; owner Rocko, reviewer Dewey) 2026-10-20 15:31:17 -05:00
jason.woltje f24f2cd40d plans: row 195 brief becomes the Case 1 install on 10.1.1.53 (Q181 hosts, Q182/Q183 recorded acceptance, Q185 existing account, chained on rows 190 and C2-13; ops-01 root steps under #423 step 1; sage-1439) 2026-10-20 15:24:33 -05:00
jason.woltje 9ba31b5b3f agents: Honker's PRD 1.7 draft r2 on refactor for the prd.approve raise (11cb0423b, sha256 e896746df595d1b5155130ca163fbf63b0dc38a87a8dd154af77f1a6d1328647, Duckworth approved duckworth-0039, Vikunja #420) 2026-10-20 15:20:37 -05:00
jason.woltje 9db578ff54 plans: agent host access with sudo through the credential store brief (#1707, Vikunja #424, Q183/Q184, chained on row 199) 2026-10-20 15:09:59 -05:00
jason.woltje 8eb9ddd12c plans: FED R8 brief to r4.11 (b93f098f0, recorded acceptance, Duckworth N1/N2, #416 date), R5 context source (sage-1419), 193a makes covers level required (duckworth-0033 N1) 2026-10-20 15:06:22 -05:00
jason.woltje ae3c36f248 agents: Gizmoduck seat dir and roster row (FED R1 owner, #414) 2026-10-20 14:42:15 -05:00
jason.woltje a2af7ab495 plans: FED R1, R5, R2 and R8 briefs (#414, r4.9 a1daa3c07) and row 193a-d build briefs (#1691); Quackerjack seat dir and roster row 2026-10-20 14:41:03 -05:00
12 changed files with 5727 additions and 77 deletions
+2
View File
@@ -23,6 +23,8 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
| 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` |
+872
View File
@@ -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`.
+9
View File
@@ -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,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.
+10
View File
@@ -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,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.
+81
View File
@@ -0,0 +1,81 @@
# Brief: a pre-push hook that refuses force-pushes, and a busy check that fails on BUSY (#426)
Sage, 2026-10-20, for Mos's order mos-1078 (Vikunja P19 #426) and its go mos-1083 item 1. Owner Rocko,
reviewer Dewey, gate owner Sage. Base: refactor at this brief's commit. Starts after row 191.
## Why
Morgana force-pushed a seat branch on 2026-10-20 (morgana-0016). Mos took it to Jason. Force-push is a
Q173 exclusion, and canon says never force-push, but nothing mechanical stops one. Mos: "Done means a
force-push from a seat worktree is refused, with a test that shows it."
Earlier the same day, a busy check chained as `| tail -1 &&` passed while the slot was BUSY. The script it
called prints BUSY or CLEAR, but it exits 0 either way.
The guard has two layers:
- (a) Gitea protection with force-push off on seat branch patterns and on refactor, next and main. ops-01
sets it up, through Mos (mos-1082). That part isn't this row.
- (b) The client hook below. It can be bypassed with `--no-verify`, so (a) is the real guard and (b) is the
early refusal.
## What ships
1. `scripts/git-hooks/pre-push` (new). For each ref line git passes on stdin:
- It refuses when the remote sha isn't all zeros and isn't an ancestor of the local sha (`git
merge-base --is-ancestor`). The refusal names the ref and both shas, and the hook exits 1.
- A new branch passes. A delete (local sha all zeros) is refused.
- Any failure of the check refuses too, for example an unknown remote object. The hook fetches nothing.
If the remote tip isn't present locally, it refuses and says to fetch first.
2. Install, following `scripts/queue-commit.sh --install-hook`. Either add a `--install-push-hook` there or
ship a small `scripts/install-git-hooks.sh`, whichever fits the existing code better. Say which in BUILD.md.
- The install copies the hook into the common `.git/hooks`, which every seat worktree shares.
- It refuses when `core.hooksPath` is set (the husky trap, 2026-10-09). It refuses when an installed
pre-push differs from the repository copy, unless `--force-install` is given.
- It never edits the installed copy in place.
3. `scripts/slot-busy.sh` (new), the shared busy check.
- It prints one BUSY line per matching process, or CLEAR.
- It exits 0 on CLEAR, 1 on BUSY and 2 on its own error.
- It matches `node --test`, `gate.sh`, `mutant`, `scripts/test-*.sh` and `docker compose` command lines.
It skips its own process and shell snapshots.
- It reads `/proc/<pid>/cmdline` and the cwd link only, never file contents under another seat's
directory.
- Usage in `docs/TOOLS.md`: `bash scripts/slot-busy.sh > LOG && gate`, with `set -o pipefail` in any
script that pipes it. Never `| tail -1 &&`.
## Tests
Add them to `scripts/test-land-work.sh` or a new `scripts/test-git-guard.sh` (say which). Run against a scratch
bare remote and a scratch clone with a worktree, never against origin:
- A rewritten branch pushed with `--force` from a worktree is refused, and the remote ref is unchanged.
- A `+refspec` push is refused.
- A fast-forward push passes.
- A new branch passes.
- A delete is refused.
- A remote tip that's missing locally is refused.
- The install refuses with `core.hooksPath` set, and refuses a differing installed hook.
- `slot-busy.sh` exits 1 with a fake `node --test` process running (a sleeping stand-in with that argv),
and 0 without one.
- `slot-busy.sh | tail -1 && false` doesn't apply here. The test runs `bash scripts/slot-busy.sh >/dev/null
&& echo ran`, and asserts it doesn't print `ran` while BUSY.
Mutants, each killed:
- drop the ancestor check
- treat an unknown remote object as passing
- let a delete pass
- install over a set `core.hooksPath`
- `slot-busy.sh` exits 0 on BUSY
## Boundaries
- Install the hook in the canonical checkout only after landing, as its own step with Sage. Tests use
scratch repositories.
- No push to origin from a test. No change to Gitea config. That's ops-01's.
- Don't touch `~/.mosaic`, `~/.mosaic-dev` or `~/.config/mosaic-dev`.
## Gate
Dewey approves the manifest. The new or changed `scripts/test-*.sh` file and every other `scripts/test-*.sh`
are green on Sage's gate, and the mutants are killed. Done when the hook is installed in the canonical
checkout and a scripted force-push from a seat worktree to a scratch remote is refused there. ops-01's
server proof (a refused force-push, plus a fast-forward push and a new branch that go through) is recorded
in Vikunja #426 by Mos.
+70
View File
@@ -0,0 +1,70 @@
# Brief: agent host access with sudo through the credential store (#424)
Sage, 2026-10-20, for Jason's rulings Q183 and Q184 relayed by Mos (mos-1056 item 3, mos-1067). Gitea #1707, Vikunja P19
#424, due 2026-10-27 (Q184: "Q5: by 10.27"). Owner Gizmoduck (design and build), reviewer Duckworth, gate owner
Sage. Chained on row 199's candidate (Q177): row 199 design r6 (4d3007ec9 on `megavolt/row199`, approved in
duckworth-0027) is not reopened. This row lands after 199 and rebases onto its landing.
## What Jason asked for
Q183, through Mos: "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... 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." Q183 also says not every agent needs that access.
Mos's requirements (mos-1067 item 1):
- An agent asks for a host credential by name.
- The stack checks a grant for that agent and that host, and does the ssh and sudo for it.
- The agent never sees the key or any secret value.
- Only agents with a grant can do it.
The PRD text is REQ-CRED-3, drafted by Honker for PRD 1.7 (sage-1422). If it misses 1.7 it goes into 1.8, and
this row lands only after the version that holds it is approved.
## What row 199 gives and what it doesn't (megavolt-0019)
It builds on 199 as 199 stands: the store layout under `<configDir>/secrets/agents/<business>/<agent>/`, the
lifecycle verbs and their `changes.jsonl` receipts, `useFor(session, service, fn)` with the D5 allowlist test,
`assertClean` on every result, and the grant shape of `publish` (a list on the role entry, checked by the
broker, plus a hard-wait action that isn't routed).
This row's own changes:
- A host kind in the closed kind set (vocabulary.mjs `SERVICES`, credentials.mjs, the broker's service check,
the CLI date flag, the remove regex), with a naming rule for one credential per host or per agent.
- A per-kind value check. An OpenSSH private key fails today's `TOKEN` pattern.
- Handing the key to ssh without the agent seeing it: an ssh-agent on a private socket, or a 0600 temp file
removed on every path, the way publish's one-shot helper works.
- Pinned host keys per host, and no trust on first use.
- The command boundary: what an agent may run under sudo, and how output returns without echoing the secret.
This is the main design work.
- The executor: a ticket and a host op running in the bus child, the same split row 199 uses for publish
(Megavolt's deviation in 199's BUILD.md).
## Steps
1. DESIGN.md first, in `agents/gizmoduck/work/queue-ROW/`. It answers each item above, names the grant shape
(`roles.<agent>.hosts` and a `host.sudo` action, or a reasoned alternative), says who grants (Jason, through
a reviewed role change, unless REQ-CRED-3 says otherwise), and lists tests and mutants. Duckworth approves it
before code. Sage reads it too, because it gives agents sudo on hosts.
2. Build on 199's candidate. Single test files run freely. Suites and mutant batches need Sage's slot.
3. The live part is not this row. Putting a key in the store and granting a host is a separate window with
Jason or Mos, after landing.
## Hosts
The first targets are `localadmin` with NOPASSWD sudo on 10.1.1.53 and 10.1.1.54 (#423 step 1, applied by
ops-01 at 20:05:47Z), and the servers agents already reach as localadmin (Q172). ops-01 owns the host side.
Tests use scratch sshd fixtures on 127.0.0.1 or fakes, never a real host.
## Boundaries
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files, the live units or `~/.mosaic`. No key is
read, copied or printed. No ssh to any real host from a test or a seat.
- No widening of row 199's kinds or regex inside row 199. Both rows touch them, so this row rebases after 199
lands.
## Gate
Duckworth approves DESIGN.md, then the manifest. The bus, business and cli suites and every
`scripts/test-*.sh` are green on Sage's gate, and the mutants are killed (one per refusal, plus one that leaks
the key into the agent's output and one that skips the host key pin).
+49 -60
View File
@@ -1,77 +1,66 @@
# Brief: Melanie's hub instance on its own host (row 195, was 190b)
# Brief: the Case 1 install on 10.1.1.53 (row 195)
Sage, 2026-10-20. Rewritten for Mos's rulings mos-0998 and mos-1005 (Vikunja #418, #419). The first version
put the hub under its own Linux user on dragon-lin. That form can't pass Duckworth's B7 there, because the
factory uid's `sudo -n` succeeds on dragon-lin (Mos's recon, brain-redux
`projects/mosaic-mvp/MELANIE-HOST-RECON.md`). Melanie's own server is ruled out too: it has 1.2 GiB RAM free
with her two Hermes bots running, and co-tenancy would share her user and her passwordless sudo. The hub runs
on a separate host. Which host is Jason's decision (#419): hardware outside the cluster, a cluster VM with
factory PVE root removed, or a cluster VM with that residual accepted. This row is written so the host choice
doesn't change it. Gitea #1693. On the 10-27 MVP path. Federation R8 (Gizmoduck's design r4, 04a6112be) needs
this row or nothing.
Sage, 2026-10-20, third version, for Darkwing's rewrite request and sage-1439. Gitea #1693.
Owner Darkwing (Opus 5.5), reviewer Duckworth (author of B5 and B7), gate owner Sage. Chained on row 190
(Q177): the hub runs under row 190's config canon. Design first. The first deliverable is
`agents/darkwing/work/queue-190b/DESIGN.md`.
Why it changed:
- The first two versions put Melanie's hub on its own Linux user, then on its own host, Jason's choice under
#419.
- Q181 settled the hosts: Case 1 (Jason's software factory) runs on 10.1.1.53 and Case 2 (Melanie's hub) on
10.1.1.54.
- Q182 and Q183 made the hub's privilege checks a recorded acceptance. They report and don't refuse
(federation R8, mos-1055, mos-1063, mos-1066).
- The Case 2 install on .54 is Gosalyn's C2-13 plus its window (#416, due by 2026-10-27). It is not this row.
This row now installs Case 1 on .53. On the 10-27 MVP path (mos-0974, T1 item 1).
Owner Darkwing (Opus 5.5), reviewer Duckworth, gate owner Sage. Chained on row 190's candidate (the config
canon) and on C2-13's candidate (the install script), Q177. It reuses C2-13's `case2-install.sh`,
`case2-precheck.sh` and `case2-unit.sh` by input, not by copy. A gap in them goes to Gosalyn as a C2-13
change, or into this row's manifest as a named, reviewed change to those files. Design first:
`agents/darkwing/work/queue-195/DESIGN.md`.
## Goal
Melanie's hub instance (Case 2, C2-13) runs on its own host. No factory agent can read or write the hub's
config, data root or secrets, reach its bus socket, or log in to the hub host. Her writing reaches the hub only
through the existing git.mosaicstack.dev remotes, books repo only.
Jason's software factory runs on 10.1.1.53 as `mosaic` under `/srv/mosaic`, in the same unit form as Case 2
(design 9, Q181). It uses row 190's config canon and the model account the factory uses today (Q185,
mos-1081 item 1). No new provider account.
## Provision (blank until Jason answers #419)
## Provision
Who provisions the host and how is left open. Sizing from Mos (mos-0998): 4 vCPU, 8 GB RAM, 64 GB disk,
internal network only. Each agent process measures 300 to 390 MB RSS on dragon-lin. The design takes the host
as given: a fresh Linux host with a named admin path, reachable by the deploying seat while the row runs.
- ops-01 runs the root steps over ssh as `localadmin`: `useradd mosaic`, `/srv/mosaic` and the unit through
`case2-unit.sh` from a root-owned checkout. #423 step 1 (localadmin with NOPASSWD sudo on .53 and .54) was
applied at 20:05:47Z (mos-1066, mos-1077), and nothing else in #423 gates this row.
- #423 step 4 (jarvis sudo) waits until Case 1 is live on .53.
- The host list comes from Q181. Agents may run read-only checks on .53 as `localadmin`.
## The design must state
1. The deploy. Exactly what Darkwing installs on the hub host and how: the stack copy, node, the units, the
config dir and data root under row 190's canon (O1: `~/.config/mosaic-NAME`, and "`~` is the home of the OS
user that runs the instance"). Which commands run as which user on the hub host, and which need its root.
2. Re-pin, rollback and backup on the hub host, and who runs them after the deploy key is gone.
3. How the hub gets model access without a copy of jwoltje's `claude-session.env` or any factory credential.
If that needs the credentials row (T1 item 6), say so and name the interim. If it needs a new subscription
or a new provider account, flag it for Jason and don't pick one (mos-1008).
4. The books repo path. Which git.mosaicstack.dev remote, which credential the hub holds for it, and that it
can't read or push any other repository.
5. The admin path and the end state. The host ends with no key the factory uid can read. The design names the
admin path that remains (for example the hypervisor console, or a key kept off the factory uid) and who
holds it.
6. The acceptance script, as code Duckworth can read. It takes Gizmoduck's list of infrastructure gate items
as its input (mos-1011), not a fixed set of checks. Each item names a path, its closed form and its
accepted form, and the script reports which form holds for each, or fails. The list covers at least:
- federation R8's separate-host form: the hub's bus socket, data root and config dir don't exist on the
factory host, the factory host mounts nothing of the hub's, and no factory agent holds a login or key
for the hub host
- B7: from the factory uid, `sudo -n`, `doas -n`, `su`, polkit, and `ssh -o BatchMode=yes` to the hub
host all fail
- the hypervisor and backup paths ops-01 measured (mos-1011: one PVE node the factory key has root on,
PBS on that node with unencrypted backups, a flat /16 LAN), each closed or accepted by Jason
7. The identity provider and TLS front end (B8, mos-1008, mos-1011). Either the factory holds no credential
that can create users, change groups, issue tokens or impersonate in the issuer the hub trusts, or the hub
uses an issuer the factory can't administer. A brand, tenant or app inside the same Authentik does not
count as separate. The same holds for the TLS front end that serves the hub. The proof is a failed admin
call from every token the factory's root can read. No credential value is printed in that proof.
1. The install as input values to C2-13's scripts: user, home, business file, roles, the human id, the
token slot path. Which commands run as `mosaic` through `sudo -u mosaic -H`, and which as root through
ops-01.
2. Where Case 1 differs from Case 2: the factory roles in place of the library business, no library sync,
and any precheck item that doesn't apply, each with a reason.
3. Model access through the existing factory account (Q185). Placing the credential on .53 is a step under
the Q173 secrets rule, done in the window. It is never copied by the script, and its value is never
printed.
4. Re-pin, rollback and backup on .53, and who runs them.
5. The acceptance checks, as code Duckworth can read. Each check names a path and gives it one of three
marks: closed, accepted under Q182 or Q183, or "not named in Q182 or Q183". An accepted path doesn't
fail the row. An unnamed one is reported to Mos, who takes it to Jason (the R8 N1 rule). The functional
checks fail the row when they fail: the unit runs as `mosaic`, the Console answers on its port on the
internal network only, and one tester message gets a reply.
## Boundaries
- Provisioning, hypervisor access and any infra or k3s change are Jason's (Q173 exclusions). Mos takes the
hypervisor residual (factory root on the PVE nodes, Q172) to Jason. It doesn't block this row.
- Melanie's server (`[email protected]`) is reference only, read only, under Jason's authorization in
mos-0995. Nothing on it changes, and her Hermes bots keep running.
- No tunnel, Traefik or other exposure outside the internal network. That waits for Jason, after Q175 SSO.
- No credential value is printed, logged or committed. Keys made for the deploy are removed at the end, and
the acceptance script proves it.
- The admin-call proof in point 7 and any call to the issuer or the TLS front end run only in a window Mos
approves, never as a test or an anonymous probe (Q175).
- Tests in the repository use scratch roots and stand-ins. The live deploy and the acceptance run on the hub
host are a window with a backup first (where there is state) and a STATE report after.
- Infra and k3s changes, exposure outside the internal network, and spend stay Jason's (Q173 exclusions).
- No change to dragon-lin's `~/.mosaic-dev`, `~/.config/mosaic-dev`, its live units or `~/.mosaic`.
- No credential value is printed, logged or committed.
- Tests use scratch roots and stand-ins. Nothing in a test reaches .53.
- The run on .53 is a window with Jason's or Mos's go, a backup first where state exists, and a STATE report
after.
## Gate
Duckworth approves DESIGN.md, then the manifest. Any repository change is green on Sage's gate (cli suite and
every `scripts/test-*.sh`), with the design's mutants killed. The row is done when the acceptance script passes
on the hub host from the factory uid, Duckworth reads its output, and Mos confirms.
every `scripts/test-*.sh`), with the design's mutants killed. The row is done when the window's acceptance
run passes on .53, Duckworth reads its output, and Mos confirms.
@@ -0,0 +1,70 @@
# Row 193 build rows: 193a, 193b1, 193b2, 193c and 193d (#1691)
Sage, 2026-10-20, for Mos's go mos-1034 item 4. Row 193's design is Morgana's
`agents/morgana/work/queue-193/DESIGN.md` at e2d161e2e on `morgana/row193` (r5, sha256
dd7fcb419ed8510e9d8d74b569183386d0942af8b68eecc0bbb5e182e677b523), approved by Duckworth (duckworth-0018).
Section 8 lists the tests and mutants, and section 9 the split. The brief behind them is
`docs/plans/2026-10-20_human-users.md`. Vikunja P19 #414/#415, Gitea #1691.
Owner Morgana for every row, reviewer Duckworth, gate owner Sage. Each row chains on its parent's candidate
(Q177) and rebases onto the parent's landing. Seats push only their own `<seat>/*` branches (mos-0981).
Single test files run freely, and suites and mutant batches need Sage's slot.
Boundaries for every row: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files, any
live access.json or `~/.mosaic`. No request to the live Console, mosaic.woltje.com or auth.diversecanvas.com.
No user thread goes live before 193b1's message filter has landed (mos-1021 B2, FED R8).
The owner-visible records are fixed by Mos (mos-1023 item 2) and named in one user sentence, which FED R6
reuses word for word: "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks
and project files, but not your messages."
## Brief: 193a, the access level check widened to viewer
Chained on FED R1. Started early on `morgana/row193a` (508ee4d61) against R1's interface as the FED brief
names it, with Sage's go (sage-1373). R1's level check accepts `viewer` and still refuses any other new
level. Tests in `packages/business/tests/` (section 8, Business), with the four mutants in Morgana's
BUILD.md. FED R4 chains on this row.
Duckworth's N1 on R1 (duckworth-0033, sage-1417): with viewer added, `covers` called with no level would let a
viewer grant cover a collaborator ask. This row makes `level` required in `covers`, with a test and a mutant that
makes it optional again. R3 and R4 call `covers` with a level.
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: 193b1, viewer verbs, board refusals and the message filter
Chained on FED R3, FED R4 and row 196's verifier. The bus gets the viewer verb list, per-grant level
checks, board reads refused to users, and the section 5.7 `trail` and `replies` filter with `#readable`
owner-only. No schema change. The governance pin test names each item in the user sentence above. Bus tests
and mutants 1, 3, 6, 10, 11 and 12 from section 8.
Gate: Duckworth approves the manifest. The bus suite and every `scripts/test-*.sh` are green on Sage's gate,
and the mutants are killed.
## Brief: 193b2, thread shares (schema 3j)
Chained on 193b1. `thread_shares`, the share verbs, the share rules of `#readable` and `#threadRead`, and the
section 5.5 share filters. Schema 3i to 3j. The table is append-only, and row 181's restore guard and
`COUNTED` include it. Bus tests for shares and the migration, and mutants 2, 4, 7, 8 and 9 from section 8.
Going live needs a `--migrate` window with Jason. Sage takes it to Mos when this row is near.
Gate: Duckworth approves the manifest. The bus suite, the cli suite and every `scripts/test-*.sh` are green on
Sage's gate, and the mutants are killed.
## Brief: 193c, mosaic access user verbs and --level viewer
Chained on FED R5. `mosaic access user add/remove` and `grant --level viewer`, only where R5 lacks them. They
refuse tester and agent callers, write a receipt line and call `access.reload` (section 8, CLI). If R5
already has all of them, this row records that and closes with no code.
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: 193d, the Console's shared list, share controls and viewer read-only
Chained on FED R6, row 192 and 193b2 (for share controls). A viewer has no composer or share control, and
with each UI check removed the bus still refuses (section 8, Webui).
Gate: Duckworth approves the manifest. The webui suite and every `scripts/test-*.sh` are green on Sage's
gate, and the mutants are killed.
+23 -3
View File
@@ -33,7 +33,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
<!-- mosaic-queue:begin -->
Generated from `docs/plans/queue.json` revision 2247 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
Generated from `docs/plans/queue.json` revision 2361 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|---|---|---|---|---|---|---|---|---|
@@ -225,9 +225,29 @@ Generated from `docs/plans/queue.json` revision 2247 by `queue render`. Do not e
| 186 | stack-release --plan: the backup line says what the run will do (P19 404, row 185 N1/N2) | darkwing; reviewers filbert | #1684 | done | — | Filbert approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the three brief mutants killed (sage) | `docs/plans/2026-10-20_plan-backup-line.md` § Brief: the activate plan's backup line says what the run will do | Landed 269134d8d (author Darkwing) on 3c5da3a91. Patch-id c97b9f6e25cd matches the r1 candidate (manifest 449e9121, Filbert approve 29419). Sage gate on b367f5730+patch 09:00:20Z-09:08:59Z: node 2681/0, scripts 422/0, queue 156, network removed; 3c5da3a91 adds only a SESSIONS line. Not live: the stack runs copy 490e2771d3d0. Follow-ups on P19 404: Filbert M6 gap, store-unclean reading. |
| 187 | stack-release activate carries the row 178 proof pair (mos-0928 Part B A0) | darkwing; reviewers filbert | #1685 | done | 186 done | Filbert approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the brief mutants killed (sage) | `docs/plans/2026-10-20_activate-proof-pair.md` § Brief: activate carries the row 178 proof pair | Landed 9a71e4e80 (author Darkwing) on d17328a55; patch-id 6b57c47bc661 matches candidate 52a5ecc1d143 and Filbert's r1 reading. Sage gate passed 10:21:55Z: cli 446/446 and every package and scripts/test-*.sh green. Filbert approve 29424, rev 2204; follow-ups N1-N4 filed on P19. |
| 188 | stack-release tests: a carried proof pair and a proof file inside the repository (P19 409, 410; gates Part B) | darkwing; reviewers filbert | #1686 | done | 187 settled | Filbert approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the brief mutants killed (sage) | `docs/plans/2026-10-20_proof-test-gaps.md` § Brief: tests for the two proof-file paths row 187 left open | Landed 8fc0ee216 (author Darkwing) on 23a609c5c; patch-id c9a56b167c07 matches candidate 86aa549bad75 and Filbert's r1 reading; stack-release.sh blob 2586a2a37cd2 unchanged. Sage gate passed 10:58Z: cli 453/453 and every package and scripts/test-*.sh green. Filbert approve 29433, rev 2212; N1 (REPO logical path) to P19 after Part B (mos-0959). |
| 189 | stack-release activate --no-send drops the Console's --send and keeps the pointer in step (Q175 interim, mos-0972, P19 413) | sage; reviewers dewey | #1687 | in-review, round 1, request posted | 163 done | Dewey approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the brief mutants killed (sage) | `docs/plans/2026-10-20_activate-no-send.md` § Brief: activate --no-send drops the Console's --send | — |
| 190 | several Mosaic Stack instances on one host: per-instance config, data root, units and ports (MVP T1.1, mos-0974, Vikunja #414) | darkwing; reviewers filbert | #1688 | in-progress | — | Filbert approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the design mutants killed (sage) | `docs/plans/2026-10-20_multi-instance.md` § Brief: several Mosaic Stack instances on one host | — |
| 189 | stack-release activate --no-send drops the Console's --send and keeps the pointer in step (Q175 interim, mos-0972, P19 413) | sage; reviewers dewey | #1687 | done | 163 done | Dewey approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the brief mutants killed (sage) | `docs/plans/2026-10-20_activate-no-send.md` § Brief: activate --no-send drops the Console's --send | Landed ace27dc5e on refactor (cherry-pick of candidate d6116771f onto a4a42dbac). Gate GATE_PASSED on 0c177e9ef (base 0217b71c8), 2026-10-20T19:09:37Z-19:20:36Z, 25 suites rc=0. patch-id ce89378718f6 for both 0c177e9ef and ace27dc5e; range-diff differs only in the message. Landed manifest hashes to 67af69bd (Dewey approve 29485). Base drift 0217b71c8..a4a42dbac: 7 doc/README files, none on manifest paths, no test reads them. Brief re-pinned to blob 29a9e6d2d. |
| 190 | several Mosaic Stack instances on one host: per-instance config, data root, units and ports (MVP T1.1, mos-0974, Vikunja #414) | darkwing; reviewers filbert | #1688 | in-review, round 1, request posted | — | Filbert approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the design mutants killed (sage) | `docs/plans/2026-10-20_multi-instance.md` § Brief: several Mosaic Stack instances on one host | — |
| 191 | one kill switch for the bus, every run, Discord and new launches, from the CLI and the Console (MVP T1.4, mos-0974, Vikunja #414) | rocko; reviewers dewey | #1689 | in-progress | — | Dewey approves DESIGN.md then the manifest; cli, bus, discord and webui suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_kill-switch.md` § Brief: one kill switch for the bus, every run, Discord and new launches | — |
| 192 | Q175 phase 1: the Console verifies SSO itself (--sso), OIDC RP with oauth4webapi 3.8.8, no live change (MVP T1.2, #412) | launchpad; reviewers webby | #1690 | in-review, round 2, request posted | — | Webby approves the manifest; webui and cli suites, scripts/test-config.sh and every scripts/test-*.sh green on Sage's gate; design section 9 mutants killed (sage) | `docs/plans/2026-10-20_sso-phase1.md` § Brief: Q175 phase 1, the Console verifies SSO itself (--sso) | — |
| 193 | many humans per business, human roles, private and shared threads, one access.json store (MVP T1.3, Vikunja #414/#415) | morgana; reviewers duckworth | #1691 | in-progress | — | Duckworth approves DESIGN.md then the manifest; bus, business, cli and webui suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users.md` § Brief: many humans per business, human roles, private and shared threads | — |
| 194 | agent lifecycle from the Console and the CLI (create, launch, terminate, regenerate, delete) with a per-agent hire/fire veto (MVP T1.5, Vikunja #414/#415) | honker; reviewers webby | #1692 | in-progress | — | Webby approves DESIGN.md then the manifest; bus, cli, business and webui suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_agent-lifecycle.md` § Brief: agent lifecycle from the Console and the CLI, with a per-agent hire/fire veto | — |
| 195 | row 195: the Case 1 install on 10.1.1.53 as mosaic under /srv/mosaic, reusing C2-13's scripts by input under row 190's canon (Q181, Q182/Q183, Q185; ops-01 root steps under #423 step 1) | darkwing; reviewers duckworth | #1693 | briefed | 190 done | Duckworth approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_hub-own-uid.md` § Brief: the Case 1 install on 10.1.1.53 (row 195) | sage-1439: rewritten to Case 1 on .53 (f24f2cd40). Chains on C2-13's candidate too once C2-13 is queued. Darkwing starts DESIGN.md after row 190's mutants. |
| 196 | the human boundary across uids and humans: Console, tester listener, broker.sock and the path-bound human proof (P19 333, before federation R3, mos-0989) | gander; reviewers duckworth | #1694 | in-progress | — | Duckworth approves DESIGN.md then the manifest; bus, webui and cli suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-boundary.md` § Brief: the human boundary across uids and humans (P19 333) | — |
| 197 | a second gate lane: two full gates at once on dragon-lin, design first (mos-0977 item 6) | herb; reviewers webby | #1695 | in-progress | — | Webby approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate, singly and in the two-lane proof (sage) | `docs/plans/2026-10-20_second-gate-lane.md` § Brief: a second gate lane, two full gates at once on dragon-lin | Order with row 198 (Sage, mos-1040 N1): 197 lands first. After B1 the shared tests are packages/webui/tests/serve.test.mjs:100 and preview.test.mjs:208. Row 198 rebases onto 197's landing before its manifest review. |
| 198 | retire the control board writer on 7331 (unauthenticated loopback writer, Q175; mos-1008) | bushroot; reviewers webby | #1696 | in-progress | 192 done, 197 done | Webby approves INVENTORY.md then the manifest; control-board, webui and cli suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_retire-control-board.md` § Brief: retire the control board writer on 7331 | Order with row 197 (Sage, mos-1040 N1): 197 lands first. Row 198 rebases onto 197's landing before its manifest review; shared tests are packages/webui/tests/serve.test.mjs:100 and preview.test.mjs:208. |
| 199 | MVP T1 item 6: a credential store per agent, own git identity, pushes to a remote (G11, G13; mos-0974) | megavolt; reviewers duckworth | #1697 | in-progress | — | Duckworth approves DESIGN.md; Mos rules on the authority model; Duckworth approves the manifest; bus, business, cli and tasks suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: a credential store per agent, own git identity, pushes to a remote | — |
| 200 | stack-release.sh activate --sso for the Q175 phase 2 window (row 192 F3; chained on 192) | launchpad; reviewers webby | #1698 | in-review, round 2, request posted | — | Webby approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_stack-release-sso.md` § Brief: stack-release.sh activates a Console with --sso (Q175 phase 2 prerequisite) | — |
| 201 | FED R1: access.json loader and validator, and covers (#414) | gizmoduck; reviewers duckworth | #1699 | in-review, round 1, request posted | — | Duckworth approves the manifest; business suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R1, the access.json loader and validator | — |
| 202 | FED R5: mosaic access list, grant and revoke (#414) | quackerjack; reviewers duckworth | #1700 | in-review, round 1, request posted | — | Duckworth approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R5, mosaic access list, grant and revoke | Chained on row 201's candidate (Q177), not its done: claimable now, lands after 201 and rebases onto its landing (Sage) |
| 203 | FED R2: the federation key in the system config (#414) | quackerjack; reviewers webby | #1701 | briefed | — | Webby approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R2, the federation key in the system config | Chained on row 192's candidate (Q177): starts on 192's candidate, lands after 192 (Sage) |
| 204 | 193a: access level check widened to viewer (#1691) | morgana; reviewers duckworth | #1702 | in-review, round 1, request posted | — | Duckworth approves the manifest; business suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193a, the access level check widened to viewer | Chained on row 201's candidate (Q177), not its done: claimable now, lands after 201 and rebases onto its landing (Sage) |
| 205 | 193b1: viewer verbs, board refusals and the message filter (#1691; also after FED R3 and R4 when filed) | morgana; reviewers duckworth | #1703 | queued | 196 done | Duckworth approves the manifest; bus suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193b1, viewer verbs, board refusals and the message filter | — |
| 206 | 193b2: thread shares, schema 3j (#1691; live needs a --migrate window with Jason) | morgana; reviewers duckworth | #1704 | queued | 205 done | Duckworth approves the manifest; bus suite, cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193b2, thread shares (schema 3j) | — |
| 207 | 193c: mosaic access user verbs and --level viewer (#1691) | morgana; reviewers duckworth | #1705 | queued | 202 done | Duckworth approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193c, mosaic access user verbs and --level viewer | — |
| 208 | 193d: Console shared list, share controls and viewer read-only (#1691; also after FED R6 when filed) | morgana; reviewers duckworth | #1706 | queued | 192 done, 206 done | Duckworth approves the manifest; webui suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193d, the Console's shared list, share controls and viewer read-only | — |
| 209 | FED R8: the hub window on 10.1.1.54, a recorded acceptance (#414, r4.11 b93f098f0; live window run by Jason and Mos, after the Case 2 install #416, Bean and Chet live and both Hermes gateways stopped) | sage; reviewers duckworth | — | queued | — | Every entry check's output is in the R8 record with who ran it; Jason signs in to the hub and sends one message, Melanie revokes, his next request is refused; Duckworth checks the record (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R8, the hub window on 10.1.1.54 | Stays queued until its entry gates near: Case 2 install on .54 (#416, by 2026-10-27), Bean and Chet live, both Hermes gateways on .54 stopped, R7, #412 phase 2, #419 answer. Filed per mos-1073 after duckworth-0034 (Sage) |
| 210 | Agent host access with sudo through the credential store: grant per agent and host, the stack does ssh and sudo, the agent never sees the key (#1707, Vikunja #424, Q183/Q184, due 10-27; chained on row 199's candidate) | gizmoduck; reviewers duckworth | #1707 | in-progress | — | Duckworth approves DESIGN.md, then the manifest; bus, business, cli suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_host-credentials.md` § Brief: agent host access with sudo through the credential store (#424) | Design runs before 199 lands (Q177 chain on 199's candidate). after cleared so Gizmoduck can claim; lands only after 199 and rebases onto it. |
| 211 | Force-push guard: scripts/git-hooks/pre-push refuses non-fast-forward and delete pushes from every worktree, and scripts/slot-busy.sh exits 1 on BUSY (#1709, Vikunja #426, mos-1078/1083) | rocko; reviewers dewey | #1709 | briefed | — | Dewey approves the manifest; the new or changed scripts/test-*.sh and every other scripts/test-*.sh green on Sage's gate; the mutants killed; hook installed in the canonical checkout with a scripted refused force-push (sage) | `docs/plans/2026-10-20_force-push-guard.md` § Brief: a pre-push hook that refuses force-pushes, and a busy check that fails on BUSY (#426) | — |
<!-- mosaic-queue:end -->
+4263 -14
View File
File diff suppressed because it is too large Load Diff