gosalyn: row 226 C2-13b design d2, release package and activate dropped, precheck split into pre-start checks and --library-status after the bus is up, Webby's N1-N6 taken in, design 10 text change listed (webby-0085, webby-0086, sage-2189, #1727)

This commit is contained in:
jason.woltje committed 2026-10-21 16:54:52 -05:00
1 parent f46e66883f
commit d572b61026
1 file changed
+134 -73
+134 -73
View File
@@ -4,8 +4,9 @@ Brief: docs/plans/2026-10-20_case2-rows.md "C2-13b install script and library pr
d37210288). Design: agents/gosalyn/work/case2/DESIGN.md r11 sections 4 and 10, on gosalyn/case2-design d37210288). Design: agents/gosalyn/work/case2/DESIGN.md r11 sections 4 and 10, on gosalyn/case2-design
75fb3e10e. Row 195's needs: agents/darkwing/work/queue-195/DESIGN.md "What row 195 needs from row 226". 75fb3e10e. Row 195's needs: agents/darkwing/work/queue-195/DESIGN.md "What row 195 needs from row 226".
This is design round d1. Nothing is built yet. Webby reviews the design before I write code, as for row This is design round d2. Nothing is built yet. d1 was f46e66883, and Webby's review of it is webby-0085
252. D1 to D9 below are my choices, and Q1 to Q3 need a ruling. (CHANGES, B1 and N1-N6) and webby-0086 (the ordering problem). Sage ruled on both in sage-2189. The
changes from d1 are listed at the end.
## Base ## Base
@@ -20,28 +21,36 @@ Branch gosalyn/c2-13b from refactor d37210288. Rows 215, 221, 222 and 230 are do
- The library is a project, `projects.<id>.kind === "library"`, checked by `checkMapping` - The library is a project, `projects.<id>.kind === "library"`, checked by `checkMapping`
(packages/library/src/mapping.mjs). There is no top-level `library` key. (packages/library/src/mapping.mjs). There is no top-level `library` key.
- `library.sync` and `library.status` are bus verbs, human only (broker.mjs:92, :2452-2476). The only - `library.sync` and `library.status` are bus verbs, human only (broker.mjs:92, :2452-2476). The only
client is `packages/bus/src/human-cli.mjs` over the bus socket, so they need a running bus. The client is `packages/bus/src/human-cli.mjs` over the bus socket.
library package exports `sync(context, { slot })` (sync.mjs:92) and `libraryStatus(context)` - The bus makes each library mirror at start, `ensureMirror({ business, project, dataRoot })`
(mirror.mjs:236), which take the lock the bus takes (`withLibraryLock`, mirror.mjs:145). (packages/bus/src/runtime.mjs:143-145). Before the bus first starts there is no mirror.
- The token slot is `<configDir>/secrets/<business>/library-gitea`, a regular file owned by the uid with - `sync()` fetches outside `withLibraryLock`. Only a per-process `running` set keeps two syncs apart
no group or other bit (sync.mjs:17, :51-52). (packages/library/src/sync.mjs:97-98), so a second process calling `sync()` would race the bus's
timer.
- `libraryStatus(context)` reads the recorded state under the flock and writes nothing
(mirror.mjs:232-239). A missing or unsafe mirror fails `library-git` (`ownedRoot`, mirror.mjs:43-49).
A mirror that has never synced returns the empty state, `lastFetch: null`, `lastError: null`,
`problems: []` (mirror.mjs:27, :205-210). `lastError` is `{ code, at }` with `code` matching
`^[a-z-]{1,64}$` (mirror.mjs:225).
- The bus builds the library context as `{ business, project, dataRoot }` and the slot as
`<slotDir>/secrets/<business.id>/library-gitea` (runtime.mjs:146-151, :162-164).
- `case2-persona-import.mjs install --staging DIR --config-dir DIR --business ID` writes - `case2-persona-import.mjs install --staging DIR --config-dir DIR --business ID` writes
`<configDir>/personas/<business>/<instance>.md` at 0600 in 0700 dirs, never overwriting, and prints `<configDir>/personas/<business>/<instance>.md` at 0600 in 0700 dirs, never overwriting, and prints
`instance bean: persona.name Bean, persona.sha256 <hex>` per persona. `instance bean: persona.name Bean, persona.sha256 <hex>` per persona.
- A Discord binding is `<dataRoot>/discord/<name>.json` (packages/discord/src/binding.mjs:70-83). - A Discord binding is `<dataRoot>/discord/<name>.json` (packages/discord/src/binding.mjs:70-83).
- `case2-unit.sh --copy ID` needs `<home>/.local/share/mosaic-stack/copies/<ID>`, which - `case2-unit.sh --copy ID` needs `<home>/.local/share/mosaic-stack/copies/<ID>`, which
`stack-copy.sh copy COMMIT --copies DIR` makes. `stack-copy.sh copy COMMIT --copies DIR` makes. The copy runs `npm ci` from package-lock.json
(stack-copy.sh:70), which installs the pinned pi.
## Design ## Design
### D1. The business file is an input ### D1. The business file is an input
`case2-install.sh` takes `--business-file FILE`, a JSON business document written for the host from Unchanged from d1. `case2-install.sh` takes `--business-file FILE`, a JSON business document written for
Jason's go: human id, arbiters, tracker, roles, the library project with its mapping, and any the host from Jason's go. The script doesn't build one from flags, because bots, botIds and credential
specialists she picks. The script doesn't build one from flags. Bots, botIds and credential refs are host refs are host values and the brief says every host value is an input. It checks the input, adds what
values, and the brief says every host value is an input. A flag per field would copy business.mjs's only it knows, writes it, and then runs `mosaic business validate`, so business.mjs stays the only
schema into bash. The script checks the input, adds what only it knows, writes it, and then runs schema.
`mosaic business validate`, so business.mjs stays the only schema.
What the script checks before it writes anything, beyond `validate`: What the script checks before it writes anything, beyond `validate`:
- `id` equals `--business`. - `id` equals `--business`.
@@ -54,36 +63,44 @@ What the script checks before it writes anything, beyond `validate`:
1. Refuse to run as root. 1. Refuse to run as root.
2. `scripts/bootstrap.sh`. It's idempotent and never rewrites an existing config. 2. `scripts/bootstrap.sh`. It's idempotent and never rewrites an existing config.
3. `scripts/release.sh package`, then `scripts/release.sh activate` (design 10 step 2b). 3. `scripts/stack-copy.sh copy <--commit> --copies <home>/.local/share/mosaic-stack/copies`. The copy
4. `scripts/stack-copy.sh copy <--commit> --copies <home>/.local/share/mosaic-stack/copies`. The copy
`case2-unit.sh --copy` names has to exist before root renders the units (row 195 need 3). `case2-unit.sh --copy` names has to exist before root renders the units (row 195 need 3).
5. `library` only: the persona import's `install` step from `--personas DIR` (D3). 4. `library` only: the persona import's `install` step from `--personas DIR` (D3).
6. Write `<configDir>/businesses/<id>.json` at 0600 with O_EXCL (D4). 5. Write `<configDir>/businesses/<id>.json` at 0600 with O_EXCL (D4).
7. `library` only: make `<configDir>/secrets/<business>/` at 0700, empty. The token is Jason's, step 4. 6. `library` only: make `<configDir>/secrets/<business>/` at 0700, empty. The token is Jason's, design 10
8. `mosaic business validate <id>`. If it refuses, remove the file step 6 wrote, then refuse. step 4.
9. Print the copy id and the next steps, which are root's 2c and Jason's 2d, 3 and 4. 7. `mosaic business validate <id>`. If it refuses and step 5 created the file in this run, remove that
file, then refuse. A file that already matched on a re-run (D4) is never removed (N4).
8. Print the copy id and the next steps: root's 2c, Jason's 2d, 3 and 4, then the `--library-status`
check (D6b).
d1's step 3, `release.sh package` and `activate`, is gone (Q4a, webby-0085 B1, sage-2189). Package is a
`docker compose build`, and activate runs a pi Compose health check that mounts `PI_AUTH_FILE`
(compose.yaml:43). Neither fits 2a, which has no Docker and no pi credential, and the hub doesn't run
containers. The pinned harness comes from the copy's `npm ci` (Q3). The `CASE2_RELEASE` seam goes with
it.
It starts no unit, writes no unit file, doesn't write `/etc/mosaic-stack/dev.args` and doesn't run It starts no unit, writes no unit file, doesn't write `/etc/mosaic-stack/dev.args` and doesn't run
`adopt`. These are my answers to row 195's needs 4 and 5 (gosalyn-0087), unchanged. `adopt`. These are my answers to row 195's needs 4 and 5 (gosalyn-0087), unchanged. Row 195's W8 now
passes `--business-file` and has no release step.
### D3. Personas come from the staging dir ### D3. Personas come from the staging dir
`library` requires `--personas DIR`, the staging dir from `case2-persona-import.mjs stage`. The install Unchanged from d1. `library` requires `--personas DIR`, the staging dir from `case2-persona-import.mjs
runs `case2-persona-import.mjs install --staging DIR --config-dir <configDir> --business <id>` and reads stage`. The install runs `case2-persona-import.mjs install --staging DIR --config-dir <configDir>
its two `instance` lines into `roles.bean.vars` and `roles.chet.vars`. Only those two lines go into the --business <id>` and reads its two `instance` lines into `roles.bean.vars` and `roles.chet.vars`. Only
business file. They hold a name and a digest, never text. those two lines go into the business file. They hold a name and a digest, never text.
This moves the persona import's stage part of design 10 step 6 ahead of 2b. That part is the read of This moves the persona import's stage part of design 10 step 6 ahead of 2b: the read of her Hermes
her Hermes profiles and Jason's check of the printed pairing. The install part stays as `<user>`, as profiles and Jason's check of the printed pairing. Webby agreed in webby-0085 (Q1).
design 9.1 says. See Q1.
### D4. Re-runs ### D4. Re-runs
Each step is safe to run again. bootstrap and the copy are idempotent. An existing business file that Unchanged from d1, plus N4. Each step is safe to run again. bootstrap and the copy are idempotent. An
matches the one the script would write passes. One that differs refuses existing business file that matches the one the script would write passes, and the script remembers it
`business-file-differs`, and the script doesn't touch it. The persona import already refuses to didn't create it. One that differs refuses `business-file-differs`, and the script doesn't touch it.
overwrite. On a re-run the install compares each existing persona file's sha256 with the staging The persona import already refuses to overwrite. On a re-run the install compares each existing
manifest, and refuses on a mismatch. persona file's sha256 with the staging manifest, and refuses on a mismatch.
### D5. Inputs and seams ### D5. Inputs and seams
@@ -93,47 +110,75 @@ case2-install.sh --business ID --business-file FILE --commit SHA [--profile fact
``` ```
`--home` defaults to `$HOME`, which is `/srv/mosaic` under `sudo -u mosaic -H`. `MOSAIC_CONFIG` is `--home` defaults to `$HOME`, which is `/srv/mosaic` under `sudo -u mosaic -H`. `MOSAIC_CONFIG` is
honoured as bootstrap.sh honours it. Test seams follow case2-unit.sh's `CASE2_*` pattern: honoured as bootstrap.sh honours it. One test seam, in case2-unit.sh's `CASE2_*` pattern:
- `CASE2_RELEASE` is the release script. The test uses a stub that logs its argv, because the real one `CASE2_STACK_COPY` is the copy script, and the test uses a stub that makes the copy dir.
needs Docker.
- `CASE2_STACK_COPY` is the copy script. The test uses a stub that makes the copy dir.
Exit codes: 0 ok, 1 refused, 4 usage, as case2-unit.sh uses. Exit codes: 0 ok, 1 refused, 4 usage, as case2-unit.sh uses.
### D6. Precheck library checks ### D6. Precheck library checks, split in two (Q4b, sage-2189)
These are added to case2-precheck.sh under `--profile library` only. Each is a `refuse` line in the d1 ran `sync()` from the precheck. Webby's N1 and webby-0086 showed two faults. The precheck runs before
script's style. the units start, so there is no mirror yet (runtime.mjs:143-145). And a precheck `sync()` after the bus
is up would race the bus's timer, since `sync()` fetches outside the lock (sync.mjs:97-98). Sage split
the check in sage-2189, and the brief's "runs library.sync" becomes "reads the bus's recorded sync
state".
#### D6a. Before the units start, `--profile library`
These run where the precheck runs today, after 2c's render and before the start. They sync nothing and
read no mirror. Each is a `refuse` line in the script's style.
- The business file is owned by the uid, with no group or other bit (`business-file-mode`). - The business file is owned by the uid, with no group or other bit (`business-file-mode`).
`businesses/.history/<id>/` is 0700 and its `changes.jsonl` is 0600, when present (`history-mode`). `businesses/.history/<id>/` is 0700 and its `changes.jsonl` is 0600, when present (`history-mode`).
- The persona dirs are 0700. Each creative role's persona file is regular, owned and 0600 - The persona dirs are 0700. Each creative role's persona file is regular, owned and 0600
(`persona-mode`), and its sha256 equals the role's `persona.sha256` (`persona-mismatch`). The output (`persona-mode`), and its sha256 equals the role's `persona.sha256` (`persona-mismatch`). The output
prints names and digests, never text. prints names and digests, never text.
- `library.sync` calls the library package's `sync(context, { slot })` from a node block with the bus's
context, then `libraryStatus`. A sync error refuses with its code (`library-sync: <code>`). Each
R-problem prints as `ok library R<n> <path>`. The paths are her file names, and no text
prints. R-problems don't refuse, since the brief says the check lists them. See Q2.
- No `<dataRoot>/discord/*.json` exists (`discord-binding`). - No `<dataRoot>/discord/*.json` exists (`discord-binding`).
The sync check needs the token from step 4. So the library precheck passes only after step 4, and the #### D6b. `case2-precheck.sh --business ID --library-status`, after the bus is up and after step 4
host run sheet says so.
A new mode, run as `<user>` once the bus is up and Jason has placed the token. It changes nothing. A
node block builds the context the bus builds, `{ business, project, dataRoot }` for each library
project, with `dataRoot` from the config (N3, runtime.mjs:146-151, :162-164). For each one it:
- refuses `library-mirror-missing` when the mirror path doesn't exist. It checks this itself, so the
message says the bus hasn't made it, not `library-git`;
- calls `libraryStatus(context)` only, and refuses with the thrown code when that fails, for example
`library-git` for an unsafe mirror or `library-sync-busy` when the flock wait runs out;
- refuses `library-never-synced` when `lastFetch` is null;
- refuses `library-last-error <code>` when `lastError` is not null. The code is the fixed
`^[a-z-]{1,64}$` string, so it's safe to print. This also covers Webby's N2;
- prints each recorded R-problem as `problem R<n> <path>`, with the path through `JSON.stringify`, then
one `problems <count>` line (N5). Her file names are the only part that prints, never text.
R-problems don't refuse (Q2, agreed by Webby).
The bus syncs each library with a `user` every 60 s, so the first run after step 4 may need a minute,
or Jason's own `library.sync`. The run sheet says so.
### D7. host-run-sheet.md fix ### D7. host-run-sheet.md fix
`docs/guides/host-run-sheet.md:239` says "I2's install writes it" about `dev.args`. It becomes: root `docs/guides/host-run-sheet.md:240` says "I2's install writes it" about `dev.args`. It becomes: root
writes it at the unit render, from the values the go names, and the install never writes it, because writes it at the unit render, from the values the go names, and the install never writes it, because
`<user>` can't write `/etc` and root reads nothing `<user>` wrote. Sage asked in sage-1832 for this fix `<user>` can't write `/etc` and root reads nothing `<user>` wrote. Sage asked in sage-1832 for this fix
to go in this build commit, with its path in the manifest. Section 9's `library-gitea` line (:270) is to go in this build commit, with its path in the manifest. Section 9's `library-gitea` line (:275) says
"C2-13b's `library.sync` (not landed)". It becomes the precheck's `library-sync` line. "C2-13b's `library.sync` (not landed)". It becomes the `--library-status` check (D6b). The install
section also gets a line that the copy's `npm ci` needs the npm registry from the host (Q3, webby-0085).
### D8. Tests, `packages/cli/tests/case2-install.test.mjs` ### D8. Design 10 text
Sage ruled that design 10 gets the same change when 226 lands, with the line in this row's manifest.
In agents/gosalyn/work/case2/DESIGN.md:
- step 2b drops `release.sh package` and `activate`;
- a new step after step 4 runs `case2-precheck.sh --business <business> --library-status`;
- the precheck's library text says it reads the bus's recorded sync state, not that it runs
`library.sync`.
### D9. Tests, `packages/cli/tests/case2-install.test.mjs`
These follow case2-unit.test.mjs: scratch HOME, MOSAIC_CONFIG, stub bin dir and seams, a local bare repo These follow case2-unit.test.mjs: scratch HOME, MOSAIC_CONFIG, stub bin dir and seams, a local bare repo
cloned `--bare --no-local` as sync.test.mjs does, and the fixture's key never in output. cloned `--bare --no-local` as sync.test.mjs does, and the fixture's key never in output.
- Install `library`: - Install `library`:
- writes the business file at 0600 with bean's and chet's persona vars; - writes the business file at 0600 with bean's and chet's persona vars;
- writes the persona files at 0600 in 0700 dirs and the empty slot dir at 0700; - writes the persona files at 0600 in 0700 dirs and the empty slot dir at 0700;
- logs the release then copy calls in order; - logs the copy call and no release call;
- starts nothing; - starts nothing;
- passes on a re-run; - passes on a re-run;
- refuses `business-file-differs` and leaves the file unchanged. - refuses `business-file-differs` and leaves the file unchanged.
@@ -142,31 +187,47 @@ cloned `--bare --no-local` as sync.test.mjs does, and the fixture's key never in
- refuses a library project and `bus.tester` off. - refuses a library project and `bus.tester` off.
- Install refuses: run as root, a business id mismatch, missing `--personas` under library, and a - Install refuses: run as root, a business id mismatch, missing `--personas` under library, and a
`validate` refusal, after which no business file is left. `validate` refusal, after which no business file is left.
- Precheck library refusals, one case each, as the brief lists: - N4: a re-run where the matching business file is already there and `validate` refuses leaves that
- a persona file at 0644; file in place, byte for byte.
- a digest mismatch; - D6a refusals, one case each: a persona file at 0644, a digest mismatch, a business file at 0644, a
- a business file at 0644; `changes.jsonl` at 0644, and a Discord binding. A clean library host passes D6a with no mirror.
- a sync failure, which is no token in the slot and a remote that doesn't resolve; - D6b, with the mirror made by `ensureMirror` and the state written by a real `sync()` against the local
- a Discord binding. bare repo, as the bus would:
- Precheck library pass: a bare repo with one R2 path prints its `R2` line and passes. - no mirror refuses `library-mirror-missing`;
- a fresh mirror refuses `library-never-synced`;
- a sync against a remote that doesn't resolve refuses `library-last-error` with its code;
- a synced bare repo with one R2 path prints its `problem` line and `problems 1`, and passes.
Every new refusal gets a mutant that removes it. The count goes in the slot request. Every new refusal gets a mutant that removes it. The count goes in the slot request.
### D9. Files ### D10. Files
Owned by the brief: `scripts/case2-install.sh` (new), `scripts/case2-precheck.sh` (library checks Owned by the brief: `scripts/case2-install.sh` (new), `scripts/case2-precheck.sh` (library checks and
only) and `packages/cli/tests/case2-install.test.mjs` (new). Also `docs/guides/host-run-sheet.md` (D7, `--library-status` only) and `packages/cli/tests/case2-install.test.mjs` (new). Also
sage-1832) and this BUILD.md. Before the slot request I'll grep for outside readers of `docs/guides/host-run-sheet.md` (D7, sage-1832), agents/gosalyn/work/case2/DESIGN.md (D8, sage-2189)
case2-precheck.sh, since case2-unit.test.mjs runs it. and this BUILD.md. Before the slot request I'll grep for outside readers of case2-precheck.sh, since
case2-unit.test.mjs runs it.
## Questions ## Questions
- Q1. Design 10 order. D3 puts the persona stage and Jason's pairing check before 2b. The other way All three d1 questions are settled:
is for the install to write bean and chet without persona vars and a second run to add them after step - Q1, persona stage before 2b: Webby agreed (webby-0085).
6. That needs a business file change after install, and D4 refuses that. I recommend D3. - Q2, R-problems don't refuse: Webby agreed.
- Q2. Should an R-problem refuse the library precheck? The brief says the check "lists" them, so D6 - Q3, the pinned harness is the copy's `npm ci`: Webby agreed, and asked for the registry note in D7.
doesn't refuse. Her repo may well have R2 paths on day one, before she confirms the mapping in step 7. - Q4a and Q4b: Sage, sage-2189.
- Q3. Design 10 step 2b names "the pinned harness" as part of the install. I read that as the pinned
pi from package.json. The copy installs it with `npm ci` from package-lock.json (stack-copy.sh:70), One choice for Webby to check: D6b's `problem` line carries the path through `JSON.stringify`. N5 asked
so it isn't a separate install step. That `npm ci` needs the npm registry from the host. Does for a prefix and a count. I kept the path because Jason needs it to tell her which file to fix, and the
anyone read it as more than that? names are hers, not text.
## Changes from d1
- B1: release package and activate dropped, with the `CASE2_RELEASE` seam and its test (D2, D5).
- N1 and webby-0086: the sync is gone from the precheck. D6 splits into D6a before the start and D6b,
`--library-status`, after the bus is up and step 4 (sage-2189).
- N2: covered by D6b's `lastError` refusal.
- N3: D6b builds the context and slot as the bus does.
- N4: step 7 removes only a file this run created, with a test.
- N5: `problem` prefix and a `problems` count.
- N6: the run sheet lines are :240 and :275.
- New D8, the design 10 text change, and its path in D10.