gosalyn: Case 2 Q222 round 2 for Webby webby-0144: the M11 fence compares against the on-disk human and refuses agent actor shapes, checks document and written as M9 does, a failed business file read checks at the defaults and never skips, the fallback reads keys by Object.hasOwn and Number.isInteger with booleans, arrays and objects named, the verb refuses unknown keys, the ceilings residual line, and cites at aca16404c (R1-R3, N1-N3, N5, N6)

This commit is contained in:
jason.woltje committed 2026-10-22 11:23:57 -05:00
1 parent df29550c9a
commit f164dcef8d
1 file changed
+44 -17
+44 -17
View File
@@ -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 <json>`, 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 `<definition>.<role>` 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