review(bus): darkwing S2b round 2, approve (row 43, #1525)

Verdict comment 26769, queue rev 146. R1, R2 and both lead decision 64
rulings are in; round 1 probes now refuse. 55/55 on Node 24 and 26,
nine of ten mutants killed, the survivor equivalent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-05 17:43:09 -05:00
co-authored by Claude Opus 5.5
parent 4699419b8b
commit bba75b4a4f
9 changed files with 405 additions and 0 deletions
@@ -0,0 +1,26 @@
Row 43, S2b, round 2: **approve** (Darkwing)
Candidate `candidate-manifest.sha256` `a89d64ca…`, three files under
`packages/bus/`. Full record:
`agents/darkwing/work/slice1-s2b-review/review-r2.md`.
The patch applies at `57738083` and the manifest checks 3/3. Tests pass
55/55 on Node 26 and on Node 24, and Rocko's S1 contract script passes.
Lead decision 64 and round 1's items are in:
- R1: `authorize`, `message.send` and `role.revoke` share
`#consumeAuthority`, each inside its own transaction. A second send or
revoke refuses with `decision-consumed`, and so does `authorize` after
either verb.
- Ruling 1: every decision is consumed once, cross-role included.
- Ruling 2 and R2: a class mismatch refuses with `decision-mismatch`.
Rocko tests all six directions, and my round 1 probe C now refuses.
- A within-role sender that names a decision gets every check.
Nine of ten mutants are killed. The survivor removes `authorize`'s
transaction, and with one synchronous writer it's equivalent today.
Not blocking: `message.send` now writes an `action.allowed` event for
every agent message, which shows up in the recipient role's trail. The
README's "within-role actions remain unchanged" is true for `authorize`
but not for that verb. S4 should expect these events.
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
# S2b round 2: Darkwing's mutants, repository layout, each compared to a clean baseline.
import pathlib,tempfile,shutil,subprocess,json,re,sys
cases=[
('drop-consumed-check','broker.mjs',"fail('decision-consumed');",";"),
('gated-only-again','broker.mjs',"result.decision &&\n this.#store.get(","result.decision && this.#decision(s, result.decision).class === 'gated' &&\n this.#store.get("),
('drop-class-match','broker.mjs',"d.class !== cls ||",""),
('within-role-ignores-decision','broker.mjs',"cls === 'within-role' && !decision","cls === 'within-role'"),
('message-send-check-only','broker.mjs',"this.#consumeAuthority(s, 'message.send',","this.#checkAuthority(s, 'message.send',"),
('role-revoke-check-only','broker.mjs',"this.#consumeAuthority(s, 'role.revoke',","this.#checkAuthority(s, 'role.revoke',"),
('is-not-to-ne','broker.mjs',"IS NOT 'decision.raise' LIMIT 1","!= 'decision.raise' LIMIT 1"),
('count-raise-event','broker.mjs',"AND json_extract(body,'$.operation') IS NOT 'decision.raise' LIMIT 1","LIMIT 1"),
('event-drops-decision','broker.mjs',"{ action, ...result, target: context.target ?? null }","{ action, class: result.class, target: context.target ?? null }"),
('authorize-outside-transaction','broker.mjs',"return this.#store.transaction(() => this.#consumeAuthority(s, action, context));","return this.#consumeAuthority(s, action, context);"),
]
source=pathlib.Path(sys.argv[1])
def run(dest):
r=subprocess.run(['node','--test',*[str(p) for p in sorted((dest/'tests').glob('*.test.mjs'))]],capture_output=True,text=True,timeout=300)
tests=set(l[2:].rsplit(' (',1)[0] for l in r.stdout.splitlines() if l.startswith('✖ ') and not l.startswith('✖ failing tests'))
return r.returncode,tests
with tempfile.TemporaryDirectory(prefix='dw-s2b2-mut-') as root:
dest=pathlib.Path(root)/'packages'/'bus';shutil.copytree(source,dest,ignore=shutil.ignore_patterns('dw'))
code,base=run(dest);print(json.dumps({'baseline_exit':code,'baseline_failures':sorted(base)}),flush=True)
code,f=run(dest);print(json.dumps({'mutation':'no-op','exit':code,'killed':bool(f-base)}),flush=True)
out=[]
for name,file,old,new in cases:
p=dest/'src'/file;b=p.read_text();pat=r'\s*'.join(re.escape(c) for c in old if not c.isspace());n=len(re.findall(pat,b))
if n!=1: print(json.dumps({'mutation':name,'error':f'{n} matches'}),flush=True);continue
p.write_text(re.sub(pat,lambda m:new,b,count=1))
try: code,f=run(dest)
finally: p.write_text(b)
x=sorted(f-base);out.append({'mutation':name,'exit':code,'killed':bool(x),'new_failures':x})
print(json.dumps(out[-1]),flush=True)
print('killed',sum(x['killed'] for x in out),'of',len(out))
@@ -0,0 +1,13 @@
{"baseline_exit": 0, "baseline_failures": []}
{"mutation": "no-op", "exit": 0, "killed": false}
{"mutation": "drop-consumed-check", "exit": 1, "killed": true, "new_failures": ["failed commit rolls consumption back; cross-role consumes and within-role stays reusable", "gated approval authorizes once, survives store reopen, and fresh approval works", "message.send consumes approval and prevents a later send or authorize", "role.revoke consumes approval and prevents a later revoke or authorize", "two scheduled callers have exactly one grant and one consumed refusal"]}
{"mutation": "gated-only-again", "exit": 1, "killed": true, "new_failures": ["failed commit rolls consumption back; cross-role consumes and within-role stays reusable"]}
{"mutation": "drop-class-match", "exit": 1, "killed": true, "new_failures": ["class drift cross-role to gated refuses before consumption", "class drift cross-role to within-role refuses before consumption", "class drift gated to cross-role refuses before consumption", "class drift gated to within-role refuses before consumption", "class drift within-role to cross-role refuses before consumption", "class drift within-role to gated refuses before consumption"]}
{"mutation": "within-role-ignores-decision", "exit": 1, "killed": true, "new_failures": ["class drift cross-role to within-role refuses before consumption", "class drift gated to within-role refuses before consumption", "failed commit rolls consumption back; cross-role consumes and within-role stays reusable"]}
{"mutation": "message-send-check-only", "exit": 1, "killed": true, "new_failures": ["message.send consumes approval and prevents a later send or authorize"]}
{"mutation": "role-revoke-check-only", "exit": 1, "killed": true, "new_failures": ["role.revoke consumes approval and prevents a later revoke or authorize"]}
{"mutation": "is-not-to-ne", "exit": 1, "killed": true, "new_failures": ["failed commit rolls consumption back; cross-role consumes and within-role stays reusable", "gated approval authorizes once, survives store reopen, and fresh approval works", "message.send consumes approval and prevents a later send or authorize", "role.revoke consumes approval and prevents a later revoke or authorize", "two scheduled callers have exactly one grant and one consumed refusal"]}
{"mutation": "count-raise-event", "exit": 1, "killed": true, "new_failures": ["another run cannot consume an approval; a failed check leaves it usable", "both arbiters require human resolution when their cross-role route is themselves", "claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic", "decision classes route from policy; gated resolution is human-only, choice and target must match", "failed commit rolls consumption back; cross-role consumes and within-role stays reusable", "gated approval authorizes once, survives store reopen, and fresh approval works", "message.send consumes approval and prevents a later send or authorize", "revocation permanently bars the old run from reclaiming first, including after broker restart", "role.revoke consumes approval and prevents a later revoke or authorize", "task action subjects and linked decision trail are complete and ordered", "two scheduled callers have exactly one grant and one consumed refusal"]}
{"mutation": "event-drops-decision", "exit": 1, "killed": true, "new_failures": ["another run cannot consume an approval; a failed check leaves it usable", "failed commit rolls consumption back; cross-role consumes and within-role stays reusable", "gated approval authorizes once, survives store reopen, and fresh approval works", "message.send consumes approval and prevents a later send or authorize", "role.revoke consumes approval and prevents a later revoke or authorize", "two scheduled callers have exactly one grant and one consumed refusal"]}
{"mutation": "authorize-outside-transaction", "exit": 0, "killed": false, "new_failures": []}
killed 9 of 10
@@ -0,0 +1,64 @@
v24.21.0
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (168.817976ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (313.208626ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (302.696792ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (174.078331ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (166.043648ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (126.024638ms)
✔ task action subjects and linked decision trail are complete and ordered (170.274208ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (81.476104ms)
✔ business isolation includes inherited object names and cross-business message references (150.077621ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (229.844251ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (105.93614ms)
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (176.471326ms)
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (113.396458ms)
✔ both arbiters require human resolution when their cross-role route is themselves (190.95561ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.69599ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (5.619075ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.666542ms)
✔ expiry refuses use and env references never become client data (0.706395ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.409483ms)
✔ opaque tokens shorter than 16 characters refuse before use (0.311777ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.168625ms)
✔ process reader gets own kernel identity without exposing environment values (0.491319ms)
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.663546ms)
✔ real pid 1 remains inspectable when its environment is protected (0.307122ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (182.635495ms)
✔ startup token refusal returns safe code without value or partial listening broker (32.982398ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (233.71422ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (212.036986ms)
✔ trusted host registers later launches; socket clients never have a registration verb (173.561943ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (33.573525ms)
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (142.219081ms)
✔ v3b prototype refusals, views and append-only mutations (1127.795659ms)
✔ gated approval authorizes once, survives store reopen, and fresh approval works (240.5127ms)
✔ another run cannot consume an approval; a failed check leaves it usable (299.636661ms)
✔ two scheduled callers have exactly one grant and one consumed refusal (188.519503ms)
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (304.19416ms)
✔ class drift gated to cross-role refuses before consumption (195.89971ms)
✔ class drift cross-role to gated refuses before consumption (164.954025ms)
✔ class drift gated to within-role refuses before consumption (157.003222ms)
✔ class drift cross-role to within-role refuses before consumption (194.824232ms)
✔ class drift within-role to gated refuses before consumption (162.080633ms)
✔ class drift within-role to cross-role refuses before consumption (169.207993ms)
✔ message.send consumes approval and prevents a later send or authorize (172.590743ms)
✔ role.revoke consumes approval and prevents a later revoke or authorize (216.382204ms)
✔ creates private WAL store and excludes a second writer until explicit close (122.973435ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (197.930322ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (288.655009ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (161.180697ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (110.373629ms)
✔ async transactions refuse before invoking their function (83.296435ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (152.484588ms)
✔ two wire claims serialize; a lost reply never automatically retries (186.003021ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (180.813868ms)
✔ client preserves UTF-8 when a response divides a multibyte character (13.90465ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (155.521905ms)
ℹ tests 55
ℹ suites 0
ℹ pass 55
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2532.657625
@@ -0,0 +1,64 @@
v26.8.1
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (181.226076ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (255.344804ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (251.234875ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (163.150806ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (163.437952ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (148.794539ms)
✔ task action subjects and linked decision trail are complete and ordered (164.145578ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (76.376018ms)
✔ business isolation includes inherited object names and cross-business message references (156.850001ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (236.167698ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (127.909574ms)
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (173.273253ms)
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (115.875126ms)
✔ both arbiters require human resolution when their cross-role route is themselves (184.414743ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.067ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (17.046437ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (3.537917ms)
✔ expiry refuses use and env references never become client data (3.354475ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.227857ms)
✔ opaque tokens shorter than 16 characters refuse before use (0.239052ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (1.548802ms)
✔ process reader gets own kernel identity without exposing environment values (1.028088ms)
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.653886ms)
✔ real pid 1 remains inspectable when its environment is protected (0.281702ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (196.537481ms)
✔ startup token refusal returns safe code without value or partial listening broker (33.473287ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (167.666375ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (166.282042ms)
✔ trusted host registers later launches; socket clients never have a registration verb (162.722497ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (34.143797ms)
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (158.81243ms)
✔ v3b prototype refusals, views and append-only mutations (1032.636254ms)
✔ gated approval authorizes once, survives store reopen, and fresh approval works (262.382417ms)
✔ another run cannot consume an approval; a failed check leaves it usable (218.753871ms)
✔ two scheduled callers have exactly one grant and one consumed refusal (157.69087ms)
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (307.874839ms)
✔ class drift gated to cross-role refuses before consumption (190.598829ms)
✔ class drift cross-role to gated refuses before consumption (192.380263ms)
✔ class drift gated to within-role refuses before consumption (172.649477ms)
✔ class drift cross-role to within-role refuses before consumption (197.388635ms)
✔ class drift within-role to gated refuses before consumption (161.380155ms)
✔ class drift within-role to cross-role refuses before consumption (160.191833ms)
✔ message.send consumes approval and prevents a later send or authorize (178.703218ms)
✔ role.revoke consumes approval and prevents a later revoke or authorize (203.765625ms)
✔ creates private WAL store and excludes a second writer until explicit close (114.437405ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (173.185092ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (181.361153ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (155.732458ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (105.177443ms)
✔ async transactions refuse before invoking their function (77.78015ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (148.359653ms)
✔ two wire claims serialize; a lost reply never automatically retries (166.394156ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (113.075798ms)
✔ client preserves UTF-8 when a response divides a multibyte character (11.775795ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (127.125122ms)
ℹ tests 55
ℹ suites 0
ℹ pass 55
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2483.527408
@@ -0,0 +1,48 @@
// Darkwing S2b round 2 probes. Not part of the candidate.
import test from 'node:test';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Store } from '../src/store.mjs';
import { Broker } from '../src/broker.mjs';
const options = [{ key: 'yes', text: 'Allow' }, { key: 'no', text: 'Decline' }];
const policy = () => ({ demo: { id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'pm' }, roles: {
pm: { authority: { withinRole: ['message.send'], crossRole: [] } },
cto: { authority: { withinRole: ['message.send'], crossRole: [] } },
coder: { authority: { withinRole: ['message.send'], crossRole: ['task.scope.change'] } } } } });
const code = (f) => { try { return JSON.stringify(f()); } catch (e) { return e.code ?? String(e.message); } };
function setup(t) {
const root = mkdtempSync(join(tmpdir(), 'dw-s2b2-'));
const store = new Store(root), b = new Broker({ store, businesses: policy() });
t.after(() => { try { store.close(); } catch {} rmSync(root, { recursive: true, force: true }); });
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const call = (cap, verb, args = {}) => b.request(cap, { verb, args });
const c = b.bindLaunch({ business: 'demo', role: 'coder', run: 'coder-run', harness: 'pi' });
const pm = b.bindLaunch({ business: 'demo', role: 'pm', run: 'pm-run', harness: 'pi' });
const cto = b.bindLaunch({ business: 'demo', role: 'cto', run: 'cto-run', harness: 'pi' });
call(c, 'role.claim'); call(pm, 'role.claim'); call(cto, 'role.claim');
const kinds = () => store.all('SELECT kind, subject FROM events ORDER BY seq').map((e) => e.kind + '@' + (e.subject ?? '-'));
return { b, store, human, call, c, pm, cto, kinds };
}
test('F events written by one within-role message.send with no decision', (t) => {
const o = setup(t), before = o.kinds().length;
o.call(o.c, 'message.send', { to: 'pm', body: 'hi' });
console.log('F new events', JSON.stringify(o.kinds().slice(before)));
console.log('F trail pm', JSON.stringify(o.call(o.cto, 'trail', { subject: 'pm' }).map((e) => e.kind)));
});
test('G within-role sender supplies a decision it does not own or that does not exist', (t) => {
const o = setup(t);
console.log('G unknown decision', code(() => typeof o.call(o.c, 'message.send', { to: 'pm', body: 'x', decision: 'nope' }).id));
const d = o.call(o.pm, 'decision.raise', { action: 'message.send', target: 'cto', question: 'Send?', options, recommendation: 'yes', choice: 'yes', blocking: false });
console.log('G another role decision', code(() => typeof o.call(o.c, 'message.send', { to: 'cto', body: 'x', decision: d.id }).id));
console.log('G owner use', code(() => typeof o.call(o.pm, 'message.send', { to: 'cto', body: 'x', decision: d.id }).id));
console.log('G owner without decision still sends', code(() => typeof o.call(o.pm, 'message.send', { to: 'cto', body: 'y' }).id));
});
test('H refused verb after consumption writes action.refused and does not add a use', (t) => {
const o = setup(t);
const d = o.call(o.c, 'decision.raise', { action: 'task.scope.change', domain: 'technical', target: 't1', question: 'Q', options, recommendation: 'no', blocking: false });
o.call(o.cto, 'decision.resolve', { id: d.id, choice: 'yes' });
console.log('H first', code(() => o.b.authorize(o.c, 'task.scope.change', { decision: d.id, target: 't1' }).class));
console.log('H second', code(() => o.b.authorize(o.c, 'task.scope.change', { decision: d.id, target: 't1' })));
console.log('H allowed uses', o.store.get("SELECT count(*) n FROM events WHERE kind='action.allowed' AND json_extract(body,'$.decision')=? AND json_extract(body,'$.operation') IS NOT 'decision.raise'", d.id).n);
});
@@ -0,0 +1,44 @@
F new events ["action.allowed@pm"]
F trail pm ["session.launched",null,"action.allowed"]
G unknown decision decision-not-found
G another role decision decision-mismatch
G owner use "string"
G owner without decision still sends "string"
H first "cross-role"
H second decision-consumed
H allowed uses 1
✔ F events written by one within-role message.send with no decision (131.747467ms)
✔ G within-role sender supplies a decision it does not own or that does not exist (168.90098ms)
✔ H refused verb after consumption writes action.refused and does not add a use (135.46588ms)
A raise class gated route_to human
A message.send 0 "string"
A message.send 1 decision-consumed
A message.send 2 decision-consumed
A action.allowed uses recorded 1
A authorize after 3 sends decision-consumed
B role.revoke verb {"revoked":true}
B action.allowed uses recorded 1
B authorize role.revoke after the verb decision-consumed
B authorize again decision-consumed
C raise class cross-role route_to cto
C current class is gated; authorize with arbiter approval:
C authorize 0 decision-mismatch
C authorize 1 decision-mismatch
C authorize 2 decision-mismatch
D authorize 0 {"class":"cross-role","decision":"0d79bffd-1410-433a-b8b0-cffd99eff2a9"}
D authorize 1 decision-consumed
D authorize 2 decision-consumed
E second Store writer-locked
✔ A gated message.send approval: reuse through the verb, then authorize (148.983012ms)
✔ B role.revoke verb, then authorize with the same decision (130.871177ms)
✔ C cross-role approval after policy makes the action gated (145.689167ms)
✔ D cross-role approval reuse under unchanged policy (81.394321ms)
✔ E a second Store on the same data root refuses, so writers serialize in one process (35.07945ms)
ℹ tests 8
ℹ suites 0
ℹ pass 8
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 602.088251
@@ -0,0 +1,109 @@
# Row 43, S2b single-use approvals, round 2 review (Darkwing)
Issue #1525. Candidate: Rocko's `candidate-manifest.sha256` (sha256
`a89d64ca68716685d947037946036f54d08d174d8c9c1f02a23e9fadda526991`), three
files under `packages/bus/`. Packet `agents/rocko/work/slice1-s2b-r2/`,
`BUILD.md` sha256 `b2ac4744…`, `build.patch` sha256 `56d572fe…`. Sage's
request is comment 26767, rev 145. Round 1 is `review-r1.md` in this
directory. The rulings are lead decision 64 (57738083).
Verdict: **approve.** R1 and R2 are fixed, and both of decision 64's
rulings are in. I reran every round 1 probe; each one that granted twice
now refuses. Three notes below, none blocking.
## What I checked
- The three packet hashes match Rocko's message. `build.patch` applies at
`57738083`; the manifest checks 3/3.
- I read the whole broker and README diff and the new tests.
- Tests: 55/55 on Node 26.8.1 (`node26-r2.txt`). In `node:24` (24.21.0),
with no network, a non-root UID and the package mounted read-only, also
55/55 (`node24-r2.txt`).
- Rocko's S1 contract script passes its three assertions
(`s1-contract-r2.txt`).
- The decision 62 README note is the same text I reviewed in round 1.
## Decision 64 and the round 1 items
**One helper.** `#consumeAuthority` runs `#checkAuthority`, refuses
`decision-consumed` when a non-raise `action.allowed` event already names
the decision, and writes the new event. `authorize` calls it inside its
transaction. The `role.revoke` and `message.send` verbs call it inside
the request transaction, before their own effect. A later failure in the
verb rolls the consumption back. Rocko tests that with an empty message
body.
**R1, the verbs.** Probe A: a gated `message.send` approval sends once,
then the second and third sends refuse with `decision-consumed`, and so
does `authorize`. Probe B: after the `role.revoke` verb, `authorize`
refuses. Rocko's tests cover both orders, verb then `authorize` and
`authorize` then verb.
**Ruling 1, every decision single-use.** The class condition is gone.
Probe D: a cross-role approval grants once, then refuses. Probe H shows
one consumption event after a refused second use. A within-role decision
passed explicitly is consumed too. Within-role use without a decision
still returns early.
**Ruling 2 and R2, class drift.** `#checkAuthority` refuses with
`decision-mismatch` when `d.class !== cls`. Probe C, a cross-role
approval used after policy makes the action gated, now refuses. Rocko
tests all six directions between the three classes, each with zero
consumption events afterwards.
**Explicit decisions on within-role actions.** The early return now
needs `!decision`. A within-role sender that names a decision gets every
check: an unknown id refuses `decision-not-found`, and another role's
decision refuses `decision-mismatch` (probe G). That fails closed, and
the README says so.
## Mutation evidence
`mutations-r2.py` runs ten mutants against the full test glob in the
repository layout, against a clean baseline (`mutations-r2.txt`). The
no-op isn't killed.
| Mutant | Result |
|---|---|
| drop `fail('decision-consumed')` | killed, 5 tests |
| consume gated decisions only, as in round 1 | killed |
| drop `d.class !== cls` | killed, 6 tests |
| within-role returns early even with a decision | killed, 3 tests |
| `message.send` checks without consuming | killed |
| `role.revoke` checks without consuming | killed |
| `IS NOT` becomes `!=` | killed, 5 tests |
| the query also counts the raise event | killed, 11 tests |
| the consumption event drops `decision` | killed, 6 tests |
| `authorize` without its transaction | survives |
The survivor is equivalent today. `Store.run` wraps a lone insert in its
own transaction, the writer lock allows one `Store` per data root, and
the API is synchronous, so nothing can run between the check and the
insert. The outer transaction starts to matter if a second writer or an
`await` ever appears. `Store.transaction` already refuses async
functions.
## Notes, not blocking
1. `message.send` now writes an `action.allowed` event for every agent
message, including within-role sends with no decision. Its subject
is the recipient role, so the event shows up in that role's `trail`
(probe F). It carries no message body. That's reasonable as evidence,
but the README's "Within-role actions without a decision remain
unchanged" holds for `authorize` and not for this verb. S4 should
expect these events in role trails. A README fix can wait for the
next change to the file.
2. `role.revoke` also writes an `action.allowed` event now, with the
revoked role as subject, before its claim-end row.
3. The events scan has no index, as decision 64 records.
## Files added for round 2
- `review-r2.md`, `comment-r2.md`
- `node26-r2.txt`, `node24-r2.txt`, `s1-contract-r2.txt`
- `probes-r2.test.mjs`: probes F to H
- `probes-r2.txt`: round 1's A to E against this candidate, plus F to H
- `mutations-r2.py`, `mutations-r2.txt`
The probes run from a `dw/` directory beside `packages/bus/src`, with
round 1's `probes-s2b.test.mjs` copied beside them.
@@ -0,0 +1,3 @@
PASS R2 loadBusiness refuses launch.by without within-role role.launch (exit 2)
PASS R2 narrowed launch is null and gated; S2 adapter/broker refuses launch without decision
PASS actual S1 validate/resolve -> S2 normalize/start -> socket claim/message; scoped fixture token callback; launch classification