gosalyn: Case 2 design r10, two system units (bus and Console) from the existing templates, copy id the only re-pin value (darkwing row 195)

This commit is contained in:
jason.woltje committed 2026-10-20 15:59:03 -05:00
1 parent 51006710ac
commit 4281b0064c
2 files changed
+66 -24

No files matched your search

+37 -10
View File
@@ -1,4 +1,4 @@
# Case 2 design: the creative writing hub (r9) # Case 2 design: the creative writing hub (r10)
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,25 @@ 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 r9
Darkwing's row 195 question (darkwing to gosalyn, after Duckworth's B1) found a gap. r9 named one
system unit, `mosaic-stack.service`, and never said what it runs. The stack is two units today: the bus
host (`packages/cli/systemd/mosaic-bus.service.in`) and the Console
(`packages/webui/systemd/mosaic-webui.service.in`). The slice needs both, because C2-10b is Console
chat. Now 2c renders both from those templates as system units, `mosaic-bus@<business>.service` and
`mosaic-webui.service`, with the same copy-id rule, and no new `mosaic-stack.service.in` exists. The
bus unit carries the `EnvironmentFile=` line. The Console unit sets no credential variable, as its
template says. The node path and the Console's serve flags are install inputs, fixed across re-pins.
The copy id is the only value a re-pin changes.
One thing this makes explicit. The Console template says whoever starts it holds the human's capability,
so Jason starts it, or Mos under lead decision 102. A system unit that systemd starts at boot holds that
capability with no human starting it. On her host the human is Melanie, signing in through step 5. So
the step 9 go is the human's start, and a reboot restarts the Console under that same go. If Mos or
Webby reads decisions 62 and 94 as needing a human at every start, the Console unit gets no `[Install]`
section and a human starts it after each boot.
## Changes from r8 ## Changes from r8
Jason's Q7 answer, "Q7: yes" (DECISIONS Q186, mos-1090, sage-1469). O4 is the model path. Jason's Q7 answer, "Q7: yes" (DECISIONS Q186, mos-1090, sage-1469). O4 is the model path.
@@ -977,7 +996,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 `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, when some creative instance's harness is claude-code, one marker prompt run through the host's claude with `--system-prompt-file` shows it replaces the default prompt (its version printed), the key file in 10 step 2d exists, is owned by `<user>` at 0600 and sets a nonempty `ZAI_API_KEY` when an instance's provider is `zai` (name only, never the value), 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=`, `EnvironmentFile=` or hardening lines are off their fixed values. 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`, which renders the bus and Console system units from their existing templates and 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, when some creative instance's harness is claude-code, one marker prompt run through the host's claude with `--system-prompt-file` shows it replaces the default prompt (its version printed), the key file in 10 step 2d exists, is owned by `<user>` at 0600 and sets a nonempty `ZAI_API_KEY` when an instance's provider is `zai` (name only, never the value), 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 either unit path, and a render whose `User=`, `Group=`, `ExecStart=`, the bus unit's `EnvironmentFile=` 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 |
@@ -1008,7 +1027,7 @@ compaction count and clear context (GAP-MAP C2.12, C2.13). Until then, a new thr
Where it runs (Jason, Q181, relayed in mos-1031). `<host>` is her VM 10.1.1.54, W-Melanie-OC, VM 1012 Where it runs (Jason, Q181, relayed in mos-1031). `<host>` is her VM 10.1.1.54, W-Melanie-OC, VM 1012
on pve. `<user>` is `mosaic`, a system user with no sudo, no supplementary groups and no keys, home on pve. `<user>` is `mosaic`, a system user with no sudo, no supplementary groups and no keys, home
`/srv/mosaic` at mode 0700. It runs as the system unit `mosaic-stack.service` with `ProtectHome`, so nothing it needs may `/srv/mosaic` at mode 0700. It runs as two system units, `mosaic-bus@<business>.service` and `mosaic-webui.service`, both with `ProtectHome`, so nothing it needs may
live under `/home`. Its config directory, `<configDir>`, is `/srv/mosaic/.config/mosaic-dev`, and the token live under `/home`. Its config directory, `<configDir>`, is `/srv/mosaic/.config/mosaic-dev`, and the token
slot and persona files live under it. ops-01's plan, section 3, set this layout. That plan stopped slot and persona files live under it. ops-01's plan, section 3, set this layout. That plan stopped
with #421, and Q183 keeps a `mosaic` user without sudo on each VM. Section 10 step 2a creates it. The rest of the design with #421, and Q183 keeps a `mosaic` user without sudo on each VM. Section 10 step 2a creates it. The rest of the design
@@ -1119,16 +1138,24 @@ What it adds is one bot account with read access to that one repo.
not part of 10-27. It is row B (#428). not part of 10-27. It is row B (#428).
- 2c, ops-01 via `localadmin`: root makes a checkout of the reviewed commit outside `/srv/mosaic`, - 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 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 `scripts/case2-unit.sh`, which renders two system units from the checkout's existing templates:
home as inputs. It checks `User=`, `Group=`, `ExecStart=` and the hardening lines against fixed `[email protected]` from `packages/cli/systemd/mosaic-bus.service.in`, enabled as
values and writes `/etc/systemd/system/mosaic-stack.service` as `root:root` 0644. It never uses `mosaic-bus@<business>.service`, and `mosaic-webui.service` from
`systemctl link`, and it refuses a symlink at the unit path. On a re-install it refuses an `packages/webui/systemd/mosaic-webui.service.in`. Inputs: `--user`, `--home`, `--business`,
installed unit that differs from the render. Root runs nothing from `/srv/mosaic` and reads no `--copy`, `--node` and the Console's serve flags. In both, `@REPO@` is the copy
unit that `mosaic` wrote. Then it enables the unit. The unit starts only at step 9. `<home>/.local/share/mosaic-stack/copies/<id>`. `%h` becomes the literal home, because a system
unit's `%h` is root's home. The render adds `User=`, `Group=`, the hardening lines and
`WantedBy=multi-user.target`. It checks `User=`, `Group=`, `ExecStart=`, the bus unit's
`EnvironmentFile=` and the hardening lines against fixed values, and the copy id is the one part
that may change between renders. It writes both under `/etc/systemd/system/` as `root:root` 0644.
It never uses `systemctl link`, and it refuses a symlink at either 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 both. They start only at step 9, the bus first
and then the Console, and they stop in the reverse order.
- 2d, Jason: the hub's own key on the existing Z.ai account, with a spend cap if the account offers - 2d, Jason: the hub's own key on the existing Z.ai account, with a spend cap if the account offers
one. Model use is spend, so the key and the cap are his. Whose Z.ai account it is hasn't been said, one. Model use is spend, so the key and the cap are his. Whose Z.ai account it is hasn't been said,
and that is his input. The key goes in `/srv/mosaic/.config/mosaic-dev/secrets/claude-session.env` and that is his input. The key goes in `/srv/mosaic/.config/mosaic-dev/secrets/claude-session.env`
as `ZAI_API_KEY=`, owned by `mosaic`, mode 0600, the file the unit's `EnvironmentFile=` line loads as `ZAI_API_KEY=`, owned by `mosaic`, mode 0600, the file the bus unit's `EnvironmentFile=` line loads
(the same line as `packages/cli/systemd/mosaic-bus.service.in:24`). The name says claude, but it is (the same line as `packages/cli/systemd/mosaic-bus.service.in:24`). The name says claude, but it is
the one file decision 119 and `packages/cli/README.md` document, so I kept it. He places it, or the one file decision 119 and `packages/cli/README.md` document, so I kept it. He places it, or
says who may, per the Q173 secrets rule. Agents never print it. Until #423 step 5, `melanie`'s says who may, per the Q173 secrets rule. Agents never print it. Until #423 step 5, `melanie`'s
+29 -14
View File
@@ -710,8 +710,8 @@ Dewey approves the manifest. The webui suite is green on Sage's gate, along with
### Problem ### Problem
The hub runs on 10.1.1.54 as `mosaic` under `/srv/mosaic`, as `mosaic-stack.service` with The hub runs on 10.1.1.54 as `mosaic` under `/srv/mosaic`, as two system units,
`ProtectHome` (design 9, Q181). Nothing installs a library business there, or checks the host before it `mosaic-bus@<business>.service` and `mosaic-webui.service`, with `ProtectHome` (design 9 and 10 step 2c, r10). Nothing installs a library business there, or checks the host before it
starts. starts.
### Owner and reviewer ### Owner and reviewer
@@ -722,8 +722,8 @@ starts.
### Files owned ### Files owned
`scripts/case2-install.sh`, `scripts/case2-precheck.sh` and `scripts/case2-unit.sh` (new), the template `scripts/case2-install.sh`, `scripts/case2-precheck.sh` and `scripts/case2-unit.sh` (new),
`packages/cli/systemd/mosaic-stack.service.in` (new), `packages/cli/tests/case2-install.test.mjs` `packages/cli/tests/case2-install.test.mjs`
(new), and one line of `packages/cli/README.md`: at `:713`, "nothing in the repository creates or reads (new), and one line of `packages/cli/README.md`: at `:713`, "nothing in the repository creates or reads
it" changes to say that nothing creates it, that only the Case 2 precheck reads it, and that the it" changes to say that nothing creates it, that only the Case 2 precheck reads it, and that the
precheck prints only variable names, never values (webby-0034 N2, webby-0035). precheck prints only variable names, never values (webby-0034 N2, webby-0035).
@@ -749,20 +749,31 @@ precheck take `--profile factory|library`, default `library`:
`case2-unit.sh` is the 2c step. Root runs it from a root-owned checkout of the reviewed commit outside `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: `/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, the home and the copy id as inputs. - It renders two system units from the checkout's existing templates and changes neither template:
- It refuses unless `User=`, `Group=`, `ExecStart=`, `EnvironmentFile=` and the hardening lines match `[email protected]` from `packages/cli/systemd/mosaic-bus.service.in` (enabled as
fixed values. `EnvironmentFile=` is the line in `packages/cli/systemd/mosaic-bus.service.in:24`. The `mosaic-bus@<business>.service`) and `mosaic-webui.service` from
copy id is the one part of `ExecStart=` that varies, so a re-pin to a new copy can re-render the `packages/webui/systemd/mosaic-webui.service.in`. Flags: `--user`, `--home`, `--business`, `--copy ID`,
unit (row 195). It must be exactly 40 lowercase hex characters naming a copy that holds `scripts/mosaic`, `--node PATH` and the Console's serve flags after `--`. Row 195 calls it with those names. `@REPO@` is
the copy dir. `%h` becomes the literal `--home`, because in a system unit `%h` is root's home.
`@PM_FLAGS@` and `@CLAUDE_ENV@` render empty. The render adds `User=`, `Group=`, the hardening lines
and `WantedBy=multi-user.target`.
- It refuses unless `User=`, `Group=`, `ExecStart=`, the bus unit's `EnvironmentFile=` and the
hardening lines match fixed values. `EnvironmentFile=` is the line in
`packages/cli/systemd/mosaic-bus.service.in:24`, with the literal home. The Console unit has none,
and it refuses one. `--node` must be an absolute path to a root-owned file outside `<home>`. The
serve flags pass `scripts/webui-service.sh`'s own flag checks. On a re-install, the node path and
serve flags must equal the installed unit's. The copy id is the one value that may change, so a
re-pin or a rollback to another copy re-renders both units (row 195). It must be exactly 40 lowercase hex characters naming a copy that holds `scripts/mosaic`,
directly under `<home>/.local/share/mosaic-stack/copies`. That is the rule `copy_root` uses directly under `<home>/.local/share/mosaic-stack/copies`. That is the rule `copy_root` uses
(`scripts/stack-copy.sh:6`, `:20`). Anything else refuses before the render. (`scripts/stack-copy.sh:6`, `:20`). Anything else refuses before the render.
- It refuses any `PermissionsStartOnly=`, and any `Exec*=` value whose leading run of prefix - It refuses any `PermissionsStartOnly=`, and any `Exec*=` value whose leading run of prefix
characters (`@`, `-`, `:`, `+`, `!`) holds a `+` or `!`. systemd takes those prefixes in any order, characters (`@`, `-`, `:`, `+`, `!`) holds a `+` or `!`. systemd takes those prefixes in any order,
so `-+/srv/mosaic/x` runs as root even with `User=mosaic`. The named-line check doesn't read these so `-+/srv/mosaic/x` runs as root even with `User=mosaic`. The named-line check doesn't read these
lines (webby-0026 N1, webby-0027, defence in depth). lines (webby-0026 N1, webby-0027, defence in depth).
- It writes `/etc/systemd/system/mosaic-stack.service` as `root:root` 0644. It never uses - It writes both units under `/etc/systemd/system/` as `root:root` 0644. It never uses
`systemctl link`, and it refuses a symlink at the unit path. `systemctl link`, and it refuses a symlink at either unit path.
- On a re-install, it refuses an installed unit that differs from the render. - On a re-install, it refuses an installed unit that differs from the render, except for the copy id.
- Order: start the bus, then the Console. Stop in reverse.
- It reads nothing from under `<user>`'s home. - It reads nothing from under `<user>`'s home.
The precheck refuses unless all of these hold: The precheck refuses unless all of these hold:
@@ -805,8 +816,12 @@ output for the fixture key value finds nothing.
- an installed unit edited to `User=root` - an installed unit edited to `User=root`
- an edited `ExecStart=` - an edited `ExecStart=`
- a template with a hardening line removed - a template with a hardening line removed
- a template with the `EnvironmentFile=` line removed or pointing elsewhere - a bus template with the `EnvironmentFile=` line removed or pointing elsewhere, and a Console template
- a symlink at the unit path with one added
- a symlink at either unit path
- a `--node` that is relative, not root-owned, or under `<home>`
- a re-install with a different `--node` or different serve flags
- an unrendered `%h` left in either unit
- a template with `ExecStartPre=+/srv/mosaic/x` - a template with `ExecStartPre=+/srv/mosaic/x`
- a template with `ExecStartPre=-+/srv/mosaic/x` - a template with `ExecStartPre=-+/srv/mosaic/x`
- a copy id that is short, uppercase, holds `/` or `..`, or names no directory - a copy id that is short, uppercase, holds `/` or `..`, or names no directory