187 lines
10 KiB
Markdown
187 lines
10 KiB
Markdown
# 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`:
|
|
|
|
```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 (`<definition>.<instance>`) 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
|
|
|
|
- `<configDir>/personas/<business>/<instance>.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 (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 `null` for 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)
|
|
|
|
- `endProtected` keeps 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.
|