gosalyn: Case 2 design r4 for #416 (T2): one human-actor rule on all five creative write paths incl. routes.set, chief's launch.models change on a creative key refuses, C2-20 count-only dry run, prompt passed by file not argv (webby-0017, mos-1038, mos-1051)

This commit is contained in:
jason.woltje committed 2026-10-20 14:48:00 -05:00
1 parent 2958acb81e
commit 5fc296e708
1 file changed
+99 -14
+99 -14
View File
@@ -1,4 +1,4 @@
# Case 2 design: the creative writing hub (r3)
# Case 2 design: the creative writing hub (r4)
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,45 @@ 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 r3
Webby's review (webby-0017, CHANGES, notes at agents/webby/work/case2-r3/REVIEW.md, fcac66f7b), with
Mos's rulings (mos-1051). I re-read each cited line at 9ae8c73ac, and each one holds.
- B1. `routes.set` was a fourth path. The tester may call it (`broker.mjs:103`), and `setRoutes`
(`launch-settings.mjs:240-277`) checks no definition before its write, so the tester could change a
creative instance's harness, model and thinking. r3's "could change no creative instance" was false
with `bus.tester` on. All creative-instance checks are now one rule: the write must come from the
business file's `human`. Role sessions write as `<definition>.<instance>` (`broker.mjs:1278`) and the
tester as `tester` (`:1272`), so neither passes. The path list in 4 names all five paths, and M10 has a
test for each (4, 5.1).
- N1, ruled refuse (mos-1051 item 2). `organization.set`'s `models` key (`launch-settings.mjs:169`)
could change the model behind a `launch.models` key that Bean or Chet names. A change or removal of
such a key now refuses, with a test and a mutant. `organization.set` is agent-only, so the human never
reaches that path, and her model changes go through `instances.set` and `routes.set`. Ending a run stays with
the chief of staff (mos-1038 item 2).
- N2 (mos-1051 item 3). Before Jason's go, C2-20 makes a dry run over her persona files that reports
only counts: files, lines, and lines starting `# ` or `## `. If the heading rule would refuse a real
persona, the rule changes before the import. Any rewrite of her text needs Melanie's say (9.1).
- N3 (mos-1051 item 4). The persona text sits in two more places, and 4 names both. `prompt.md` is
written 0600 into each run's bundle and kept until prune (`bundle.mjs:241-244`, `:265`). Both adapters
pass it as an argument (`adapters/claude/adapter.sh:79`, `:82`, `adapters/pi/adapter.sh:106`, `:121`),
so any local user on the host can read it in `/proc/<pid>/cmdline`. The adapters now pass the file,
not its text. Pi 0.85.1 reads `--system-prompt` as a file when the value is an existing path. Claude's
help names `--system-prompt-file`. C2-08 does this, and C2-13 checks the run root's mode.
- Webby's answers. Keying on the actor is enough, and C2-08 runs the tester cases through the broker
with a bound tester session, so a change at `broker.mjs:1272` fails a test. The heading refusal fails
closed, and N2 covers its risk.
Mos, mos-1038 items 2 and 3. The chief of staff keeps ending runs, which matches TARGET.md:66, and a
per-agent forbid comes through Jason (4). ops-01 makes the 16 GiB, 6-core change at
2026-10-21T09:00Z (9).
Section 10 step 1 is on hold (mos-1050). Jason isn't pursuing #421 and accepts the risk, and Mos is
confirming that reading with him. The step keeps its r3 wording until he does. If he confirms, step 1
becomes "Jason's written acceptance of the factory paths to .54 is recorded", and section 9's isolation
paragraph changes with it.
## Changes from r2
Mos, mos-1031 (Jason's answer to Q2, DECISIONS Q181). The host is now fixed (9, 10):
@@ -461,17 +500,37 @@ at most 8,000 characters, mode 0600, in 0700 directories owned by `<user>`. The
with `O_NOFOLLOW`. It refuses a file that isn't regular, isn't owned by the bus's uid or has any group or
other bit, the same check `organization.mjs:141` makes on `changes.jsonl`. It refuses `persona-mismatch`
when the file's sha256 isn't the var's. So the `instances` read, the Console's view of it and
`changes.jsonl` show `persona.sha256` and never the text. The persona's own session sees the text in its
prompt, which is the point. No verb returns the file, and agent file tools stay inside the workspace
(`packages/harness/src/gate.mjs:146-156`), so no other session can read it. A persona var or file is
refused on a non-creative definition.
`changes.jsonl` show `persona.sha256` and never the text. No verb returns the file, and agent file tools
stay inside the workspace (`packages/harness/src/gate.mjs:146-156`). A persona var or file is refused on
a non-creative definition.
Where else the text sits (N3). The persona's own session gets it in its prompt, which is the point, and
two copies come with that:
- `prompt.md` in each run's bundle, mode 0600 in a 0700 directory (`bundle.mjs:241-244`, `:265`), kept
until the run is pruned. Only `<user>` can read it. The C2-13 precheck checks the run root's mode.
- The harness's argv. Both adapters read `prompt.md` into a variable and pass the text as
`--system-prompt` (`adapters/claude/adapter.sh:79`, `:82`, `adapters/pi/adapter.sh:106`, `:121`). Any
local user on the host can read that in `/proc/<pid>/cmdline`. C2-08 makes both adapters pass the
file instead. Pi 0.85.1, the pinned version (`package.json:8`), reads `--system-prompt` as a file when
the value is an existing path (`dist/core/resource-loader.js:17-30`). If the read fails it falls back
to using the path itself as the prompt (`:24-27`), so the adapter checks the file is readable first
and refuses otherwise. Claude Code 2.1.304's `--help` names `--system-prompt-file`. I read the help
but didn't run it, and the repo doesn't pin the claude version. C2-08's adapter tests check that argv
holds the path and not a marker from the prompt, and C2-13 checks the flag on the host's claude. If the
host's claude lacks the flag, the argv copy is a residual for Jason, stated with that version.
Modes, from code. The writer creates the business file's candidate and `changes.jsonl` with mode 0600
(`organization.mjs:61`, `:139`) and its `.history` directories with 0700 (`:39`). It refuses a log with any
group or other bit (`:141`). C2-08 tests those modes after an `instances.set` write, and the C2-13
precheck checks the business file, `.history` and the persona files on `<host>`.
Who can change a persona or a creative instance (B3). Today's paths, and what C2-08 does to each:
Who can change a persona or a creative instance (B3, and r3's B1). The rule is one check, used on every
path below: a write that touches a `CREATIVE_DEFINITIONS` instance is accepted only when its actor is
the business file's `human`. The broker sets the actor to the human's id for a human session, to
`tester` for the tester (`broker.mjs:1272`), and to `<definition>.<instance>` for a role session such as
the chief of staff (`:1278`). Otherwise it refuses `organization-creative-human-only`. The five paths
today, and what C2-08 does to each:
- `organization.set`'s `assistants` key, which the chief of staff holds (`broker.mjs:145`, `:1266-1271`),
targets only definition `assistant` (`packages/business/src/launch-settings.mjs:140`, `:186`, `:191`).
@@ -480,6 +539,19 @@ Who can change a persona or a creative instance (B3). Today's paths, and what C2
- `organization.set`'s `instances` key sets the `model` var of any instance (`launch-settings.mjs:170-177`).
It now refuses a creative instance with `organization-creative-instance`, for every actor. Test: the
chief of staff's and the human's `instances: {bean: ...}` both refuse.
- `organization.set`'s `models` key replaces `launch.models` whole (`launch-settings.mjs:169`), which can
change the model behind a key Bean or Chet names without touching the instance. A write that changes
or removes a `launch.models` entry named by a creative instance's `model` or `fallback.model` now
refuses `organization-creative-human-only` (N1, mos-1051 item 2). `organization.set` takes only an
agent session. Its authority check (`broker.mjs:1277`) refuses a human with `agent-required` (`:370`), so no verb writes
`launch.models` for the human today. She changes Bean's model with `instances.set` or `routes.set`,
pointing at a key she owns, and Jason edits `launch.models` itself by hand. Test: the chief of staff's
change to Bean's key refuses with the file unchanged, and its change to an unnamed key works. Mutant:
dropping the check makes the first case fail.
- `routes.set` replaces an instance's harness, model and thinking vars (`launch-settings.mjs:240-277`).
It is human only, but the tester may call it too (`broker.mjs:103`, `:1258`). `setRoutes` checks no
definition before its write at `:277`. It now refuses a creative instance unless the human calls it.
Test: the tester's `routes.set` on `bean` refuses with the file unchanged, and the human's works.
- `instances.set` is human only, and the tester may call it too (`broker.mjs:103`, `:1263-1265`). Today it
refuses any `END_PROTECTED` definition (`instance-settings.mjs:23`, `:109`), which would make every
creative add or change a hand edit. C2-08 adds one branch. An add or change of a
@@ -490,7 +562,12 @@ Who can change a persona or a creative instance (B3). Today's paths, and what C2
refuses an `assistant` instance.
- The tester is off on her business. `bindTester` refuses unless `bus.tester` is exactly true
(`broker.mjs:542-545`), and the C2-13 precheck refuses a library business with it on. If it were on,
the tester would see digests only and could change no creative instance.
the tester would see digests only, and each of the paths above would refuse it.
C2-08 runs every tester case through the broker with a bound tester session, and every chief of staff
case with a role session, so a change to how the broker sets the actor (`:1272`, `:1278`) fails a test.
The human id can't be `tester` (`broker.mjs:245`), and no instance may be named `tester`
(`packages/business/src/instance-settings.mjs:86`).
The persona block (B4). `prompt()` puts the contract first (`packages/harness/src/bundle.mjs:177`), then
"## This session" (`:179`) and the session lines. The persona block goes between the two:
@@ -538,7 +615,9 @@ New constants in `vocabulary.mjs`, each changed only by a reviewed commit:
- `END_PROTECTED` (`vocabulary.mjs:135`) becomes `THREAD_DEFINITIONS` (N2). An instance of one is then
ended only by the chief of staff or a decision the human resolved (`broker.mjs:148-150`, Q169), not by
another agent. r2 said only the human, which was wrong. Ending a run changes no persona, and a thread's
session stops after 900 idle seconds anyway.
session stops after 900 idle seconds anyway. This matches TARGET.md:66 ("hire/fire authority for agents
as needed; unless user forbids per agent"). If Melanie wants it forbidden for an agent, the request
comes through Jason (mos-1038).
- `CREATIVE_DEFINITIONS`, the six above.
- `CREATIVE_KINDS`, mapping each definition to the item kinds in the table.
- `CREATIVE_TOOLS = ["read", "grep", "find", "ls", "web"]`. `validateRoleDocument` refuses a creative
@@ -560,7 +639,7 @@ New constants in `vocabulary.mjs`, each changed only by a reviewed commit:
| M7 no raw session view | On a business with a library project, the conversation reader leaves creative sessions out of `catalogue()`, and `open()` refuses them with `conversation-not-listed` | C2-10a: a creative session file under the root isn't listed and won't open by id. An assistant session still lists |
| M8 no raw Discord engine | On a business with a library project, `discord check` and `discord run` refuse a binding whose engine isn't `mosaic-bus` (`discord-engine-not-allowed`), and a `mosaic-bus` binding with `tools` | C2-10a: both refuse on a library business. A pi binding on a business without a library still checks clean |
| M9 no agent write to the mapping | An organization write whose `projects` differ from the file's refuses `organization-projects-protected`, in `organizationWriter.write` | C2-04: `write` called directly with a changed playground prefix, and again with a changed hybrid entry, refuses, and the file and `changes.jsonl` are unchanged |
| M10 personas and creative instances are the human's | The chief of staff's `assistants` and `instances` keys on a creative instance, and the tester's `instances.set` on one. A persona file that fails its digest, mode or fence check | C2-08: each path refuses with its code (4), and the file is unchanged. No `instances` read or `changes.jsonl` line holds a marker string from a fixture persona |
| M10 personas and creative instances are the human's | Any write by an actor other than the business file's `human` that touches a creative instance: the chief of staff's `assistants`, `instances` and `models` keys, and the tester's `instances.set` and `routes.set`. A persona file that fails its digest, mode or fence check. A harness argv that holds the prompt text | C2-08: each path refuses with its code (4) and the file is unchanged. The human's `instances.set` and `routes.set` on `bean` work. Tester cases through a bound tester session. A mutant without the `models` check fails. No `instances` read, `changes.jsonl` line or adapter argv holds a marker string from a fixture persona |
Residual, in numbers. One reply can carry up to 5 `idea` items of 200 characters, which is 1,000
characters of free suggestion text inside a total of 1,200 characters of item text. Each item must quote
@@ -794,13 +873,13 @@ Chains follow Q177 merge-chaining. S marks a slice row.
| C2-05b | S | Broker lookup verbs and typed tools. Sync before a read older than 60 s | C2-05a, C2-03a | Sol (Rocko) | broker verb tests. Typed tool name snapshot per creative role (M3) |
| C2-06 | S | Cited-reply guard (5.2) in `packages/library`, the broker's pre-check on agent-to-human `message.send`, the page store, the three notices | C2-05a | Opus | one pass and one refusal per check in M5 and 5.2, through the RESULT path and `send_message`, and with a non-creative sender. `messages` count unchanged on each refusal |
| C2-07 | S | Runner retry on a guard refusal (at most 2), then `no-cited-answer`. `no-answer` and `turn-failed` in a library business | C2-06 | Haiku high | fake harness: free text then a valid reply stores one message. An empty and a failed turn store the fixed notices |
| C2-08 | S | Six role files and contracts, the new constants, M1, M6, the `webPlan` refusal, gate tests (M2), messaging test. `persona.name` and `persona.sha256` in `REGISTRY`, the persona file read and checks, and the fenced persona block between the contract and "## This session" in `prompt()`. M10: the `organization.set` refusals and the `instances.set` creative branch (4) | none | Opus | role tests, gate tests, a creative raise refused, `harness-builtin` refused for `research` and `companion`, `scripts/test-config.sh`. Persona vars refused at other layers, malformed and on a non-creative definition. The persona file refused when missing, a symlink, another uid's, group or other readable, over 8,000 characters, off its digest, holding `</persona-file>` or a `# `/`## ` line. A prompt snapshot pinning contract, persona, session. The M10 tests. Business file, `.history` and `changes.jsonl` modes after a write |
| C2-08 | S | Six role files and contracts, the new constants, M1, M6, the `webPlan` refusal, gate tests (M2), messaging test. `persona.name` and `persona.sha256` in `REGISTRY`, the persona file read and checks, and the fenced persona block between the contract and "## This session" in `prompt()`. M10: the `organization.set` refusals, the `models` check, the `routes.set` and `instances.set` creative branches (4). Both adapters pass the prompt as a file, not its text (4) | none | Opus | role tests, gate tests, a creative raise refused, `harness-builtin` refused for `research` and `companion`, `scripts/test-config.sh`. Persona vars refused at other layers, malformed and on a non-creative definition. The persona file refused when missing, a symlink, another uid's, group or other readable, over 8,000 characters, off its digest, holding `</persona-file>` or a `# `/`## ` line. A prompt snapshot pinning contract, persona, session. The M10 tests, tester cases through a bound tester session, the `models` mutant. Adapter tests: argv holds the prompt path and no marker from the prompt, and pi's adapter refuses an unreadable prompt file. Business file, `.history` and `changes.jsonl` modes after a write |
| C2-09a | S | `#threaded` and launcher read `THREAD_DEFINITIONS`. `thread.create` takes `instance`. The Console new-thread control picks it | C2-08 | Sol (Rocko) | the C2-09a tests in section 6 |
| C2-10a | S | M7 in the conversation reader. M8 in `discord check` and `discord run` | C2-08 | Haiku high | the M7 reader test, the M8 tests |
| 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-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 | 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 |
| 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, the `mosaic-stack.service` 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 no groups, `bus.tester` is not on, the business file, `.history`, `changes.jsonl` and persona files have the modes in 4 and each persona digest matches, 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-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, the `mosaic-stack.service` 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 no groups, `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, the host's claude accepts `--system-prompt-file` (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-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 |
@@ -842,7 +921,7 @@ already uses.
Memory. From mos-1000, her two Hermes services use about 1.8 GB together, and each Mosaic agent session
measured 300 to 390 MB on dragon-lin. I estimated at least 2 GB more than the 3.8 GiB her guest saw, and
4 GB more with specialists running at the same time. ops-01 is setting VM 1012 to 16 GiB fixed with 6
cores (mos-1031), which covers both. C2-13's precheck still prints free memory before the start.
cores at 2026-10-21T09:00Z (mos-1031, mos-1038), which covers both. C2-13's precheck still prints free memory before the start.
Isolation on that host. `<user>` without sudo keeps the hub from touching her files. The reverse was the
B7 residual in r2, because `melanie` had passwordless sudo (mos-1000). Step 2b of ops-01's plan removes
@@ -863,7 +942,13 @@ her sudo and lxd, so it is no longer a residual to accept. It depends on Jason a
second step run as `<user>` installs it as `<configDir>/personas/<business>/<instance>.md` (4). It
prints `persona.name` and `persona.sha256` for the business file entries of `bean` and `chet`, and
otherwise names, byte counts and sha256 only. The text never enters the repository, the business file
or `changes.jsonl`, and only Bean's or Chet's own session sees it.
or `changes.jsonl`. Outside the persona file, it sits only in Bean's or Chet's own run bundles, 0600
until prune, and in the harness argv if the host's claude lacks `--system-prompt-file` (4).
- Dry run first (N2, mos-1051 item 3). Before Jason's go to import, the row runs a dry pass that reads the
same two persona definitions and prints counts only: files, lines, and lines starting `# ` or `## `.
No text and no line numbers. If any count of heading lines is nonzero, the fence rule in 4 would refuse
a real persona, so the rule changes, in a reviewed C2-08 follow-up, before the import runs. The text is
never rewritten to fit the rule unless Melanie says so through Jason.
- Hermes keeps running. The row changes nothing in the Hermes checkout, its profiles, its services or its
Discord bots. Bean and Chet in Mosaic are new agents with the same persona, not a move of the Hermes
ones. She uses both until she is happy, and stopping Hermes is her choice through Jason, after Bean and