docs(review): row 37 S2 round 1, changes (darkwing)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-04 23:13:35 -05:00
co-authored by Claude Opus 5.5
parent dc0c41c426
commit 79699c143b
17 changed files with 571 additions and 0 deletions
@@ -0,0 +1,8 @@
# alacritty terminal ancestry, 2026-10-04
8357 uid=1000 environ=readable alacritty
4295 uid=1000 environ=readable Hyprland --watchdog-fd 4
4152 uid=1000 environ=readable /usr/bin/start-hyprland
4136 uid=0 environ=DENIED /usr/lib/plasmalogin-helper --socket /tmp/plasmalogin-auth-2
2173 uid=0 environ=DENIED /usr/bin/plasmalogin
# systemd user manager
1274 uid=1000 environ=DENIED /usr/lib/systemd/systemd --user --deserialize=8
@@ -0,0 +1,33 @@
Row 37, S2, round 1: **changes** (Darkwing)
Candidate `candidate-manifest.sha256` `dbfd4a07…`, 24 files under
`packages/bus/`. Full record:
`agents/darkwing/work/slice1-s2-review/review-r1.md`.
What holds on my machine: the patch applies at `fef4b362` and adds only
`packages/bus/`; the manifest checks 24/24; the schema is byte-identical to
v3b; the tests pass 36/36 on Node 26 and on Node 24; the S1 contract check
passes against S1 round 2; and all 11 mutants are killed against a clean
baseline.
Required:
- **R1.** The human proof refuses every real terminal here. It refuses
when it can't read an ancestor's environ, and Jason's terminals descend
from root-owned `plasmalogin-helper`. `systemd --user` and `sshd` have
the same problem. Skip only the marker check for such an ancestor, and
test with real `/proc` (pid 1).
- **R2.** A revoked holder can claim its role again with its old
capability, before the replacement. `role.claim` should refuse a run
with a `revoke` row.
- **R3.** An arbiter resolves its own cross-role decision: cto with
`task.scope.change` via `domain: technical`, and pm with
`task.priority.change`. Route to `human` when the arbiter is the raiser.
- **R4.** A refused `bindLaunch` over IPC exits the broker with 2. Reply
and keep running.
- **R5.** `mutations.py` runs on a flattened copy where two tests fail
before any mutation, so `killed` is always true. Keep the repository
layout and compare against the baseline.
Eight notes, none blocking. Two of them: one approval authorizes the same
action without limit (a lead question for Sage), and the per-commit
timestamp scan costs 116 ms at 100k events.
@@ -0,0 +1,85 @@
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (327.283488ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (352.419355ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (334.415458ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (225.908515ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (168.374155ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (149.553744ms)
✔ task action subjects and linked decision trail are complete and ordered (120.891844ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (68.677116ms)
✔ business isolation includes inherited object names and cross-business message references (98.2943ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (187.772264ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (93.163536ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.499889ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.237347ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (32.669675ms)
✔ expiry refuses use and env references never become client data (1.225229ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (2.979725ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.848627ms)
✔ process reader gets own kernel identity without exposing environment values (0.898907ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (343.988351ms)
✖ startup token refusal returns safe code without value or partial listening broker (62.495699ms)
✖ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (60.149224ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (251.798699ms)
✔ trusted host registers later launches; socket clients never have a registration verb (250.627344ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (60.119362ms)
✔ v3b prototype refusals, views and append-only mutations (1437.059795ms)
✔ creates private WAL store and excludes a second writer until explicit close (274.812312ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (199.395272ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (258.703697ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (239.366608ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (125.181636ms)
✔ async transactions refuse before invoking their function (113.038006ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (328.571433ms)
✔ two wire claims serialize; a lost reply never automatically retries (232.268638ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (163.54825ms)
✔ client preserves UTF-8 when a response divides a multibyte character (13.212496ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (167.701574ms)
ℹ tests 36
ℹ suites 0
ℹ pass 34
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2231.263241
✖ failing tests:
test at tests/process.test.mjs:67:1
✖ startup token refusal returns safe code without value or partial listening broker (62.495699ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ 'credential-location'
- 'credential-file'
^
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/tmp/tmp.K2k73RU0Vy/bus/tests/process.test.mjs:81:10)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'credential-location',
expected: 'credential-file',
operator: 'strictEqual',
diff: 'simple'
}
test at tests/process.test.mjs:86:1
✖ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (60.149224ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
false !== true
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/tmp/tmp.K2k73RU0Vy/bus/tests/process.test.mjs:102:10)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,19 @@
// The human CLI started as a direct child of an agent run is refused.
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { spawn, execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { startBroker } from '../src/runtime.mjs';
const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'cto' }, roles: { cto: { authority: { withinRole: ['message.send'], crossRole: [] } } } } };
const root = mkdtempSync(join(tmpdir(), 'dw-human-')); mkdirSync(join(root, 'data'));
const rt = await startBroker({ dataRoot: join(root, 'data'), businesses });
const cli = fileURLToPath(new URL('../src/human-cli.mjs', import.meta.url));
const req = JSON.stringify({ business: 'demo', verb: 'launch.revoke' });
try {
const child = spawn(process.execPath, [cli, rt.path]); let text = '';
child.stdout.on('data', (b) => (text += b)); child.stderr.on('data', (b) => (text += b)); child.stdin.end(req);
const status = await new Promise((r) => child.on('close', r));
console.log('direct child of this agent run:', status, text.trim());
} catch (e) { console.log('error', e.code ?? e.message); }
finally { await rt.close(); rmSync(root, { recursive: true, force: true }); }
@@ -0,0 +1 @@
direct child of this agent run: 2 human-required
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
# Rocko's 11 cases, run in the repository layout, each compared against an unmutated baseline.
import pathlib,tempfile,shutil,subprocess,json,re,sys,importlib.util
spec=importlib.util.spec_from_file_location('m',sys.argv[1]);src=open(sys.argv[1]).read()
cases=eval(src[src.index('cases=')+6:src.index(']\nresults')+1])
source=pathlib.Path(sys.argv[2])
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=120)
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-bus-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)}))
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());assert len(re.findall(pat,b))==1,name
p.write_text(re.sub(pat,lambda m:new,b,count=1))
try: code,f=run(dest)
finally: p.write_text(b)
extra=sorted(f-base);out.append({'mutation':name,'exit':code,'killed':bool(extra),'new_failures':extra})
print(json.dumps(out[-1]))
print('killed',sum(x['killed'] for x in out),'of',len(out))
@@ -0,0 +1,13 @@
{"baseline_exit": 0, "baseline_failures": []}
{"mutation": "holder-check", "exit": 1, "killed": true, "new_failures": ["claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic", "launch identity is stamped, payload identity is refused and stale holder cannot send"]}
{"mutation": "human-resolution", "exit": 1, "killed": true, "new_failures": ["decision classes route from policy; gated resolution is human-only, choice and target must match", "launch events require a human CLI capability; generic emit cannot forge authority events"]}
{"mutation": "decision-action", "exit": 1, "killed": true, "new_failures": ["authority never transfers between action, run, target, unresolved or replaced role holder"]}
{"mutation": "decision-target", "exit": 1, "killed": true, "new_failures": ["decision classes route from policy; gated resolution is human-only, choice and target must match"]}
{"mutation": "decision-choice", "exit": 1, "killed": true, "new_failures": ["authority never transfers between action, run, target, unresolved or replaced role holder", "decision classes route from policy; gated resolution is human-only, choice and target must match"]}
{"mutation": "reader-write", "exit": 1, "killed": true, "new_failures": ["observer capabilities read human inbox but cannot mutate or forge launch identity"]}
{"mutation": "payload-secret", "exit": 1, "killed": true, "new_failures": ["loaded fixture token is absent from socket replies and SQLite, including refusal evidence"]}
{"mutation": "schema-open", "exit": 1, "killed": true, "new_failures": ["rollback is atomic and schema metadata is checked against trusted DDL, not just itself"]}
{"mutation": "task-current", "exit": 1, "killed": true, "new_failures": ["task projection uses schema current view, skipping earlier and equal-start polls"]}
{"mutation": "timestamp-commit", "exit": 1, "killed": true, "new_failures": ["writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers"]}
{"mutation": "human-ancestry", "exit": 1, "killed": true, "new_failures": ["human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse"]}
killed 11 of 11
@@ -0,0 +1,45 @@
v24.21.0
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (245.06358ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (1142.141856ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (2394.791078ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (1075.893721ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (871.842456ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (513.357109ms)
✔ task action subjects and linked decision trail are complete and ordered (1120.610371ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (707.229775ms)
✔ business isolation includes inherited object names and cross-business message references (713.088638ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (966.612104ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (449.349586ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (5.421455ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (12.385838ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (4.761414ms)
✔ expiry refuses use and env references never become client data (1.151008ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (2.879108ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.611938ms)
✔ process reader gets own kernel identity without exposing environment values (0.778583ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (259.158665ms)
✔ startup token refusal returns safe code without value or partial listening broker (51.170183ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (857.29398ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (940.56401ms)
✔ trusted host registers later launches; socket clients never have a registration verb (1251.441474ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (64.802777ms)
✔ v3b prototype refusals, views and append-only mutations (5692.249049ms)
✔ creates private WAL store and excludes a second writer until explicit close (173.827041ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (449.7513ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (1532.33713ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (1265.555561ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (664.51824ms)
✔ async transactions refuse before invoking their function (502.005626ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (216.996179ms)
✔ two wire claims serialize; a lost reply never automatically retries (455.787446ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (966.353921ms)
✔ client preserves UTF-8 when a response divides a multibyte character (33.714587ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (1509.265511ms)
ℹ tests 36
ℹ suites 0
ℹ pass 36
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 10356.911701
@@ -0,0 +1,45 @@
v26.8.1
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (1299.252328ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (1784.874653ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (1761.468106ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (300.172157ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (1225.044933ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (122.399485ms)
✔ task action subjects and linked decision trail are complete and ordered (110.240069ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (60.441721ms)
✔ business isolation includes inherited object names and cross-business message references (157.572812ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (209.508135ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (331.218276ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (4.086579ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (24.227391ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (61.833018ms)
✔ expiry refuses use and env references never become client data (4.965603ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (19.912455ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.929166ms)
✔ process reader gets own kernel identity without exposing environment values (0.852077ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (1234.027941ms)
✔ startup token refusal returns safe code without value or partial listening broker (46.596127ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (1100.730149ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (725.201808ms)
✔ trusted host registers later launches; socket clients never have a registration verb (1003.357659ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (60.32702ms)
✔ v3b prototype refusals, views and append-only mutations (6351.025319ms)
✔ creates private WAL store and excludes a second writer until explicit close (1095.939265ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (1097.529651ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (1092.896429ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (985.459125ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (536.247869ms)
✔ async transactions refuse before invoking their function (154.406673ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (1262.225842ms)
✔ two wire claims serialize; a lost reply never automatically retries (1374.708433ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (575.311861ms)
✔ client preserves UTF-8 when a response divides a multibyte character (12.372236ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (934.564269ms)
ℹ tests 36
ℹ suites 0
ℹ pass 36
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 7446.300331
@@ -0,0 +1,85 @@
// Darkwing review probes. Not part of the candidate.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fork } from 'node:child_process';
import { once } from 'node:events';
import { Store } from '../src/store.mjs';
import { Broker } from '../src/broker.mjs';
const options = [{ key: 'yes', text: 'Allow' }, { key: 'no', text: 'Decline' }];
// Data-model table: cto crossRole task.scope.change, pm crossRole task.priority.change.
const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'pm' }, roles: {
pm: { authority: { withinRole: ['message.send', 'task.create'], crossRole: ['task.priority.change'] } },
cto: { authority: { withinRole: ['message.send'], crossRole: ['task.scope.change'] } },
coder: { authority: { withinRole: ['message.send'], crossRole: ['task.scope.change'] } } } } };
function setup(t) {
const root = mkdtempSync(join(tmpdir(), 'dw-bus-'));
const store = new Store(root);
const b = new Broker({ store, businesses });
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const agent = (role, run = role + '-run') => b.bindLaunch({ business: 'demo', role, run, harness: 'pi', address: run });
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
return { b, store, agent, human };
}
const call = (b, cap, verb, args = {}) => b.request(cap, { verb, args });
const raise = (b, cap, action, extra = {}) => call(b, cap, 'decision.raise', { action, question: 'Allow?', options, recommendation: 'no', blocking: false, ...extra });
test('P1 revoked run reclaims its role with its old capability', (t) => {
const { b, agent, human } = setup(t), c = agent('coder'), p = agent('pm');
call(b, c, 'role.claim'); call(b, p, 'role.claim');
const d = raise(b, c, 'role.revoke', { target: 'pm' });
call(b, human, 'decision.resolve', { id: d.id, choice: 'yes' });
call(b, c, 'role.revoke', { role: 'pm', decision: d.id });
let out; try { out = call(b, p, 'role.claim'); } catch (e) { out = e.code; }
console.log('P1 revoked pm reclaims ->', JSON.stringify(out));
const p2 = agent('pm', 'pm-next');
let out2; try { out2 = call(b, p2, 'role.claim'); } catch (e) { out2 = e.code; }
console.log('P1 replacement pm claim ->', JSON.stringify(out2));
});
test('P2 arbiter resolves its own cross-role decision', (t) => {
const { b, agent } = setup(t), cto = agent('cto'), pm = agent('pm');
call(b, cto, 'role.claim'); call(b, pm, 'role.claim');
const d = raise(b, cto, 'task.scope.change', { domain: 'technical', target: 'x1' });
console.log('P2 cto raise route_to', d.route_to);
call(b, cto, 'decision.resolve', { id: d.id, choice: 'yes' });
console.log('P2 cto authorize ->', JSON.stringify(b.authorize(cto, 'task.scope.change', { decision: d.id, target: 'x1' })));
const d2 = raise(b, pm, 'task.priority.change', { target: 'x2' });
console.log('P2 pm raise route_to', d2.route_to);
call(b, pm, 'decision.resolve', { id: d2.id, choice: 'yes' });
console.log('P2 pm authorize ->', JSON.stringify(b.authorize(pm, 'task.priority.change', { decision: d2.id, target: 'x2' })));
});
test('P3 one approval authorizes repeatedly', (t) => {
const { b, agent, human } = setup(t), c = agent('coder');
call(b, c, 'role.claim');
const d = raise(b, c, 'deploy', { target: 'v1' });
call(b, human, 'decision.resolve', { id: d.id, choice: 'yes' });
for (let i = 0; i < 3; i++) console.log('P3 authorize', i, JSON.stringify(b.authorize(c, 'deploy', { decision: d.id, target: 'v1' })));
});
test('P4 refusals from a reader and a non-holder append events', (t) => {
const { b, store, agent } = setup(t), r = b.bindReader({ business: 'demo' }), c = agent('coder');
const before = store.get('SELECT count(*) n FROM events').n;
for (let i = 0; i < 5; i++) { try { call(b, r, 'role.claim'); } catch {} try { call(b, c, 'message.send', { to: 'pm', body: 'x' }); } catch {} }
console.log('P4 events added by 10 refusals:', store.get('SELECT count(*) n FROM events').n - before);
});
test('P5 empty in_reply_to', (t) => {
const { b, store, agent } = setup(t), c = agent('coder');
call(b, c, 'role.claim');
let out; try { out = call(b, c, 'message.send', { to: 'pm', body: 'x', in_reply_to: '' }); out = store.get('SELECT in_reply_to FROM messages WHERE id=?', out.id); } catch (e) { out = e.code; }
console.log('P5 ->', JSON.stringify(out));
});
test('P6 a refused bindLaunch over IPC stops the broker process', async (t) => {
const root = mkdtempSync(join(tmpdir(), 'dw-bus-p6-'));
const child = fork(new URL('../src/process.mjs', import.meta.url), [], { stdio: ['ignore', 'pipe', 'pipe', 'ipc'] });
t.after(() => { if (child.exitCode === null) child.kill('SIGKILL'); rmSync(root, { recursive: true, force: true }); });
let m = once(child, 'message');
child.send({ op: 'boot', config: { dataRoot: root, businesses, launches: [] } });
console.log('P6 boot', (await m)[0].ok);
const rec = { business: 'demo', role: 'cto', run: 'r1', harness: 'pi', pid: process.pid, startTime: '1' };
m = once(child, 'message'); child.send({ op: 'bindLaunch', record: rec }); console.log('P6 first bind', (await m)[0].ok);
const exit = once(child, 'exit');
m = once(child, 'message'); child.send({ op: 'bindLaunch', record: rec }); console.log('P6 duplicate bind', JSON.stringify((await m)[0]));
const timer = setTimeout(() => child.kill('SIGKILL'), 3000);
console.log('P6 exit code after refused bind', (await exit)[0]); clearTimeout(timer);
});
@@ -0,0 +1,29 @@
P1 revoked pm reclaims -> {"role":"pm","run":"pm-run"}
P1 replacement pm claim -> "role-already-held"
P2 cto raise route_to cto
P2 cto authorize -> {"class":"cross-role","decision":"3d7d663d-9e2a-4a89-8c30-8bcc8a9e823c"}
P2 pm raise route_to pm
P2 pm authorize -> {"class":"cross-role","decision":"b4b8950a-2960-45d3-9b7c-ecd6a3e5889f"}
P3 authorize 0 {"class":"gated","decision":"a5cc81c6-9708-4916-980f-53925572d8f5"}
P3 authorize 1 {"class":"gated","decision":"a5cc81c6-9708-4916-980f-53925572d8f5"}
P3 authorize 2 {"class":"gated","decision":"a5cc81c6-9708-4916-980f-53925572d8f5"}
P4 events added by 10 refusals: 10
P5 -> "storage-refused"
✔ P1 revoked run reclaims its role with its old capability (99.988778ms)
✔ P2 arbiter resolves its own cross-role decision (91.589345ms)
✔ P3 one approval authorizes repeatedly (76.263444ms)
✔ P4 refusals from a reader and a non-holder append events (99.06011ms)
✔ P5 empty in_reply_to (49.308542ms)
P6 boot true
P6 first bind true
P6 duplicate bind {"ok":false,"error":"duplicate-run"}
P6 exit code after refused bind 2
✔ P6 a refused bindLaunch over IPC stops the broker process (121.706676ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 609.863753
@@ -0,0 +1,151 @@
# Row 37, S2 bus and broker core, round 1 review (Darkwing)
Issue #1519. Candidate: Rocko's `candidate-manifest.sha256` (sha256
`dbfd4a074601cf49b01abcae88eef15a2b30c28e6992bd7eb559d0c6c2dc6b6e`), the
24 files under `packages/bus/`. Packet `agents/rocko/work/slice1-s2/`,
`BUILD.md` sha256 `35ec7d92…`, `build.patch` sha256 `c2c75d71…`.
Verdict: **changes.** The store, the transport and the authority checks
are careful work, and most of what Rocko claims holds up on my machine.
Three defects in the broker's own rules need fixing first, and so does the
human proof. On this machine the proof refuses Jason from every real
terminal. One piece of packet evidence also needs redoing.
## What I checked and what holds
- `build.patch` applies to a fresh clone at `fef4b362` and adds only
`packages/bus/`. The manifest checks 24/24.
- `schema.sql` is byte-identical to `slice1-proto/schema-v3b.sql`
(`179ffe35…`).
- `node --test packages/bus/tests/*.test.mjs`: 36/36 on Node 26.8.1
(`node26.txt`). In `node:24` (24.21.0), with no network, a non-root
UID and the package mounted read-only in the repository layout, also
36/36 (`node24.txt`).
- Rocko's `s1-contract-check.mjs` passes against S1 round 2
(`build-r2.patch`, manifest 34/34) plus this candidate
(`s1r2-contract.txt`). Round 2's narrowing doesn't break S2.
- Rocko's 11 mutants, rerun in the repository layout against a clean
baseline: each one fails at least one test the baseline passes, so 11
of 11 are killed (`mutations-dw.txt`). The packet's own run doesn't show
that; see R5.
- From inside my agent run, `human-cli.mjs` as a direct child is refused
with `human-required` (`human-escape.txt`). The reader capability
can't mutate, the client never retries, and a dropped reply reports
`outcome-unknown`. The tests show all three, and I read the code paths.
## Required
**R1. The human proof refuses every real terminal on this machine.**
`readProcess` reads `/proc/<pid>/environ` for every ancestor and refuses
when it can't. On Jason's desktop each alacritty window descends from
`plasmalogin-helper` and `plasmalogin`, both owned by root, and a
non-root user can't read their environ. `systemd --user` is the same: it
runs as UID 1000, but its environ is unreadable too, and it adopts any
detached or `systemd-run` process. `terminal-walk.mjs` reads the real
ancestry of terminal 8357 and gives the CLI hop only as a fixture. It
ends `human-required` (`terminal-walk.txt`, `ancestry.txt`). SSH logins
hit the same wall at `sshd`. The tests missed this because every
positive case uses a synthetic ancestry.
Fix: an ancestor whose environ can't be read (EACCES) shouldn't refuse.
Its `stat` and `cmdline` can still be read, so keep the command check and
the launch-registry check on it, and skip only the marker check. The CLI
process itself still needs a readable environ for the nonce. Test it on
real `/proc` with pid 1 (environ unreadable to a non-root test), plus a
synthetic chain whose top two ancestors throw EACCES. The agent-side
refusals hold as they are, since a managed run is a same-UID dumpable
process. This doesn't weaken the cooperative boundary README already
states.
**R2. A revoked holder can take its role back.** After a gated revoke the
revoked run's capability still works for `role.claim`. In `probes.txt`
P1, `pm-run` is revoked, claims `pm` again and gets it, and the
replacement launch then gets `role-already-held`. A revoke that Jason
approved is undone by the stuck session waking up. The existing revoke
test claims the replacement first, so it never sees this.
Fix: `role.claim` refuses a run that has a `revoke` row for that business
and role. That survives a restart, which dropping capabilities wouldn't.
Test the revoked run claiming before the replacement does.
**R3. An arbiter approves its own cross-role decision.** The raiser
chooses `domain`, and `route_to` is that domain's arbiter, even when the
arbiter is the raiser. With the data model's own table (cto crossRole
`task.scope.change`, technical arbiter cto; pm crossRole
`task.priority.change`, delivery arbiter pm), each role raises, resolves
and authorizes its own cross-role action (P2). Cross-role turns into
within-role for both arbiters. The data model I wrote didn't cover this,
so the gap started with me, not Rocko.
Fix: when the route equals the raising role, route to `human`. That
fails closed, and Sage can pick the other arbiter instead if they'd
rather; either way, add a test for each arbiter.
**R4. One refused `bindLaunch` stops the broker.** In `process.mjs` every
caught error replies and then calls `close(2)`. A duplicate run over IPC
gets `{ok:false,error:'duplicate-run'}` and the broker exits 2 (P6).
Every agent's capability goes with it. S6 binds launches over this
channel, so a retried launch would take the bus down. Fix: a refused
`bindLaunch` replies and keeps running; only boot and protocol failures
exit. Test both.
**R5. The mutation evidence.** `mutations.py` copies the package
flattened to `<tmp>/bus`. There the runtime infers the repository root as
the temp parent, which also holds the fixture tokens, so two process
tests fail before any mutation: "startup token refusal…" and "loaded
fixture token is absent…" (`flattened-baseline.txt`). Every entry in
`mutations.json` lists both, and `killed` is `exit != 0`, so the file
would say 11/11 for mutations that change nothing. My rerun shows the
mutants really are killed. Fix the script so it keeps the `packages/bus`
layout and counts a mutant killed only on a failure the baseline doesn't
have. `mutations-dw.py` does both and can be reused.
## Notes, not blocking
1. **Approval is reusable.** One resolved gated decision authorizes the
same action, target and run without limit: three `deploy` calls on
one "yes" in P3. README says so and leaves exactly-once to S3 and S6.
Whether a gated approval is single-use is a lead question, and it
should be answered before S3 and S6 call `authorize`. I'll raise it
with Sage.
2. **The timestamp scan grows with the database.** `#verifyTimes` scans
every timed table on every commit, reads included, synchronously. An
empty transaction takes 14.7 ms at 10k events, 116 ms at 100k and
462 ms at 500k (`scan-bench.txt`). Fine for slice 1. Near 5M events it
would pass the 5 s client timeout. Checking only rows above each
table's starting `seq` keeps the same guarantee. A follow-up row would
do.
3. Refusals from a reader or a non-holder each append an `action.refused`
event: 10 refusals gave 10 events (P4). That's acceptable under the
cooperative boundary; nothing limits the rate.
4. `in_reply_to: ""` reaches the foreign key and returns `storage-refused`
(P5). `identifier()` on `in_reply_to` and `corrects` would return
`invalid-request`.
5. `assertClean` refuses any value containing a token as a substring.
Nothing stops a one-character token, which would refuse almost
everything. A minimum token length (16 or so) costs nothing.
6. If the refusal-evidence transaction in `request()` throws, the raw
error reaches `serve`, which reports `invalid-envelope`.
7. For S3, my row: there is no registration point for task verbs.
`#dispatch` is a closed switch, `request()` runs synchronously inside a
transaction, and Vikunja calls are async. Nothing lets the `@sync`
identity write `task_snapshots` or `task.changed.external`, because
`recordEvent` needs an agent holder. The S3 brief lets me touch
`packages/bus` to register verbs, so I'll add these there and
coordinate with Rocko. `decision.resolve.technical` is in the
vocabulary but nothing checks it yet.
8. A message delivered to a holder that dies before `message.read` is
never delivered again, and there is no `failed` write path. The trail
shows it. S4 should show it too.
## Files here
- `review-r1.md`, `comment.md`
- `probes.test.mjs`, `probes.txt`: P1 to P6
- `terminal-walk.mjs`, `terminal-walk.txt`, `ancestry.txt`: R1
- `human-escape.mjs`, `human-escape.txt`: the direct-child refusal
- `scan-bench.mjs`, `scan-bench.txt`: note 2
- `mutations-dw.py`, `mutations-dw.txt`, `flattened-baseline.txt`: R5
- `node24.txt`, `node26.txt`, `s1r2-contract.txt`
The probe scripts run from a `dw/` directory beside `packages/bus/src`.
@@ -0,0 +1 @@
PASS actual S1 validate/resolve -> S2 normalize/start -> socket claim/message; scoped fixture token callback; launch classification
@@ -0,0 +1,15 @@
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
import { Store } from '../src/store.mjs';
const root = mkdtempSync(join(tmpdir(), 'dw-bench-'));
const store = new Store(root);
let ms = Date.parse('2026-10-01T00:00:00.000Z'), have = 0;
for (const target of [10000, 100000, 500000]) {
store.transaction(() => { for (; have < target; have++) store.run('INSERT INTO events(id,at,business,kind,actor_role,actor_run,subject,body) VALUES(?,?,?,?,?,?,?,?)', randomUUID(), new Date(ms++).toISOString(), 'demo', 'action.allowed', 'pm', 'pm-run', null, '{"action":"routine"}'); });
const t0 = performance.now();
for (let i = 0; i < 5; i++) store.transaction(() => {});
console.log(`events=${target} empty transaction ms=${((performance.now() - t0) / 5).toFixed(1)}`);
}
store.close(); rmSync(root, { recursive: true, force: true });
@@ -0,0 +1,3 @@
events=10000 empty transaction ms=14.7
events=100000 empty transaction ms=116.3
events=500000 empty transaction ms=461.8
@@ -0,0 +1,10 @@
// Real /proc reads for every ancestor; only the CLI hop itself is synthetic.
import { readProcess, verifyHuman } from '../src/human.mjs';
const terminal = Number(process.argv[2]);
const nonce = 'a'.repeat(64), cliPath = '/fixture/human-cli.mjs';
for (const pid of [terminal, 4295, 4152, 4136, 2173]) {
try { const p = readProcess(pid); console.log('readProcess', pid, 'ok', p.argv[0]); } catch (e) { console.log('readProcess', pid, e.code); }
}
const read = (pid) => pid === 999999 ? { pid, ppid: terminal, startTime: '1', uid: process.getuid(), argv: ['node', cliPath], env: { MOSAIC_BUS_CLI_NONCE: nonce } } : readProcess(pid);
try { console.log('verifyHuman from a real alacritty ancestry:', JSON.stringify(verifyHuman({ pid: 999999, startTime: '1', nonce, business: 'demo' }, { cliPath, readProcess: read }))); }
catch (e) { console.log('verifyHuman from a real alacritty ancestry:', e.code); }
@@ -0,0 +1,6 @@
readProcess 8357 ok alacritty
readProcess 4295 ok Hyprland
readProcess 4152 ok /usr/bin/start-hyprland
readProcess 4136 human-required
readProcess 2173 human-required
verifyHuman from a real alacritty ancestry: human-required