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)

This commit is contained in:
jason.woltje committed 2026-10-20 15:04:15 -05:00
1 parent 8645cb4560
commit eab6e1a3a8
2 files changed
+56 -13

No files matched your search

+33 -6
View File
@@ -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`, 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). 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 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. 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 ## Changes from r6
Webby's review (webby-0023, CHANGES, notes at agents/webby/work/case2-r6/REVIEW.md, 216302d65). 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 `[email protected]` with NOPASSWD sudo. and runs them under Jason's go for step 2, over ssh as `[email protected]` with NOPASSWD sudo.
#423 step 1 creates that account. They use neither `melanie`'s sudo nor pve root. So the install #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 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 - 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 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 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-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 `<host>` | C2-01 | Luna | doc only, reviewed against the survey's real flags | | 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 `<host>` | 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-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 `<host>` as `<user>`: 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, `<user>` 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 `<host>` as `<user>`: 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, `<user>` 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-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-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 | | 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. doesn't run them without the go.
- 2a, ops-01 via `localadmin`: `useradd` `mosaic` as a system user with no sudo, no supplementary - 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`. groups and no keys. Create `/srv/mosaic` at 0700, owned by `mosaic`.
- 2b: the install as `<user>`, by him or his delegate. `scripts/bootstrap.sh`, release package and - 2b: the install as `<user>`, 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 activate, the pinned harness, and which model accounts its agents use. Model use is spend, so the
accounts are his call. accounts are his call.
- 2c, ops-01 via `localadmin`: install the unit file that C2-13 writes as `mosaic-stack.service`, - 2c, ops-01 via `localadmin`: root makes a checkout of the reviewed commit outside `/srv/mosaic`,
and enable it. The unit starts only at step 9. 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 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 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. admin, which is his call.
+23 -7
View File
@@ -713,17 +713,26 @@ starts.
### Files owned ### Files owned
`scripts/case2-install.sh` and `scripts/case2-precheck.sh` (new), a unit template under `scripts/case2-install.sh`, `scripts/case2-precheck.sh` and `scripts/case2-unit.sh` (new), the template
`packages/cli/systemd/` if the existing one can't take the paths, and `packages/cli/systemd/mosaic-stack.service.in` (new), and `packages/cli/tests/case2-install.test.mjs`
`packages/cli/tests/case2-install.test.mjs` (new). (new).
### What ships ### What ships
The install does: bootstrap, release package and activate, the business file with the mapping, `bean` 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 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 `<user>`, so it doesn't create `<user>` or path. It runs as `<user>` through `sudo -u mosaic -H` from `localadmin`, and `mosaic` gets no key. It
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 doesn't create `<user>` and writes no unit file. Root does those in design 10 steps 2a and 2c. Every
only inputs. 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 `<user>`'s home.
The precheck refuses unless all of these hold: The precheck refuses unless all of these hold:
- The config validates. - 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 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. 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 ### 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 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 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. is step 9 and her choice.