agents: row 195 Case 1 install design r2 (#1693, brief blob 45be7f09af, Sage rulings sage-1451)

This commit is contained in:
2026-10-20 15:38:55 -05:00
parent 088087ac96
commit 818b79696d
+196
View File
@@ -0,0 +1,196 @@
# Row 195 design: the Case 1 install on 10.1.1.53 (r2)
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 (8862feccc on
`darkwing/row190`, 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. This design names the inputs and
the changes Case 1 needs from it, so the brief at a0f6afc50 on `gosalyn/case2-design` is the interface I
read.
Changes from r1 (sage-1451): section 6 now holds Sage's rulings on Q1-Q4. Section 4 keeps the system unit,
and the copy id in `ExecStart=` is a C2-13 change. Section 1 marks the bwrap install and the user namespace
fix as host security changes that wait for Jason's ruling through Mos. F2 now states the 127.0.0.1 listener
and `ssh -L`.
## 0. What I checked on the host
Read only, as `localadmin` over ssh, 2026-10-20 about 20:40Z (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 |
| 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` | bwrap needs an unprivileged user namespace; this setting can refuse it |
| 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 |
## 1. The install as inputs to C2-13's scripts
Instance. Row 190's canon with the default name, `dev`, as Case 2 uses it (Gosalyn design 9). `~` is
`mosaic`'s home, `/srv/mosaic`, so:
| Input | Value |
|---|---|
| user | `mosaic` |
| 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) |
| roles | the factory set in section 2 |
| human id | Jason's id, given in the window's go. Not `tester` (`broker.mjs:245`) |
| model credential | `<configDir>/secrets/claude-session.env`, 0600 (row 190's `SESSION_ENV` for the default instance) |
| token slot path | none for this row (section 2) |
| Console, 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 |
Why `dev` and not a named instance: one instance per OS user, and C2-13 already assumes
`/srv/mosaic/.config/mosaic-dev`. A second instance under `mosaic` on .53 would need a name, and row 190
makes that a flag, not a move.
Why node lives under `/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
download is checked against the release's `SHASUMS256.txt` before unpacking.
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 [email protected]`, `sudo` (#423 step 1) |
| 2a': `apt-get install bubblewrap`, and the user namespace fix (section 6, Q2) | root | ops-01, same path, only after Jason rules on both through Mos. Both are host security changes, and nothing runs on .53 until they're ruled |
| 2b: node and claude under `~/.local`, `scripts/bootstrap.sh`, release package and activate, business file, roles, `case2-precheck.sh` | `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` from a root-owned checkout of the reviewed commit outside `/srv/mosaic`, then `systemctl enable` | root | ops-01, same path |
| start, stop, restart of the unit | root | ops-01, or Jason |
| acceptance (section 5) | `localadmin`, with `sudo -u mosaic -H` for the in-instance checks | the agent the window names |
## 2. Where Case 1 differs from Case 2
| Case 2 item (C2-13 brief) | Case 1 | Reason |
|---|---|---|
| business file with the library mapping | business `mosaic-stack` with no `library` key | the factory has no books repo |
| `bean`, `chet`, persona vars and files, specialists | factory roles from `roles/`, with a chief that has a harness var | Jason names the set in the go. I haven't read his live business file and won't. The acceptance needs only the chief (F3), since a chief with no harness var answers `no-route`. Others are added later by the human with `instances.set`, no new row |
| token slot `library-gitea` | none | the factory's per-agent git identity is row 199's credential store (Megavolt, #1697). Until it lands, git work stays on dragon-lin (Q182) |
| precheck: `bus.tester` is not on | `bus.tester` is on | the acceptance and the Q173 test system need the tester (agents act as tester, never Jason) |
| precheck: persona file modes and digests | skipped | no persona files |
| precheck: `library.sync`, `library.status` | skipped | no library |
| 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: marker prompt through claude with `--system-prompt-file` | kept | |
| precheck: `sudo -n true` fails, `id -G` one entry, run root 0700, config validates, free memory, HOME | kept | |
| (not in C2-13) | added: node 24 or later, `bwrap --unshare-all --ro-bind / / true` exits 0 as `mosaic` | Case 2 needs both too; they go to Gosalyn as C2-13 changes |
How the differences reach C2-13's scripts. I'd ask Gosalyn for one input, `--profile factory|library`,
default `library`. `factory` skips the library and persona items and requires `bus.tester` on. The
alternative is a Case 1 copy of the scripts, which the brief rules out. If Gosalyn declines, the profile goes
into this row's manifest as a named change to `case2-install.sh` and `case2-precheck.sh`, reviewed by
Duckworth with Webby told.
## 3. Model access
Q185: Case 1 uses the account the factory uses today, the Claude subscription behind dragon-lin's
`claude-session.env`. The install never copies that file from dragon-lin. In the window, Jason (or the
person he names under the Q173 secrets rule) makes a credential for .53 from that account and writes it
to `/srv/mosaic/.config/mosaic-dev/secrets/claude-session.env` as `mosaic`, mode 0600. Nobody prints it,
and no script reads its value. The precheck checks only its mode, owner and that the variable name is
present (a `grep -c` on the name, output a count).
Quota: the factory on dragon-lin and on .53 share one subscription. Q185's "use existing" accepts that.
Case 2 shares nothing with it (Mos's reading of Q185).
The pi harness stays off on .53 until P19 #383 settles (mos-0865). The business file names claude only.
## 4. Re-pin, rollback and backup on .53
Unit form (ruled, sage-1451 Q1). The Case 2 form: a system unit written by root with `User=mosaic`. Today's
re-pin path (`stack-release.sh activate`, row 85, row 190) drives `systemctl --user` units, which a
system unit doesn't answer to, and `mosaic` can't restart a system unit without sudo. So on .53:
- Re-pin. As `mosaic`: `stack-copy.sh` makes the new copy under `~/.local/share/mosaic-stack/copies/`, then
the pointer and the backup (below) are written. `stack-release.sh activate` drives `systemctl --user`
today, so it needs a mode that writes the pointer and backup and leaves units to root. I'll size that in
the build and name it in the manifest. As root (ops-01):
`case2-unit.sh` re-renders the unit for the new copy and refuses any other difference, then `systemctl
restart mosaic-stack.service`. This needs C2-13's fixed `ExecStart=` check to allow exactly one varying
part, the copy id (a 40-hex directory under the copies dir). That is a C2-13 change. I've sent it to
Gosalyn, and the manifest names it either way (section 7).
- Rollback. The same two steps with the previous distinct copy (from the activation log, as on
dragon-lin).
- Backup. Before every activate, as `mosaic`, a tar of the data root and config dir, without `secrets/`,
into `/srv/mosaic/backups/` at 0700 (the row 185 rule: back up every activate). The window's first
backup comes before the first activate even though the data root is new, so the rule has one path.
- Who. The re-pin goes as on dragon-lin: Mos or Sage plans it, ops-01 runs the root half, and the agent
the window names runs the `mosaic` half. Each re-pin is a window with Jason's or Mos's go and a STATE
report.
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`.
## 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).
Functional checks. Any failure fails the row:
| Id | Check | How |
|---|---|---|
| F1 | the unit runs as `mosaic` | `systemctl show mosaic-stack.service -p User -p MainPID`, then the MainPID's uid from `/proc/<pid>/status` is `mosaic`'s, and the unit is active |
| 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 [email protected]`. A LAN listener waits for Q175 SSO (sage-1451 Q3) |
| F3 | one tester message gets a reply | the tester listener on 127.0.0.1:7333 sends one marked message to the chief, and a stored reply appears within 5 minutes (`talk`, then `follow`; inbox alone shows decisions only) |
| F4 | the precheck passes | `case2-precheck.sh --profile factory` exits 0 as `mosaic` |
Privilege and host paths. These report and never fail the row:
| Id | Path | Mark | Evidence the script prints |
|---|---|---|---|
| P1 | `mosaic` has sudo | closed when `sudo -l -U mosaic` grants nothing | the `sudo -l` result |
| P2 | `mosaic` has supplementary groups or a key | closed when `id -G mosaic` is one entry and `~mosaic/.ssh/authorized_keys` is absent or empty | `id`, file state |
| P3 | `localadmin` NOPASSWD sudo, reached with the dragon-lin key | accepted Q183 (agents keep ssh to .53; localadmin from #423 step 1) | `sudo -l -U localadmin` |
| P4 | root on pve over VM 1008, and PBS backups of VM 1008, which hold `/srv/mosaic` including the credential | accepted Q182 and Q183 (agents keep root on pve and PBS) | a fixed line; the script can't see pve from inside |
| P5 | `jarvis`, which runs Hermes, can reach `/srv/mosaic` through sudo, or through the docker or lxd groups, which are root-equivalent | unnamed. Q183 accepts the jarvis key's reach to pve, not local root over the `mosaic` home. It ends at #423 step 4 (jarvis sudo), which waits until Case 1 is live. Sage passed it to Mos (sage-1451) | `id jarvis` |
| P6 | other users in sudo, docker, lxd | closed when the only members are `localadmin` and `jarvis`; any other is unnamed | `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 |
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` and `sudo` on PATH. Cases:
- each F check failing gives a non-zero exit with its line;
- each P check in each of its marks;
- 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), and one that maps P5 to `closed`.
Nothing in the tests reaches .53 or dragon-lin's live state.
## 6. Rulings (sage-1451)
- Q1, unit form: the brief's system unit. C2-13's `ExecStart=` check accepts a varying copy id. If that
changes C2-13, it goes to Gosalyn and my manifest names it (section 4).
- Q2, bwrap and user namespaces. Ubuntu 24.04 with `apparmor_restrict_unprivileged_userns = 1` can refuse
bwrap's namespace. Two host security changes are needed: installing `bubblewrap` through apt, and the
user namespace fix, either an AppArmor profile that allows `userns` for `/usr/bin/bwrap` or the sysctl
turned off. Sage sends both to Mos for Jason. This design names them and stops there. Nothing runs on
.53 until they're ruled. .54 is likely the same, so Gosalyn needs the answer too.
- Q3, the Console: it listens on 127.0.0.1 on .53 (`packages/webui/src/serve.mjs:466`; `cli.mjs` passes no
host), and Jason reaches it with `ssh -L`. F2 says so. A LAN listener waits for Q175 SSO.
- Q4, the date: if C2-13's candidate is late, I build on C2-13's latest commit and rebase (Q177). Under
the #426(a) push rules (mos-1086) a rebase goes to a new branch name.
- P5 keeps the mark "not named in Q182 or Q183", and Sage passed it to Mos.
## 7. Manifest (planned)
- `scripts/case1-accept.sh` (new) and `packages/cli/tests/case1-accept.test.mjs` (new).
- The `--profile factory` change, the two precheck lines and the copy id in `case2-unit.sh`'s `ExecStart=`
check, in C2-13's files, either as Gosalyn's C2-13 change or as named changes here (sections 2 and 4).
- Docs: the Case 1 run sheet `docs/guides/case1-install.md` (new): the window's steps in order, each with
who runs it and its go.
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, dragon-lin's units or `~/.mosaic`.