docs(review): row 37 S2 round 1, changes (darkwing)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user