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

This commit is contained in:
jason.woltje committed 2026-10-20 16:08:53 -05:00
1 parent 9a16d7795d
commit 0075be3581
1 file changed
+292 -131
+292 -131
View File
@@ -1,46 +1,62 @@
# Row 195 design: the Case 1 install on 10.1.1.53 (r3)
# Row 195 design: the Case 1 install on 10.1.1.53 (r4)
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 (537a02392
on `darkwing/row190-r2`, round 2, the config canon) and on C2-13's candidate (Gosalyn,
`scripts/case2-install.sh`, `case2-precheck.sh`, `case2-unit.sh`), Q177. C2-13 has no candidate yet, so
the interface I read is its brief, `docs/plans/2026-10-20_case2-rows.md` "## C2-13" at ecbfd6deb
(from Gosalyn's 29e85196b).
`scripts/case2-install.sh`, `case2-precheck.sh`, `case2-unit.sh`), Q177. C2-13 has no candidate yet. The
interface I read is Gosalyn's design r10 at 4281b0064 on `gosalyn/case2-design` and gosalyn-0046. Sage
(sage-1481) has asked Gosalyn to split out C2-13a in r11: `case2-unit.sh` and `case2-precheck.sh` with
both system units, Webby's r10 render rules (`@PATH@`, `@NODE@`, `@FLAGS@`), the hardening list, and no
`[Install]` on the Console unit until Jason rules. Row 195 chains on C2-13a's candidate once that row
exists. Until r11 is approved I build against 4281b0064 and expect a rebase.
Changes from r2:
Changes from r3 (Duckworth's REVIEW-r2.md at 9ba8385a9, duckworth-0050; mos-1096; gosalyn-0046):
- Duckworth B1 (duckworth-0047, REVIEW-r1.md at 56d59c4c7). Section 4 is now one ordered list with a
runner for each step: root stops the units, `mosaic` takes the stopped-store backup and logs, root
renders and starts, `mosaic` gates, and a failure runs the list back to the previous copy in the same
run. One root command, `scripts/case1-repin.sh`, drives it. The `mosaic` half is a new
`--units external` mode of `stack-release.sh`, with `confirm` and `revert` verbs. Section 7 names
both and their tests, including tests that dragon-lin's path is unchanged. The r2 tar backup is gone.
- Duckworth N1-N6. P6 is tightened (N1), P1 matches sudo's refusal text (N2), F3 names its commands
(N3), the node download is checked against the signed `SHASUMS256.txt.asc` and a pinned sha256 (N4),
claude is pinned with the autoupdater off (N5), and the run sheet says a restore leaves the credential
in place (N6).
- Mos, mos-1091. Installing bubblewrap is an install step (4a). The user namespace fix is a per-binary
AppArmor profile for `/usr/bin/bwrap` with the sysctl left at 1, and its text, load, test and rollback
are in section 1 (4b). P5 closes at #423 step 4, which Mos moved ahead of the install (4c). Section 6
Q2 records these.
- Gosalyn, gosalyn-0034. C2-13 took `--profile factory|library`, the node and bwrap precheck lines and
the copy-id rule. Section 2 points at them instead of asking.
- B1. Root no longer needs any value that only `mosaic`'s data holds. `confirm` and `revert` take no
activation id: each finds the one open activation itself and refuses unless there is exactly one, and
that one names the copy root passed. Rollback takes `--to ID` from the go, and A1's plan refuses
unless ID is `from_log previous`. A2's failure has its own row, and root keeps its own step log.
Section 4, tests in section 7.
- B2. `confirm` runs `gate` (:522), which polls until `--timeout`. .53 runs the PM. Under
`--units external` the PM check reads `launches list` alone, with this start's time taken from the
bus unit's `ActiveEnterTimestamp`, so it reads no journal. `pm-on` and `--pm-after-harness` refuse
under external. Section 4, tests and mutants in section 7.
- UNITS is settled (gosalyn-0046, design r10): `[email protected]`, then
`mosaic-webui.service`, both from the existing templates. The full `case2-unit.sh` call is in
section 1, and root keeps its arguments in a root-owned file so a re-pin changes only the copy id.
- `DISABLE_AUTOUPDATER` goes in `/etc/claude-code/managed-settings.json`, root-owned. The session
allowlist doesn't pass the variable, and `mosaic`'s `~/.claude` doesn't reach a role session either
(`packages/seat/src/session.mjs:92-93`), so gosalyn-0046 item 3's settings file wouldn't reach one.
The sandbox mounts `/etc` read-only (`packages/seat/src/sandbox.mjs:85`). Section 1.
- N1 and mos-1096 item 2. Ubuntu 24.04 ships `bwrap-userns-restrict`. r4 installs that one file from
the pinned `apparmor-profiles` deb, checked by sha256, in place of r3's own profile. `/usr/bin/bwrap`
gets a `dpkg-statoverride` to `root:mosaic` 0750. Section 1 has both, each with its rollback, and
section 5's P9 and F5 check them.
- P9 is accepted by Mos (mos-1096 item 3). N2: P5 closes after #423 step 4. N3: the marker is
`AWAITING-UNITS`. N4: `--migrate` under external runs only with UNITS stopped, with a test. N5 is moot
now that both units come from C2-13.
- Jason's Q9 (install and upgrade) is open. Mos: carry on, since the re-pin and rollback work is
needed whatever he answers. Section 6.
## 0. What I checked on the host
Read only, as `localadmin` over ssh, 2026-10-20 about 20:40Z (the brief allows this):
Read only, as `localadmin` over ssh, 2026-10-20 between 20:40Z and 21:00Z (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, `/srv/mosaic` | absent | step 2a creates both |
| `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; apt candidate `bubblewrap 0.9.0-1ubuntu0.3` | every role session runs under bwrap (`packages/cli/src/launcher.mjs:310`) |
| user namespaces | `kernel.apparmor_restrict_unprivileged_userns = 1`, apparmor 4.0.1, no bwrap profile shipped | bwrap needs an unprivileged user namespace, which this setting refuses unless a profile allows it |
| 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) | Console, board, 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 | see section 5, path P5 |
| `jarvis` | groups sudo, docker, lxd, adm | #423 step 4 removes all four (section 5, P5) |
## 1. The install as inputs to C2-13's scripts
@@ -49,11 +65,11 @@ Instance. Row 190's canon with the default name, `dev`, as Case 2 uses it (Gosal
| Input | Value |
|---|---|
| user | `mosaic` |
| 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 file | `<configDir>/businesses/mosaic-stack.json`, the factory business (section 2) |
| business | `mosaic-stack`, file `<configDir>/businesses/mosaic-stack.json` (section 2) |
| profile | `--profile factory` (C2-13) |
| roles | the factory set in section 2 |
| human id | Jason's id, given in the window's go. Not `tester` (`broker.mjs:245`) |
@@ -62,6 +78,7 @@ Instance. Row 190's canon with the default name, `dev`, as Case 2 uses it (Gosal
| Console, board, tester ports | 7332, 7331, 7333, the default instance's ports |
| harness | claude at `/srv/mosaic/.local/bin/claude`, pinned through `MOSAIC_CLAUDE_BIN` (decision 91) |
| node | `/srv/mosaic/.local/node/bin/node`, a pinned 26.x release |
| PM | on: the bus serve flags carry `--pm` (section 4, B2) |
| 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 C2-13 already assumes
@@ -71,6 +88,22 @@ makes that a flag, not a move.
Why node lives under `/srv/mosaic`: changing `/usr/local/bin/node` would change what Hermes and anything
else on the host runs, and Q181 keeps Hermes running. A per-user node touches no other account.
The units (gosalyn-0046, design r10). 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. The install call is:
```
case2-unit.sh --user mosaic --home /srv/mosaic --business mosaic-stack --copy ID \
--node /srv/mosaic/.local/node/bin/node -- <serve flags>
```
The serve flags are `--pm` plus whatever the window's go adds. A re-install must pass the same
`--node` and serve flags, and only the copy id may change (gosalyn-0046 item 2). 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.
Node download (N4). The run sheet pins one release, its tarball name and its sha256. As `mosaic`:
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
@@ -80,47 +113,61 @@ then `sha256sum -c` the tarball and unpack. Any mismatch stops the step.
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 `DISABLE_AUTOUPDATER=1`. Where that variable lives depends on what reaches a role session: the
unit's environment only helps if the session env allowlist passes it (row A touches that allowlist for
`ZAI_API_KEY`). The build checks the launcher's allowlist. If it passes the variable, I ask Gosalyn for
it as a fixed `Environment=` line in the unit template. If not, it goes in `mosaic`'s claude settings
file (`env`), and the manifest says which. Either way F6 compares `claude --version` with the pin after
F3.
through `/etc/claude-code/managed-settings.json`, `root:root` 0644, holding
`{"env":{"DISABLE_AUTOUPDATER":"1"}}`. Root writes it in 2a'. Why there:
bwrap and the user namespace (mos-1091 4a, 4b). Two root steps, each with its rollback:
- `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.
1. `apt-get install bubblewrap` (0.9.0-1ubuntu0.3 today). Rollback: `apt-get remove bubblewrap`.
2. The AppArmor profile `/etc/apparmor.d/mosaic-bwrap`, `root:root` 0644, in the form Ubuntu 24.04 uses
for the browsers it lets create user namespaces:
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.
```
abi <abi/4.0>,
include <tunables/global>
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).
profile mosaic-bwrap /usr/bin/bwrap flags=(unconfined) {
userns,
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.
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`.
include if exists <local/mosaic-bwrap>
}
```
Load it with `apparmor_parser -r /etc/apparmor.d/mosaic-bwrap`. `aa-status` then lists
`mosaic-bwrap`. The sysctl stays at 1, so no other binary on the host gains user namespaces. Any user
who runs `/usr/bin/bwrap` gets one, though, and that is the residual Duckworth reviews with the
profile. Test: as `mosaic`, `bwrap --unshare-all --ro-bind / / true` exits 0 (C2-13's precheck line,
F4), and a role session runs under bwrap (F5). Rollback: `apparmor_parser -R
/etc/apparmor.d/mosaic-bwrap`, then `rm /etc/apparmor.d/mosaic-bwrap`, then step 1's rollback.
Full rollback runs 3, 2, 1. Tests after 2a' and again in section 5: as `mosaic`, `bwrap --unshare-all
--ro-bind / / true` exits 0 (C2-13'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.
Who runs what:
| Step | Runs as | Through |
|---|---|---|
| 2a: `useradd --system --home-dir /srv/mosaic --shell /usr/sbin/nologin mosaic`, `/srv/mosaic` 0700 | root | ops-01, `ssh localadmin@10.1.1.53`, `sudo` (#423 step 1) |
| 2a': bubblewrap and the AppArmor profile, each with the test and rollback above | root | ops-01, same path, R27 (mos-1091) |
| 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': bubblewrap, the AppArmor profile, the statoverride and the managed settings file, each with the test and rollback above | 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: node and claude under `~/.local` (above); `git clone` of the bundle to `/srv/mosaic/src/mosaic-stack`; `scripts/bootstrap.sh`; `stack-copy.sh copy`; `case2-install.sh --profile factory` with the inputs above; `case2-precheck.sh --profile factory` | `mosaic` | `sudo -u mosaic -H` from `localadmin`. `mosaic` gets no key and no login shell |
| 2b': place `claude-session.env` | Jason, or who he names (Q173 secrets rule) | section 3 |
| 2c: `case2-unit.sh --user mosaic --home /srv/mosaic --copy <id>` from `/root/mosaic-stack`, then `systemctl enable` and the first start | root | ops-01, same path |
| 2c: write `case1-unit.args`; `case2-unit.sh` with those arguments and `--copy <id>`, from `/root/mosaic-stack`; `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 |
@@ -141,7 +188,8 @@ brings a new bundle that `mosaic` fetches into its checkout.
| 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 now in C2-13 (gosalyn-0034) |
| 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 C2-13 (gosalyn-0034) |
| serve flags | `--pm` | the factory's PM dispatches its work, as on dragon-lin |
All of these are C2-13 inputs. Nothing in C2-13's scripts hardcodes a Case 1 value (gosalyn-0034 item
4). If C2-13's candidate drops any of them, the manifest names the change to `case2-install.sh` or
@@ -163,8 +211,8 @@ The pi harness stays off on .53 until P19 #383 settles (mos-0865). The business
## 4. Re-pin, rollback and backup on .53
Unit form (ruled, sage-1451 Q1): a system unit that root writes, with `User=mosaic`. `mosaic` can't
start or stop it, and root never runs code that `mosaic` can write. `stack-release.sh activate` today
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
@@ -173,63 +221,129 @@ ops-01 from `/root/mosaic-stack`. Each `mosaic` step is `sudo -u mosaic -H` runn
`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.
Root reads only exit codes, and the copy id from the unit file it installed itself. It parses nothing
`mosaic` wrote and runs nothing under `/srv/mosaic` except through `sudo -u mosaic -H`.
What root holds, and where each value comes from (B1):
The units. C2-13's brief names one unit, `mosaic-stack.service`, and doesn't say whether its
`ExecStart=` runs the bus, the Console or both. Case 1 needs both, the Console for F2 and the bus for F3.
I've asked Gosalyn. Below, UNITS is the list `case2-unit.sh` installs, one unit or two. Root stops them
in reverse order and starts them in order. The gate checks the bus process and the Console's `/healthz`
whichever way they're split.
| 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.
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` removes the file once it logs a result. So does `confirm --rollback`.
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` (C2-13's copy-id rule). Root saves the installed unit's copy id as OLD. `stack-release.sh activate ID mosaic-stack --units external --plan` exits 0 | root; the plan as `mosaic` | refuse, exit 3. Nothing has changed |
| A2 | `systemctl stop` UNITS, then wait until each is inactive, within `--timeout` | root | start UNITS, then R4. Nothing else has changed |
| A3 | `stack-release.sh activate ID mosaic-stack --units external`. It refuses `units-running` unless each of UNITS is inactive (system-manager reads, which need no privilege) and the store is stopped (`stopped`, :359). Then it captures the rollback set (pointer, wrapper, and the sha256 and `ExecStart=` of each installed unit, read only), logs `result started`, takes `backup_store` (:372) into `$DATA/backups/stack/<ACT>`, migrates under `--migrate` (sets TAKEN, as today), installs the wrapper for ID, and writes `$RS/PENDING` naming ID. It writes no pointer | `mosaic` | it puts the wrapper back, restores TAKEN if it migrated, logs `result failed code <c> rollback pending`, exits 1. Root runs R3-R4 |
| A4 | `case2-unit.sh --user mosaic --home /srv/mosaic --copy ID`, `systemctl daemon-reload`, `systemctl start` UNITS | root | R1-R4 |
| A5 | `stack-release.sh confirm <ACT> mosaic-stack --units external`. This is the gate (`gate_once`, :471), with system-manager reads where today's has `--user`: the units are active; the bus's MainPID is the host's pid, and that pid's uid is `mosaic`'s; boot status is from after this start; `/healthz` answers 200 on the Console port; `stack-copy.sh check --units external` agrees on ID. On a pass it writes the pointer and logs `result ok`, exit 0. On a failure it logs `result gate-failed code <gate> rollback pending`, exit 1 | `mosaic` | R1-R4 |
| 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` (C2-13's copy-id rule). Root reads OLD from the unit files. `stack-release.sh activate ID mosaic-stack --units external --plan` exits 0 | 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]`. It refuses `units-running` unless each of UNITS is inactive (system-manager reads, which need no privilege) and the store is stopped (`stopped`, :359), before it logs or migrates anything. Then it captures the rollback set (pointer, wrapper, and the sha256 and `ExecStart=` of each installed unit, read only), logs `result started`, takes `backup_store` (:372) into `$DATA/backups/stack/<ACT>`, migrates under `--migrate` (sets TAKEN, as today), installs the wrapper for ID, and writes `AWAITING-UNITS`. It writes no pointer | `mosaic` | it puts the wrapper back, restores TAKEN if it migrated, logs `result failed code <c> rollback pending`, exits 1. Root runs R3-R4. A refusal before `started` exits 3, and root runs A2f |
| A4 | `case2-unit.sh` with `case1-unit.args` and `--copy ID`, `systemctl daemon-reload`, `systemctl start` UNITS in order | root | R1-R4 |
| 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 logs `result gate-failed code <gate> rollback pending`, exit 1 | `mosaic` | R1-R4 |
A2f, a stop that fails or times out. Nothing of `mosaic`'s has changed, and the stack log has no
`started` line for this run. Root runs `systemctl start` UNITS in order, then `stack-release.sh gate
OLD mosaic-stack --units external --timeout S`, a new verb that runs `gate` on OLD and writes no log
line, no pointer and no file. If that passes, root logs `A2 failed, OLD running` in its step log and
exits 1. If it fails, root stops UNITS, logs `A2 failed, OLD gate failed <code>` and exits 2. The stack
log is unchanged either way, so the next run's `interrupted` check finds nothing open.
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.
`confirm` refuses `stale-start` when that time is before the `started` line of the open activation,
so a unit root never restarted can't pass.
- `copy-check` runs `stack-copy.sh check --units external`, which reads the system units.
- The PM check is below.
The PM (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 C2-13 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 <ACT> mosaic-stack --units external`. It puts the wrapper back from the rollback set and checks its sha256, and restores TAKEN (`restore_store`, :396) only if this run migrated | `mosaic` | rollback-failed |
| R3 | `case2-unit.sh --user mosaic --home /srv/mosaic --copy OLD`, `systemctl daemon-reload`, `systemctl start` UNITS | root | rollback-failed |
| R4 | `stack-release.sh confirm <ACT> mosaic-stack --units external --rollback`. It gates OLD and logs `result gate-failed code <c> rollback ok`, or `result rollback-failed code <c> rollbackCode <r>` | `mosaic` | 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 this run migrated | `mosaic` | rollback-failed |
| R3 | `case2-unit.sh` with `case1-unit.args` and `--copy OLD`, `systemctl daemon-reload`, `systemctl start` UNITS | root | rollback-failed |
| R4 | `stack-release.sh confirm ID mosaic-stack --units external --rollback OLD --timeout S`. It finds the open activation, checks OLD is its `from`, gates OLD with `gate`, and logs `result gate-failed code <c> rollback ok`, or `result rollback-failed code <c> rollbackCode <r>` | `mosaic` | rollback-failed |
rollback-failed: root stops UNITS and exits 2, as `rollback_failed` (:587) does. When R2 or R4 can't log
it, root prints the codes, and the next run's `interrupted` check (:146) names the activation that has
`started` and no result. Exit codes match `stack-release.sh`: 0 ok, 1 gate-failed and rolled back, 2
rollback-failed with UNITS stopped, 3 refused with nothing changed.
it, root's step log has the codes, and the next run's `interrupted` check names the activation that has
`started` and no result. 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 [--restore ACT --events N]`
### Rollback to the previous copy: `case1-repin.sh rollback --to ID [--restore ACT --events N]`
The same list. TO is `from_log previous` (:162), the previous distinct copy, and A3 runs `stack-release.sh
rollback mosaic-stack --units external` with `--restore` as today. 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).
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`, 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. The r2 tar is gone: it ran with the unit up, and
it landed where neither reader looks. 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).
`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 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 result. If a run dies between them, the log keeps `started` with no result, and the next
run's `interrupted` check reports it.
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 result. If a run dies between them, 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
@@ -239,23 +353,27 @@ follows. The gate and the in-run rollback have one owner, the root command, as t
### 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. `confirm` and `revert` refuse without it. Section 7 names the tests that show this.
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 unit. P1 and C2-13's precheck require that
- A sudoers rule that lets `mosaic` start and stop the units. P1 and C2-13'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 (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` or
`accepted Q183`, and `unnamed`, which means "not named in Q182 or Q183". 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).
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:
@@ -265,8 +383,9 @@ Functional checks. Any failure fails the row:
| F2 | the Console listens on 127.0.0.1 on .53 and answers there | `curl -fsS http://127.0.0.1:7332/healthz` on .53 succeeds, and the listener table shows 7332 bound to 127.0.0.1 only. Jason reaches it with `ssh -L 7332:127.0.0.1:7332 localadmin@10.1.1.53`. 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 | while F3's reply is pending, a `bwrap` process owned by `mosaic` has a claude process under it. If `launches list --json` records the sandbox, the build reads that instead, since a short session can end before `ps` sees it |
| 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` reads `bwrap (enforce)` and the claude process's reads `unpriv_bwrap` (stacked) in enforce mode. 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:
@@ -276,23 +395,23 @@ Privilege and host paths. These report and never fail the row:
| 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. Q183 accepts the jarvis key's reach to pve, not local root over the `mosaic` home. #423 step 4 (jarvis sudo) lands before the install (mos-1091 4c). Whether it also removes docker and lxd I don't know, and the check reports what it finds | `id jarvis` |
| 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 7331-7333 are bound to 127.0.0.1 only | listener table |
| P9 | `/usr/bin/bwrap` gives any local user a user namespace | unnamed: the residual of mos-1091 4b, which Duckworth reviews with the profile | `aa-status` line for `mosaic-bwrap`, the sysctl value |
| P9 | a user namespace through `/usr/bin/bwrap` | accepted mos-1096 when all of these hold, unnamed otherwise: `dpkg-statoverride --list /usr/bin/bwrap` reads `root mosaic 0750`; `aa-status` lists `bwrap` and `unpriv_bwrap` in enforce mode; `/etc/apparmor.d/bwrap-userns-restrict` has the pinned sha256; `sudo -u jarvis /usr/bin/bwrap --version` fails with "Permission denied"; `getent group mosaic` lists no member and no other account in `/etc/passwd` has its gid; `fs.protected_hardlinks` is 1 | 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` |
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` and
`claude` on PATH. Cases:
`/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);
text (unnamed), and P9 unnamed once for each of its six items missing;
- 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`, and one that
closes P1 on any output without a rule line.
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, and one that drops P9's jarvis item.
Nothing in the tests reaches .53 or dragon-lin's live state.
@@ -311,48 +430,90 @@ 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` with `userns`, the sysctl left at 1. The
profile text, load, test and rollback are in section 1. Duckworth reviews the profile.
- 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.
## 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`.
Cases:
`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 and exits 1;
- a failure at R2 or R4 exits 2 with UNITS stopped;
- a HEAD mismatch, or an ID that fails the copy-id rule, refuses with exit 3 before any stop;
- 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;
- 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);
- R3 renders OLD, which comes from the unit file, not from anything the `mosaic` steps print.
- `scripts/stack-release.sh`: `--units external` for `adopt`, `activate` and `rollback`, and the new
`confirm` and `revert` 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`:
- `units-running` refuses while any unit is active;
- activate leaves the pointer as it was, writes `PENDING`, and writes a backup of the stopped store
under `$DATA/backups/stack/<ACT>`;
- `confirm` on a pass writes the pointer and logs `ok`; on a failure it logs `gate-failed ...
rollback pending` and leaves the pointer;
- 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`;
- `confirm` refuses `stale-start` when `ActiveEnterTimestamp` is older than the `started` line;
- `confirm` on a pass writes the pointer, logs `ok` and removes `AWAITING-UNITS`; on a failure it logs
`gate-failed ... rollback pending` and leaves the pointer;
- 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` logs `rollback ok` or `rollback-failed`;
- `rollback --restore` finds a backup that an external activate took;
- `confirm` and `revert` refuse without `--units external`.
- `confirm --rollback OLD` logs `rollback ok` or `rollback-failed`, and refuses when OLD isn't the
open activation's `from`;
- `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; `case1-repin.sh` skips R1-R4;
`case1-repin.sh` runs `mosaic`'s `stack-release.sh` without `sudo -u mosaic`; the default path drops
`--user` from one `systemctl` call.
- 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.
- `scripts/case1-accept.sh` (new) and `packages/cli/tests/case1-accept.test.mjs` (new), section 5.
- C2-13. `--profile`, the node and bwrap precheck lines and the copy-id rule are in its brief
(gosalyn-0034). Still open with Gosalyn: what UNITS is (section 4), and `DISABLE_AUTOUPDATER=1` as a
fixed `Environment=` line if the session allowlist passes it (section 1). Any of these that C2-13's
candidate lacks becomes a named change here.
- C2-13. `--profile`, the node and bwrap precheck lines, the copy-id rule and the two units are in
Gosalyn's design r10 (4281b0064). Any of these that C2-13's candidate lacks becomes a named change
here. I've told Gosalyn that the settings-file route for `DISABLE_AUTOUPDATER` doesn't reach a role
session, and that r4 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, the AppArmor profile text
with its load, test and rollback, and the line that a restore leaves the credential in place.
each with who runs it and its go, the pinned node sha256, claude version, bubblewrap version and
profile sha256, the statoverride and the managed settings file, each with its rollback, and the line
that a restore leaves the credential in place.
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, dragon-lin's units or `~/.mosaic`.