gosalyn: row 226 C2-13b design d1, the business file as an input, install steps, persona staging before 2b, re-run rules, precheck library checks and tests, Q1-Q3 for review (#1727)

This commit is contained in:
jason.woltje committed 2026-10-23 13:50:44 -05:00
1 parent 45dca69f6e
commit bfed0aa6c7
1 file changed
+172
+172
View File
@@ -0,0 +1,172 @@
# Row 226 build notes, C2-13b install script and library precheck (#1727)
Brief: docs/plans/2026-10-20_case2-rows.md "C2-13b install script and library precheck" (:1073-1134 at
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".
This is design round d1. Nothing is built yet. Webby reviews the design before I write code, as for row
252. D1 to D9 below are my choices, and Q1 to Q3 need a ruling.
## Base
Branch gosalyn/c2-13b from refactor d37210288. Rows 215, 221, 222 and 230 are done.
## Facts the design rests on
- No CLI writes a business file. `mosaic business validate <id>` checks one and exits 0 ok, 2 invalid
(packages/business/src/cli.mjs). A business file needs `human`, `arbiters`, `projects`, `tracker`
with a sync bot and a Vikunja credential ref, and `roles`, each role with its tracker bot when its
definition uses Vikunja (business.mjs:23, :84-100, :131-149).
- The library is a project, `projects.<id>.kind === "library"`, checked by `checkMapping`
(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
client is `packages/bus/src/human-cli.mjs` over the bus socket, so they need a running bus. The
library package exports `sync(context, { slot })` (sync.mjs:92) and `libraryStatus(context)`
(mirror.mjs:236), which take the lock the bus takes (`withLibraryLock`, mirror.mjs:145).
- The token slot is `<configDir>/secrets/<business>/library-gitea`, a regular file owned by the uid with
no group or other bit (sync.mjs:17, :51-52).
- `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
`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).
- `case2-unit.sh --copy ID` needs `<home>/.local/share/mosaic-stack/copies/<ID>`, which
`stack-copy.sh copy COMMIT --copies DIR` makes.
## Design
### D1. The business file is an input
`case2-install.sh` takes `--business-file FILE`, a JSON business document written for the host from
Jason's go: human id, arbiters, tracker, roles, the library project with its mapping, and any
specialists she picks. The script doesn't build one from flags. Bots, botIds and credential refs are host
values, and the brief says every host value is an input. A flag per field would copy business.mjs's
schema into bash. The script checks the input, adds what only it knows, writes it, and then runs
`mosaic business validate`, so business.mjs stays the only schema.
What the script checks before it writes anything, beyond `validate`:
- `id` equals `--business`.
- `library`: at least one library project, `bus.tester` not true, roles `bean` and `chet` with
definition `companion`, and no `persona.*` var in the input, since D3 adds them.
- `factory`: no library project, and `bus.tester` true. These are the precheck's rules at
case2-precheck.sh:127-135, so the install refuses what the precheck would refuse.
### D2. Install steps, in order
1. Refuse to run as root.
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).
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).
5. `library` only: the persona import's `install` step from `--personas DIR` (D3).
6. 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.
8. `mosaic business validate <id>`. If it refuses, remove the file step 6 wrote, then refuse.
9. Print the copy id and the next steps, which are root's 2c and Jason's 2d, 3 and 4.
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.
### D3. Personas come from the staging dir
`library` requires `--personas DIR`, the staging dir from `case2-persona-import.mjs stage`. The install
runs `case2-persona-import.mjs install --staging DIR --config-dir <configDir> --business <id>` and reads
its two `instance` lines into `roles.bean.vars` and `roles.chet.vars`. Only 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
her Hermes profiles and Jason's check of the printed pairing. The install part stays as `<user>`, as
design 9.1 says. See Q1.
### D4. Re-runs
Each step is safe to run again. bootstrap and the copy are idempotent. An existing business file that
matches the one the script would write passes. One that differs refuses
`business-file-differs`, and the script doesn't touch it. The persona import already refuses to
overwrite. On a re-run the install compares each existing persona file's sha256 with the staging
manifest, and refuses on a mismatch.
### D5. Inputs and seams
```
case2-install.sh --business ID --business-file FILE --commit SHA [--profile factory|library]
[--personas DIR] [--home DIR]
```
`--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:
- `CASE2_RELEASE` is the release script. The test uses a stub that logs its argv, because the real one
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.
### D6. Precheck library checks
These are added to case2-precheck.sh under `--profile library` only. 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`).
`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
(`persona-mode`), and its sha256 equals the role's `persona.sha256` (`persona-mismatch`). The output
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`).
The sync check needs the token from step 4. So the library precheck passes only after step 4, and the
host run sheet says so.
### 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
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
to go in this build commit, with its path in the manifest. Section 9's `library-gitea` line (:270) is
"C2-13b's `library.sync` (not landed)". It becomes the precheck's `library-sync` line.
### D8. 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
cloned `--bare --no-local` as sync.test.mjs does, and the fixture's key never in output.
- Install `library`:
- 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;
- logs the release then copy calls in order;
- starts nothing;
- passes on a re-run;
- refuses `business-file-differs` and leaves the file unchanged.
- Install `factory`:
- writes no library mapping, no persona file and no slot dir, which is the brief's case;
- refuses a library project and `bus.tester` off.
- Install refuses: run as root, a business id mismatch, missing `--personas` under library, and a
`validate` refusal, after which no business file is left.
- Precheck library refusals, one case each, as the brief lists:
- a persona file at 0644;
- a digest mismatch;
- a business file at 0644;
- a sync failure, which is no token in the slot and a remote that doesn't resolve;
- a Discord binding.
- Precheck library pass: a bare repo with one R2 path prints its `R2` line and passes.
Every new refusal gets a mutant that removes it. The count goes in the slot request.
### D9. Files
Owned by the brief: `scripts/case2-install.sh` (new), `scripts/case2-precheck.sh` (library checks
only) and `packages/cli/tests/case2-install.test.mjs` (new). Also `docs/guides/host-run-sheet.md` (D7,
sage-1832) 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
- Q1. Design 10 order. D3 puts the persona stage and Jason's pairing check before 2b. The other way
is for the install to write bean and chet without persona vars and a second run to add them after step
6. That needs a business file change after install, and D4 refuses that. I recommend D3.
- Q2. Should an R-problem refuse the library precheck? The brief says the check "lists" them, so D6
doesn't refuse. Her repo may well have R2 paths on day one, before she confirms the mapping in step 7.
- 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),
so it isn't a separate install step. That `npm ci` needs the npm registry from the host. Does
anyone read it as more than that?