diff --git a/agents/gosalyn/work/case2/DESIGN.md b/agents/gosalyn/work/case2/DESIGN.md index 05de030ad..6c5a36eb7 100644 --- a/agents/gosalyn/work/case2/DESIGN.md +++ b/agents/gosalyn/work/case2/DESIGN.md @@ -143,32 +143,59 @@ A `correction`'s `to` is within edit distance max(`correctionMinDistance`, `corr `from`) of its `from`. Where. The object sits beside `projects`, not inside the library entry, because M9 keeps `projects` to -hand edits and Mos asked for a CLI verb. `TOP_KEYS` (`business.mjs:21`) gains `replyLimits`, and +hand edits and Mos asked for a CLI verb. `TOP_KEYS` (`packages/business/src/business.mjs:24`) gains `replyLimits`, and `validateBusinessDocument` accepts any JSON value under it. A bad hand edit then never stops her business from loading. Who changes them (M11). Only the business file's `human`, through a new human verb `library.limits.set` with `revision` and `replyLimits`. She runs it as `mosaic limits replies --file `, a new verb in `packages/cli/src/limits.mjs` beside `mosaic limits set`, and later from the Console. The verb replaces the whole object, and a key left out means its default. It sits outside -`ORGANIZATION_KEYS` and `TESTER_ORGANIZATION_VERBS`, as `instances.set` does (`broker.mjs:103`, `:145`, +`ORGANIZATION_KEYS` and `TESTER_ORGANIZATION_VERBS`, as `instances.set` does (`broker.mjs:219`, `:139`, sage-0918). An agent session gets `human-required` and the tester gets `tester-refused`. A value outside the -table refuses `invalid-request`, and nothing is written. The fence that covers every other path sits in -`organizationWriter.write` (`organization.mjs:177`), next to M9: a write whose `replyLimits` differs from -the file's, compared in canonical JSON, refuses `organization-reply-limits-human-only` unless `actor` equals -the file's `human`. That stops the chief of staff's `organization.set`, any actor's `limits.set`, -`instances.set`, `routes.set` and `plugins.set`, the tester, and a direct call to `write`. Every setter -builds its document from the file, so none of them changes `replyLimits` today. +table refuses `invalid-request`, and so does a key not in the table. Nothing is written. + +The fence that covers every other path sits in `organizationWriter.write` +(`packages/business/src/organization.mjs:180`), next to M9. It compares against the file on disk, `old` +in `write`, never against the incoming document. A write whose `replyLimits` differs from +`old.replyLimits`, compared in canonical JSON, refuses `organization-reply-limits-human-only` unless both +hold: +- `actor` equals `old.human`. A write that sets `human` to its own actor in the same document still + compares against the old `human`, so it refuses. +- `actor` isn't `tester` and isn't `.` for any role in `old.roles`, the two shapes of + an agent actor (`broker.mjs:4201`, `:4206`, `:4306`). `ID_PATTERN` (`vocabulary.mjs:199`) allows a dotted + `human` id, and the broker refuses only `tester` (`broker.mjs:328`), so without this a human id such as + `assistant.bean` would let that agent through. + +As M9 does (`organization.mjs:191-193`, `:199-200`), the fence checks `document` and then `written`, the +JSON round trip, so a value with a `toJSON` can't differ between the check and the disk. That stops the +chief of staff's `organization.set`, any actor's `limits.set`, `instances.set`, `routes.set` and +`plugins.set`, the tester, and a direct call to `write`. Every setter builds its document with +`structuredClone` of the file, so none of them changes `replyLimits` today and the fence refuses no +ordinary write. Read at check time. The guard reads `replyLimits` from the business file on each check, through the -organization reader, not from the broker's copy loaded at start (`broker.mjs:276-278`). A change applies to -the next reply with no restart. +organization reader, not from the broker's copy loaded at start (`broker.mjs:419`, read by `#business` +at `:570`). A change applies to the next reply with no restart. + +A failed read never skips the check. If the business file is missing, can't be read, isn't JSON or isn't +an object at check time, the guard checks the reply at the defaults and `library.status` shows +`reply-limits-unreadable`. The reply is neither refused for that alone nor let through unchecked. + +Fallback. Each key resolves on its own. The guard reads a key only if `Object.hasOwn(replyLimits, key)`, +and uses it only if `Number.isInteger(value)` is true and the value is within the table's range. Anything +else resolves to its default: a missing key, `null`, a boolean, a string such as `"5"`, an array such as +`[7]`, an object, a fraction, `NaN`, a negative number, a value below the minimum or above the ceiling. +Every key resolves to its default when `replyLimits` is missing or isn't a plain object, an array +included. The guard never compares a value it hasn't checked with `Number.isInteger`, so JavaScript's +coercion of `true` or `[7]` to a number can't reach a range check. Unknown keys are ignored when read. The +guard has no path without a number, so no value reads as "no limit". `library.status` shows the limits in +force and names each key that fell back. -Fallback. Each key resolves on its own. A key that is missing, `null`, a string, a fraction, negative, -below its minimum or above its ceiling resolves to its default, and so does every key when `replyLimits` -is missing or isn't an object. Unknown keys are ignored. The guard has no path without a number, so no -value reads as "no limit". `library.status` shows the limits in force and names each key that fell back. The ceilings are validity bounds, not policy: they keep a value from reading as unlimited. 100,000 -characters is far past any reply, and Mos can change them. +characters is far past any reply, and Mos can change them. At the ceilings the human can make the guard +loose. The idea residual is `ideasPerReply` times `ideaLength`, up to 100,000,000 characters at both +ceilings, and `correctionPercent` 100 lets a correction replace its whole `from`. Cites stay required at +every value. `library.status` shows the residual, and the setup sheet says so in one line. What agents see. The contracts don't state numbers. A refusal reason names the key and its value in force, so the runner's retry (C2-07) tells the agent the limit it broke. @@ -952,13 +979,13 @@ New constants in `vocabulary.mjs`, each changed only by a reviewed commit: | M2 tool gate (`gate.mjs:137-138`, unchanged) | `Write`, `Edit` or `Bash` from a creative session | C2-08: `decide()` with each creative policy refuses all three, on pi and claude-code | | M3 typed tool set (`tools.mjs`) | The tool list is exact: lookups and `send_message`. No write tool, no `raise_decision` | C2-05b: a snapshot of tool names per creative role | | M4 library verbs are human only | `library.file`, `library.sync` and `library.status` from an agent session, with `human-required` as at `broker.mjs:1434-1435`. Later write verbs join the list | C2-04 and C2-03a: each verb from an agent session refuses | -| M5 cited-reply guard (5.2) | A reply that isn't a list of short cited items, within the `replyLimits` settings read at check time (Q222). The defaults: at most 10 items and 1,200 characters of item text per reply, an `idea` item at most 120 characters and at most 3 per reply (Q219), a `fact`, `finding`, `correction` or `question` item at most 300, and a `correction`'s `to` within edit distance max(12, 25% of `from`) of its `from`. A missing or invalid setting uses its default | C2-06: each default limit refused one past the boundary and accepted at it. A changed value takes effect on the next reply. Each invalid value in the fallback list uses the default | +| M5 cited-reply guard (5.2) | A reply that isn't a list of short cited items, within the `replyLimits` settings read at check time (Q222). The defaults: at most 10 items and 1,200 characters of item text per reply, an `idea` item at most 120 characters and at most 3 per reply (Q219), a `fact`, `finding`, `correction` or `question` item at most 300, and a `correction`'s `to` within edit distance max(12, 25% of `from`) of its `from`. A missing or invalid setting uses its default | C2-06: each default limit refused one past the boundary and accepted at it. A changed value takes effect on the next reply. Each invalid value in the fallback list uses the default, `true`, `[7]`, `{}` and `"5"` included. With the business file unreadable, an over-limit reply refuses and `messages` is unchanged | | M6 no free-text decision channel | `decision.raise` from a creative definition refuses `decision-not-allowed` in the broker. The typed tool is also absent (M3) | C2-08: a creative session's raise refuses and no decision row appears. An assistant's raise still works | | 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 | 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 through `organization.set`, 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, and so does her `limits.set` change to Bean's `launch.models` key. 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 | -| M11 reply limits are the human's | `library.limits.set` from an agent session (`human-required`) or the tester (`tester-refused`). Any organization write whose `replyLimits` differs from the file's, unless the actor is the file's `human`, refuses `organization-reply-limits-human-only` in `organizationWriter.write` | C2-06: the human's `library.limits.set` works. The chief of staff's `organization.set`, the tester's `instances.set` and a direct `write` with a changed `replyLimits` refuse, and the file and `changes.jsonl` are unchanged. A mutant without the writer check fails | +| M11 reply limits are the human's | `library.limits.set` from an agent session (`human-required`) or the tester (`tester-refused`). Any organization write whose `replyLimits` differs from the on-disk file's refuses `organization-reply-limits-human-only` in `organizationWriter.write`, on `document` and on `written`, unless the actor is the on-disk `human` and not an agent actor shape | C2-06: the human's `library.limits.set` works. The chief of staff's `organization.set`, the tester's `instances.set` and a direct `write` with a changed `replyLimits` refuse. A direct `write` that changes both `human`, to its own actor, and `replyLimits` refuses. With a dotted human id equal to a role's actor, that role's write refuses. `library.limits.set` with an unknown key refuses `invalid-request`. In each refusal the file and `changes.jsonl` are unchanged. A mutant without the writer check fails, and so does one that compares `document.human` | Residual, in numbers. At the defaults one reply can carry up to 3 `idea` items of 120 characters, which is 360 characters of free suggestion text inside a total of 1,200 characters of item text. The numbers