agents: row 195 Case 1 install design r2 (#1693, brief blob 45be7f09af, Sage rulings sage-1451)
This commit is contained in:
@@ -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`.
|
||||
Reference in New Issue
Block a user