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:
1 parent
9a16d7795d
commit
0075be3581
1 file changed
+292
-131
@@ -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`.
|
||||
Reference in new issue
Block a user