10 KiB
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 2, 2026-10-22: Webby's d1 review (webby-0122, notes
171480e6e) R1, Q1, Q2 and N1-N2 applied; the changes are listed at the end.
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:
// 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.humangoes on to the remaining checks: references, then kept, then credentials. - The code and message match
creativeHumanOnlyin 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
creativeHumanOnlyitself, because it throws and deleteRefusal returns.
- The code is
actorundefined, null, "tester", or a role actor (<definition>.<instance>) never equalsdocument.human, so those actors are refused.- validateBusinessDocument runs
requireId(document.human)(business.mjs:239), so an undefined actor can't match an undefined human.
- validateBusinessDocument runs
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
#deleteRequestalready passess.humantoinstances.check(broker.mjs:2843). - The launcher pass
lifecycleDeletealready passes the request'sr.by, which is#owner(s)at record time, toinstances.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-refusedat the verb, before any of this (broker.mjs:2595, serve.mjs:394). - That is unchanged.
- The tester is refused
3. The listing: endProtected becomes the per-viewer delete check
read(business)becomesread(business, actor), and runtime.mjs passes the actor.- The broker already passes
"tester"for a tester session ands.humanotherwise (broker.mjs:2627).
- The broker already passes
- Each instance's
endProtectedbecomesprotectedRefusal(document, def, actor) !== null.- This is the same server predicate the delete runs, per the brief.
- The human's read shows
endProtected: falsefor a creative instance. - The tester's read, and any role read, show
true. - The chief of staff shows
truefor 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 fromtruetofalsefor "jason".
- This covers
- 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.isTesterbranch, 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.firekeep 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
<configDir>/personas/<business>/<instance>.mdis 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 (R1): with bean the only name in launch.instances, the human's delete of bean gets the last-listed BusinessError from deletedDocument (:64-66), not the creative code, so it passes protectedRefusal and reaches the later checks. A creative instance can't be in launch.keep, launch.by or the arbiters (business.mjs:208, :181-183, :251-255), so those aren't used. | |
T5: read(business, human) shows endProtected: false for bean; read(business, "tester") and read(business) with no actor show true; the chief of staff shows true for every actor. |
|
| 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 (Q2: its runtime() is a real startBroker with the business writer) | 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 is refused human-required (broker.mjs:2585) and the tester tester-refused (:2595), both 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 (all must be killed)
- M1: protectedRefusal's creative branch returns
nullfor every actor, so a non-human deletes a creative instance. Killed by T2 only (N1): through the bus no non-human actor reaches the delete, since the chief and the tester are refused at the verb. - 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. - M4 (N2): the actor isn't threaded: runtime.mjs:283 calls
checkDelete(id, args.instance)without it. Killed by T6, the human's real-writer delete. A second variant, set op delete not passing the actor to planDelete, is killed by T1 and by T6's launcher pass.
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 four mutants above (M4 in both variants), each on a green base, restored and blob-checked (the runb8 shape).
Comments rewritten (Q1)
endProtectedkeeps its name: agents.js:46 is its only reader, and it means "don't offer Delete".- instance-settings.mjs:28-31 loses "no one deletes one yet". It says the human deletes a creative instance and any other actor is refused, and that the listing's endProtected is protectedRefusal for the reader.
- agents.js:45 says the same, naming row 256.
Changes from draft 1 (webby-0122)
- R1: T4 uses the last-listed refusal instead of launch.keep, which the validator refuses for a creative instance.
- Q1: keep the name; comments rewritten; T5 pins read(business) with no actor as true.
- Q2: the bus test goes in creative.test.mjs.
- N1: M1 is killed by T2 only; T6 pins the chief's
human-required. - N2: M4 added for the threading.