From eab6e1a3a897edc20b51272db73f5b3d4c9bc15a Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 15:04:15 -0500 Subject: [PATCH] gosalyn: Case 2 design r8 for #416 (T2): root renders and checks the unit from a root-owned checkout in 2c, mosaic writes no unit, 2b via sudo -u mosaic from localadmin, step 1 hold lifted, C2-13 brief to match (webby-0025, mos-1074) --- agents/gosalyn/work/case2/DESIGN.md | 39 ++++++++++++++++++++++++----- docs/plans/2026-10-20_case2-rows.md | 30 ++++++++++++++++------ 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/agents/gosalyn/work/case2/DESIGN.md b/agents/gosalyn/work/case2/DESIGN.md index aeaad378a..5dd7a9c9b 100644 --- a/agents/gosalyn/work/case2/DESIGN.md +++ b/agents/gosalyn/work/case2/DESIGN.md @@ -1,4 +1,4 @@ -# Case 2 design: the creative writing hub (r7) +# Case 2 design: the creative writing hub (r8) Gosalyn, 2026-10-20 (from `date -u`). Track T2 in jason-master-restart `projects/mosaic-mvp/PLAN.md`, Vikunja #416, DECISIONS Q176-Q178. The requirement is Jason's text in TARGET.md (addendum 2026-10-20). @@ -8,6 +8,26 @@ Base: `refactor` at 9ae8c73ac. r1 cited 5d019d876. `git diff --stat 5d019d876 9a adapters roles scripts` is empty (Mos in mos-0980, Webby in webby-0001), so every `file:line` holds at 9ae8c73ac. Paths are relative to the repository root. +## Changes from r7 + +Webby's review (webby-0025, CHANGES, notes at agents/webby/work/case2-r7/REVIEW.md, 19bb3ae36). + +- B1. r7 had root install a unit file that `mosaic` wrote. By 2c, package code has run as `mosaic` in + 2b and could rewrite that file to `User=root` or another `ExecStart`, so the user without sudo had a + path to root, and again on every re-install. Now `mosaic` writes no unit. In 2c, root works only from + a root-owned checkout of the reviewed commit outside `/srv/mosaic`. It renders the unit from that + checkout's template, with the user and home as inputs, and checks `User=`, `Group=`, `ExecStart=` and + the hardening lines against fixed values. It then writes `/etc/systemd/system/mosaic-stack.service` + as `root:root` 0644, and it never uses `systemctl link`. Webby suggested the template in the release. + The release sits under `/srv/mosaic`, where `mosaic` can write it, so I used a root-owned checkout + instead. 2c refuses an installed unit that differs from the render, and a symlink at the unit path. + C2-13 tests a unit edited to `User=root`, and 2c refuses it. +- N1. `mosaic` has no keys and no login shell, so 2b runs as `sudo -u mosaic -H` from `localadmin`. + Nobody adds a key to `mosaic` to make 2b work. + +Mos lifted the mos-1050 hold on step 1 after Webby passed it (mos-1074). Section 10 still goes to +Jason one step at a time. + ## Changes from r6 Webby's review (webby-0023, CHANGES, notes at agents/webby/work/case2-r6/REVIEW.md, 216302d65). @@ -19,7 +39,7 @@ Webby's review (webby-0023, CHANGES, notes at agents/webby/work/case2-r6/REVIEW. and runs them under Jason's go for step 2, over ssh as `localadmin@10.1.1.54` with NOPASSWD sudo. #423 step 1 creates that account. They use neither `melanie`'s sudo nor pve root. So the install depends on #423 step 1 and on nothing else in #423. C2-13 writes the unit file and doesn't install - it. Section 9 cites Q183 for the layout, not the stopped plan. + it. r8 replaces that, because `mosaic` must write no unit (webby-0025). Section 9 cites Q183 for the layout, not the stopped plan. - Mos, mos-1064 item 3. r6 said Jason's acceptance covers `melanie`'s sudo. It covers the factory's paths to .54 (Q182, Q183): pve root, ssh as `melanie`, and the root her sudo gives through those paths. It doesn't cover the Hermes processes, which run as `melanie` and could use her sudo to read @@ -937,7 +957,7 @@ Chains follow Q177 merge-chaining. S marks a slice row. | C2-10b | S | Console chat: cited-item renderer through `renderItems`, the status line | C2-06, C2-09a, C2-03a | Haiku high | webui tests for items, scope badges, notices, "changed since" and the status line | | C2-12 | S | The setup sheet for Melanie (7.1, including N2 and N4, and what changes for Bean and Chet in 4) and the survey run sheet for `` | C2-01 | Luna | doc only, reviewed against the survey's real flags | | C2-20 | S | Persona import (9.1): a script that reads only the persona definitions of the two Hermes profiles, confirms profile to name, and writes each persona file (4) with its `persona.name` and `persona.sha256` for the business file. Prints names, byte counts and sha256 only. A `--dry-run` mode, run before Jason's go, prints only counts of files, lines and lines starting `# ` or `## ` | C2-08 | Opus (Gosalyn), run under Jason's go | fixture Hermes profile trees: the right fields are taken, memory and session files are never opened (a read trace on the fixture), a profile with no persona field refuses, a persona that would fail C2-08's fence check refuses, output holds no persona text, each written file is 0600 and its digest matches, `--dry-run` writes nothing and prints the right counts for a fixture with two heading lines and no marker string from it | -| C2-13 | S | Install script and precheck for `` as ``: bootstrap, release activate, business file with the mapping, `bean` and `chet` with their persona vars and files, any specialists she picks, her human id, token slot path, and the `mosaic-stack.service` unit file, which root installs in 10 step 2c. Jason or his delegate runs it (10) | C2-04, C2-03a, C2-20 | Opus (Gosalyn), Jason's go | precheck: config validates, `` has no sudo and `id -G` prints exactly one entry, `bus.tester` is not on, the business file, `.history`, `changes.jsonl` and persona files have the modes in 4 and each persona digest matches, the run root is 0700, one marker prompt run through the host's claude with `--system-prompt-file` shows it replaces the default prompt (its version printed), free memory printed, `library.sync` fetches her repo read-only, `library.status` lists R-problems, no Discord binding exists. Tested against a scratch data root and a local bare repo | +| C2-13 | S | Install script and precheck for `` as ``: bootstrap, release activate, business file with the mapping, `bean` and `chet` with their persona vars and files, any specialists she picks, her human id, token slot path, and `scripts/case2-unit.sh` with the template `packages/cli/systemd/mosaic-stack.service.in`, which root runs from a root-owned checkout in 10 step 2c. The install writes no unit. Jason or his delegate runs it (10) | C2-04, C2-03a, C2-20 | Opus (Gosalyn), Jason's go | precheck: config validates, `` has no sudo and `id -G` prints exactly one entry, `bus.tester` is not on, the business file, `.history`, `changes.jsonl` and persona files have the modes in 4 and each persona digest matches, the run root is 0700, one marker prompt run through the host's claude with `--system-prompt-file` shows it replaces the default prompt (its version printed), free memory printed, `library.sync` fetches her repo read-only, `library.status` lists R-problems, no Discord binding exists. `case2-unit.sh` refuses an installed unit that differs from its render (one edited to `User=root` is the test), a symlink at the unit path, and a render whose `User=`, `Group=`, `ExecStart=` or hardening lines are off their fixed values. Tested against a scratch data root and a local bare repo | | C2-14 | S | End-to-end acceptance on a scratch data root and a bare repo in her layout: sync, ask a creative agent in its thread, a reply whose cites verify, a playground cite as canon refused, a force-push held as diverged, the Console shows the reply as items | slice rows | Opus | one script, which the reviewer also runs | | C2-02 | | Bus writes: `writeLibraryFile`, `commitFile` refactor, r1's S0 rule on the working tree | C2-04 | Sol (Rocko) | writer tests, `packages/project/tests` unchanged and green | | C2-03b | | Push path, race retry, `unpushed`, `library-remote-invalid` on writes | C2-02, C2-03a | Opus | push tests against a bare repo | @@ -1057,11 +1077,18 @@ What it adds is one bot account with read access to that one repo. doesn't run them without the go. - 2a, ops-01 via `localadmin`: `useradd` `mosaic` as a system user with no sudo, no supplementary groups and no keys. Create `/srv/mosaic` at 0700, owned by `mosaic`. - - 2b: the install as ``, by him or his delegate. `scripts/bootstrap.sh`, release package and + - 2b: the install as ``, by him or his delegate, run as `sudo -u mosaic -H` from `localadmin`. + `mosaic` has no keys and no login shell, and nobody adds a key to make 2b work. `scripts/bootstrap.sh`, release package and activate, the pinned harness, and which model accounts its agents use. Model use is spend, so the accounts are his call. - - 2c, ops-01 via `localadmin`: install the unit file that C2-13 writes as `mosaic-stack.service`, - and enable it. The unit starts only at step 9. + - 2c, ops-01 via `localadmin`: root makes a checkout of the reviewed commit outside `/srv/mosaic`, + owned by root, and checks that its HEAD is the commit Jason's go names. From that checkout it runs + `scripts/case2-unit.sh`, which renders `mosaic-stack.service` from the template with the user and + home as inputs. It checks `User=`, `Group=`, `ExecStart=` and the hardening lines against fixed + values and writes `/etc/systemd/system/mosaic-stack.service` as `root:root` 0644. It never uses + `systemctl link`, and it refuses a symlink at the unit path. On a re-install it refuses an + installed unit that differs from the render. Root runs nothing from `/srv/mosaic` and reads no + unit that `mosaic` wrote. Then it enables the unit. The unit starts only at step 9. 3. A new Gitea bot account for the hub, with read access to the books repo only. Not bean-brain, and not her other three repos. Her repo's access list is hers, so adding the bot means asking her or acting as admin, which is his call. diff --git a/docs/plans/2026-10-20_case2-rows.md b/docs/plans/2026-10-20_case2-rows.md index 3f171b5fe..5cf325038 100644 --- a/docs/plans/2026-10-20_case2-rows.md +++ b/docs/plans/2026-10-20_case2-rows.md @@ -713,17 +713,26 @@ starts. ### Files owned -`scripts/case2-install.sh` and `scripts/case2-precheck.sh` (new), a unit template under -`packages/cli/systemd/` if the existing one can't take the paths, and -`packages/cli/tests/case2-install.test.mjs` (new). +`scripts/case2-install.sh`, `scripts/case2-precheck.sh` and `scripts/case2-unit.sh` (new), the template +`packages/cli/systemd/mosaic-stack.service.in` (new), and `packages/cli/tests/case2-install.test.mjs` +(new). ### What ships The install does: bootstrap, release package and activate, the business file with the mapping, `bean` and `chet` with their persona vars and files, any specialists she picks, her human id, the token slot -path, and the `mosaic-stack.service` unit file. It runs as ``, so it doesn't create `` or -install the unit. Root does those in design 10 steps 2a and 2c. Every host value is an input, so a later host move changes -only inputs. +path. It runs as `` through `sudo -u mosaic -H` from `localadmin`, and `mosaic` gets no key. It +doesn't create `` and writes no unit file. Root does those in design 10 steps 2a and 2c. Every +host value is an input, so a later host move changes only inputs. + +`case2-unit.sh` is the 2c step. Root runs it from a root-owned checkout of the reviewed commit outside +`/srv/mosaic`, never from the release, because `mosaic` can write the release. It does the following: +- It renders the unit from the checkout's template, with the user and home as inputs. +- It refuses unless `User=`, `Group=`, `ExecStart=` and the hardening lines match fixed values. +- It writes `/etc/systemd/system/mosaic-stack.service` as `root:root` 0644. It never uses + `systemctl link`, and it refuses a symlink at the unit path. +- On a re-install, it refuses an installed unit that differs from the render. +- It reads nothing from under ``'s home. The precheck refuses unless all of these hold: - The config validates. @@ -741,11 +750,18 @@ It also prints free memory and verifies HOME is `/srv/mosaic`. Tests run against a scratch data root and a local bare repo. Each precheck refusal has a case. The claude check runs against a fake claude that appends rather than replaces, and that case refuses. +`case2-unit.sh` runs against a scratch unit directory. These cases refuse: +- an installed unit edited to `User=root` +- an edited `ExecStart=` +- a template with a hardening line removed +- a symlink at the unit path + +A clean render writes a 0644 file. A test checks the script for `systemctl link`. ### Out of scope Running it on the host. Design 10 steps 2 to 7 are Jason's. Step 1 no longer waits on #421 (Q183), and -its hold lifts once Webby approves r7. The root steps 2a and 2c are ops-01's, over ssh as `localadmin`, +Mos lifted its hold (mos-1074). The root steps 2a and 2c are ops-01's, over ssh as `localadmin`, so the install waits on #423 step 1 and on nothing else in #423. The Gitea bot account and the token slot are steps 3 and 4. Stopping Hermes is step 9 and her choice.