From c3e0bd9db6730f97ee8942cd793868a78beddf38 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 22 Oct 2026 03:46:48 -0500 Subject: [PATCH] honker: row 256 (#1761) design draft 1: one protectedRefusal predicate for delete and the listing's per-viewer endProtected, actor threaded through planDelete/checkDelete/instances.check, fire unchanged, persona file left in place --- agents/honker/work/queue-256/DESIGN.md | 170 +++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 agents/honker/work/queue-256/DESIGN.md diff --git a/agents/honker/work/queue-256/DESIGN.md b/agents/honker/work/queue-256/DESIGN.md new file mode 100644 index 000000000..6c6610b8b --- /dev/null +++ b/agents/honker/work/queue-256/DESIGN.md @@ -0,0 +1,170 @@ +# Row 256 (#1761) design: the human deletes a creative instance + +Brief: docs/plans/2026-10-21_creative-delete-human.md (REQ-COS-3). Base be2b008cc (row 194 landed as +fd6dd327c). Builder Honker, reviewer Webby. Draft 1, 2026-10-22. + +## What changes + +### 1. One predicate in instance-settings.mjs + +Today `deleteRefusal(document, rolesDir, instance)` refuses every END_PROTECTED definition with +`organization-definition-refused`, and read()'s `endProtected` field is the same check +(`END_PROTECTED.includes(definition)`). + +New, in `packages/business/src/instance-settings.mjs`: + +```js +// Row 256 (#1761, REQ-COS-3): the protected-definition part of a delete. A creative instance is +// the human's to delete; any other actor is refused. A non-creative END_PROTECTED definition (the +// chief of staff) is refused for every actor, the human included. +function protectedRefusal(document, definition, actor) { + if (!END_PROTECTED.includes(definition)) return null; + if (!CREATIVE_DEFINITIONS.includes(definition)) return { code: "organization-definition-refused" }; + return actor === document.human ? null : { code: "organization-creative-human-only" }; +} +export function deleteRefusal(document, rolesDir, instance, actor) { + ...unknown-instance unchanged... + const refusal = protectedRefusal(document, entry.definition, actor); + if (refusal) return refusal; + ...references, kept, credentials unchanged... +} +``` + +- A creative definition with `actor === document.human` goes on to the remaining checks: + references, then kept, then credentials. +- The code and message match `creativeHumanOnly` in launch-settings.mjs:13. + - The code is `organization-creative-human-only`. + - It is raised as a plain refusal code here, as deleteRefusal's other codes are. + - I don't call `creativeHumanOnly` itself, because it throws and deleteRefusal returns. +- `actor` undefined, null, "tester", or a role actor (`.`) never equals + `document.human`, so those actors are refused. + - validateBusinessDocument runs `requireId(document.human)` (business.mjs:239), so an undefined + actor can't match an undefined human. + +### 2. The actor reaches every delete path + +| Path | Today | Change | +| --- | --- | --- | +| `planDelete(document, instance)` | no actor | `planDelete(document, instance, actor)` | +| `set(business, args, actor)` op delete (:207) | has the actor, doesn't pass it | passes it to planDelete | +| `checkDelete(business, instance)` | no actor | `checkDelete(business, instance, actor)` | +| runtime.mjs:283 `instances.check` | `checkDelete(id, args.instance)` | `checkDelete(id, args.instance, actor)` | + +- The broker's verb check `#deleteRequest` already passes `s.human` to `instances.check` (broker.mjs:2843). +- The launcher pass `lifecycleDelete` already passes the request's `r.by`, which is + `#owner(s)` at record time, to `instances.set op delete`. +- So the broker gets no logic change. Only the deferred comment at :2825 is touched, if it names + the old rule. +- The CLI (`mosaic instances delete`) and the Console both reach the same bus verb with the human's + session. + - The tester is refused `tester-refused` at the verb, before any of this (broker.mjs:2595, + serve.mjs:394). + - That is unchanged. + +### 3. The listing: `endProtected` becomes the per-viewer delete check + +- `read(business)` becomes `read(business, actor)`, and runtime.mjs passes the actor. + - The broker already passes `"tester"` for a tester session and `s.human` otherwise (broker.mjs:2627). +- Each instance's `endProtected` becomes `protectedRefusal(document, def, actor) !== null`. + - This is the same server predicate the delete runs, per the brief. + - The human's read shows `endProtected: false` for a creative instance. + - The tester's read, and any role read, show `true`. + - The chief of staff shows `true` for everyone. +- The listing that a write returns is read with the writer's actor. + - This covers `commit`'s `{ ...read(business), ... }` at :405 and fire's at :338. + - So the human's add of bean returns `endProtected: false`, and the existing :288 assertion + changes from `true` to `false` for "jason". +- I keep the field name, so the listing gains no key and the deepEqual shapes in + instance-settings.test, lifecycle-settings.test and agents-browser.test keep their keys. + - Only the value for a creative entry read by the human changes. + - The cost is a name that now means "delete refused for its protected definition, for this + viewer". + - The read() comment says so. + - A rename (for example `deletable`) touches every listing fixture; I'd rather not, but I'll take + Webby's call. +- agents.js:46 is unchanged in logic: `if (!e.endProtected)` offers Delete. + - Its comment changes to name row 256. + - Delete stays inside the existing `!c.isTester` branch, so the tester page offers none whatever + the field says. + - The tester has two guards: the server's `true`, and the page's tester branch. + +### 4. Fire is unchanged + +- `fireChecks` / `checkFire` / `instance.fire` / `role.fire` keep refusing END_PROTECTED for + every actor. +- protectedRefusal is not called from fire. +- A test pins that the human's fire of a creative instance still refuses. + +### 5. Persona file + +- `/personas//.md` is left in place on delete. + - That is the brief's smallest choice. +- The delete commits the business file and renames the workspace under `.retired`, as today. + - The persona file isn't under either path, and nothing reads it once the entry is gone. + - bundle.mjs reads it only for a launched creative instance. +- Retired names are never reused (row 194 D1), so a later hire can't pick up the old persona. +- This goes in BUILD.md, plus a DEFERRED line if Sage wants a cleanup later. +- No new file open, so no O_NOFOLLOW test. + +### 6. DEFERRED.md + +The "Creative instances: the human can't delete one" entry (DEFERRED.md:387) gets a closing line: + +> Closed by row 256 (#1761) for delete. Fire still refuses, by REQ-COS-3. + +The entry says "the row covers fire and delete together". The brief keeps fire unchanged, so the +closing line says the fire half is closed by ruling, not by code. + +## Tests + +| File | Test | +| --- | --- | +| business/tests/instance-settings.test.mjs | T1: the human deletes a creative instance (bean); the file loses the entry. | +| | T2: the same delete by a role actor (`chief-of-staff.chief`), by "tester" and by no actor refuses `organization-creative-human-only`, and the file is unchanged. | +| | T3: the human's delete of the chief of staff refuses `organization-definition-refused`. | +| | T4: a creative instance in launch.keep refuses `organization-instance-kept` for the human, so it reaches the later checks. | +| | T5: read(business, human) shows `endProtected: false` for bean; read(business, "tester") and read(business) show `true`; the chief of staff shows `true` for both. | +| | The existing :283-292 test is updated where it pinned the old refusal. | +| business/tests/lifecycle-settings.test.mjs | checkDelete with the human passes for a creative instance and with another actor refuses. | +| | The fire of a creative instance by the human still refuses (fire unchanged). | +| bus/tests/creative.test.mjs (or instances-verb.test.mjs, wherever the creative fixture lives) | T6: the human's `instances.set op delete` on bean records a lifecycle.delete request. | +| | Taking it and running the launcher pass commits the delete (the r.by path). | +| | The chief's tool and the tester are refused, with the tester refused `tester-refused` at the verb. | +| | The human's `instances` read has `endProtected: false` for bean; the tester's has `true`. | +| webui/tests/agents-browser.test.mjs | T7: the human page with bean at `endProtected: false` offers Delete for bean. | +| | T8: the tester page with the same listing offers no Delete. | +| | The existing sage-2164 assertion moves to the tester-shaped listing (bean `endProtected: true`). | + +- The browser tests stub `/api/instances`, so they pin the page's use of the field. +- The server side of "never on the tester listener" is T5/T6. +- No test touches live paths: fixtures under mktemp in /mnt/storage/scratch, as today. + +## Mutants (both must be killed) + +- M1: protectedRefusal's creative branch returns `null` for every actor, so a non-human deletes a + creative instance. Killed by T2 and T6. +- M2: protectedRefusal checks the human before the creative check, so + `actor === document.human ? null : ...` for every END_PROTECTED definition. The human then + deletes the chief of staff. Killed by T3. +- M3: read() keeps the old `END_PROTECTED.includes`, so the listing isn't the per-viewer predicate. + Killed by T5. + +## Boundaries kept + +- No change to M10 add/change, role.hire, fire, veto or launch.keep. +- No schema change and no migration. +- No new verb. +- No change to the tester gate. + +## Gate (on Sage's CLEAR) + +- Touched test files, each whole. +- The business, bus, cli and webui suites. +- Every scripts/test-*.sh. +- The three mutants above, each on a green base, restored and blob-checked (the runb8 shape). + +## Open for Webby + +1. Keep `endProtected` with a per-viewer value, or rename it. +2. Whether the bus test belongs in creative.test.mjs (the existing creative-human-only fixture) or + instances-verb.test.mjs.