docs(review): row 37 S2 round 2, approve (darkwing)
Round 2 of #1519: R1 to R5 fixed and reproduced, 43/43 on Node 26 and Node 24, 22 of 23 mutants killed against a clean baseline. Records a reparented-CLI path past the human proof for Sage's ruling. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
2275787 bash 1000
|
||||||
|
406108 claude 1000
|
||||||
|
609458 node-MainThread 1000
|
||||||
|
609414 node-MainThread 1000
|
||||||
|
1274 systemd 1000
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
Row 37, S2, round 2: **approve** (Darkwing)
|
||||||
|
|
||||||
|
Candidate `candidate-manifest.sha256` `61519059…`, 24 files under
|
||||||
|
`packages/bus/`. Full record:
|
||||||
|
`agents/darkwing/work/slice1-s2-review/review-r2.md`.
|
||||||
|
|
||||||
|
The patch applies at `fef4b362` and adds only `packages/bus/`. The
|
||||||
|
manifest checks 24/24. Tests pass 43/43 on Node 26 and on Node 24, and
|
||||||
|
Rocko's contract script passes against S1 round 2.
|
||||||
|
|
||||||
|
All five required items are fixed, and I reran my round 1 probes:
|
||||||
|
- R1: terminal 8357's real ancestry passes. Unreadable ancestors still
|
||||||
|
get the command and launch checks.
|
||||||
|
- R2: a revoked run gets `run-revoked`, including after a broker restart.
|
||||||
|
- R3: cto and pm both route their own cross-role decisions to `human`.
|
||||||
|
- R4: a refused bind replies and the broker keeps serving.
|
||||||
|
- R5: 17 of Rocko's mutants, plus 5 of my 6, are killed against a clean
|
||||||
|
baseline, and the no-op isn't.
|
||||||
|
|
||||||
|
For Sage: since R1, an agent run here passes the human proof with
|
||||||
|
`setsid -f env -i node human-cli.mjs`, because `systemd --user`'s environ
|
||||||
|
is unreadable. I reproduced it from my own session. A CLI reparented to
|
||||||
|
pid 1 passed in round 1 too. That's inside the README's cooperative
|
||||||
|
limit, but REQ-DEC-3 doesn't say so. I recommend a lead decision that
|
||||||
|
slice 1's human proof is cooperative, with S6 isolating managed agents.
|
||||||
|
My round 1 claim that the fix wouldn't weaken anything was wrong.
|
||||||
|
|
||||||
|
Non-blocking: no test checks a within-role `route_to`, which is my one
|
||||||
|
surviving mutant. Two R2 contract lines come from a script that isn't in
|
||||||
|
the packet. A refused `bindLaunch` uses up its run name.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Round 2: does a reparented human CLI started from this agent run pass now that EACCES ancestors skip markers?
|
||||||
|
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { setTimeout as sleep } from 'node:timers/promises';
|
||||||
|
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));
|
||||||
|
writeFileSync(join(root, 'req'), JSON.stringify({ business: 'demo', verb: 'inbox' }));
|
||||||
|
const cases = {
|
||||||
|
'setsid -f, env -i': `setsid -f sh -c 'sleep 0.5; echo "ppid $(cut -d" " -f4 /proc/$$/stat) $(cat /proc/$(cut -d" " -f4 /proc/$$/stat)/comm)" > "$0/A.ppid"; exec env -i PATH="$PATH" "$1" "$2" "$3" < "$0/req" > "$0/A.out" 2>&1' "${root}" "${process.execPath}" "${cli}" "${rt.path}"`,
|
||||||
|
'setsid -f, inherited env': `setsid -f sh -c 'sleep 0.5; exec "$1" "$2" "$3" < "$0/req" > "$0/B.out" 2>&1' "${root}" "${process.execPath}" "${cli}" "${rt.path}"`,
|
||||||
|
'env -i, no setsid': `env -i PATH="$PATH" "${process.execPath}" "${cli}" "${rt.path}" < "${root}/req" > "${root}/C.out" 2>&1`,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
for (const [name, cmd] of Object.entries(cases)) {
|
||||||
|
const c = spawn('sh', ['-c', cmd], { stdio: 'ignore' });
|
||||||
|
await new Promise((r) => c.on('close', r));
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 50 && !['A', 'B', 'C'].every((k) => existsSync(join(root, k + '.out')) && readFileSync(join(root, k + '.out'), 'utf8')); i++) await sleep(200);
|
||||||
|
for (const [k, name] of [['A', 'setsid -f, env -i'], ['B', 'setsid -f, inherited env'], ['C', 'env -i, no setsid']]) {
|
||||||
|
const f = join(root, k + '.out');
|
||||||
|
console.log(name + ':', existsSync(f) ? readFileSync(f, 'utf8').trim() : 'no output');
|
||||||
|
}
|
||||||
|
if (existsSync(join(root, 'A.ppid'))) console.log('setsid child reparented to', readFileSync(join(root, 'A.ppid'), 'utf8').trim());
|
||||||
|
} finally { await rt.close(); rmSync(root, { recursive: true, force: true }); }
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
setsid -f, env -i: []
|
||||||
|
setsid -f, inherited env: human-required
|
||||||
|
env -i, no setsid: human-required
|
||||||
|
setsid child reparented to ppid 1274 systemd
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Round 2: Rocko's cases plus Darkwing's boundary mutants, repository layout, each compared to a clean baseline.
|
||||||
|
import pathlib,tempfile,shutil,subprocess,json,re,sys
|
||||||
|
src=open(sys.argv[1]).read()
|
||||||
|
cases=eval(src[src.index('cases=[')+6:src.index('\n]\n')+2])
|
||||||
|
extra=[
|
||||||
|
('dw-eacces-all','human.mjs',"if(e.code!=='EACCES')throw e;",''),
|
||||||
|
('dw-command-skip-null-env','human.mjs',"const command=basename(current.argv[0]??'');","const command=current.env===null?'':basename(current.argv[0]??'');"),
|
||||||
|
('dw-launch-skip-null-env','human.mjs',"launches.some((r)=>r.pid===current.pid","current.env!==null&&launches.some((r)=>r.pid===current.pid"),
|
||||||
|
('dw-route-any-class','broker.mjs',"cls==='cross-role'&&proposedRoute===s.role","proposedRoute===s.role"),
|
||||||
|
('dw-reply-identifier','broker.mjs',"if(a[k]!==undefined){identifier(a[k]);","if(a[k]!==undefined){"),
|
||||||
|
('dw-token-15','credentials.mjs',"if(token.length<16)","if(token.length<15)"),
|
||||||
|
]
|
||||||
|
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=180)
|
||||||
|
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-mut2-') 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+extra:
|
||||||
|
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,26 @@
|
|||||||
|
{"baseline_exit": 0, "baseline_failures": []}
|
||||||
|
{"mutation": "no-op", "exit": 0, "killed": false}
|
||||||
|
{"mutation": "ancestor-eacces", "exit": 1, "killed": true, "new_failures": ["EACCES ancestor environments skip only markers; commands and registered launches still refuse", "real pid 1 remains inspectable when its environment is protected"]}
|
||||||
|
{"mutation": "revoked-reclaim", "exit": 1, "killed": true, "new_failures": ["revocation permanently bars the old run from reclaiming first, including after broker restart"]}
|
||||||
|
{"mutation": "self-approval", "exit": 1, "killed": true, "new_failures": ["both arbiters require human resolution when their cross-role route is themselves"]}
|
||||||
|
{"mutation": "bind-refusal", "exit": 1, "killed": true, "new_failures": ["a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it"]}
|
||||||
|
{"mutation": "short-token", "exit": 1, "killed": true, "new_failures": ["opaque tokens shorter than 16 characters refuse before use"]}
|
||||||
|
{"mutation": "refusal-error", "exit": 1, "killed": true, "new_failures": ["empty message references refuse before storage; refusal-evidence failure stays a typed error"]}
|
||||||
|
{"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": ["both arbiters require human resolution when their cross-role route is themselves", "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", "both arbiters require human resolution when their cross-role route is themselves", "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": ["EACCES ancestor environments skip only markers; commands and registered launches still refuse", "human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse"]}
|
||||||
|
{"mutation": "dw-eacces-all", "exit": 1, "killed": true, "new_failures": ["EACCES ancestor environments skip only markers; commands and registered launches still refuse"]}
|
||||||
|
{"mutation": "dw-command-skip-null-env", "exit": 1, "killed": true, "new_failures": ["EACCES ancestor environments skip only markers; commands and registered launches still refuse"]}
|
||||||
|
{"mutation": "dw-launch-skip-null-env", "exit": 1, "killed": true, "new_failures": ["EACCES ancestor environments skip only markers; commands and registered launches still refuse"]}
|
||||||
|
{"mutation": "dw-route-any-class", "exit": 0, "killed": false, "new_failures": []}
|
||||||
|
{"mutation": "dw-reply-identifier", "exit": 1, "killed": true, "new_failures": ["empty message references refuse before storage; refusal-evidence failure stays a typed error"]}
|
||||||
|
{"mutation": "dw-token-15", "exit": 1, "killed": true, "new_failures": ["opaque tokens shorter than 16 characters refuse before use"]}
|
||||||
|
killed 22 of 23
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
v24.21.0
|
||||||
|
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (154.473782ms)
|
||||||
|
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (224.999695ms)
|
||||||
|
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (218.2267ms)
|
||||||
|
✔ launch events require a human CLI capability; generic emit cannot forge authority events (137.241048ms)
|
||||||
|
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (132.177847ms)
|
||||||
|
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (81.576178ms)
|
||||||
|
✔ task action subjects and linked decision trail are complete and ordered (82.729828ms)
|
||||||
|
✔ launch binding is durable and reconnecting requires the identical trusted record (43.684156ms)
|
||||||
|
✔ business isolation includes inherited object names and cross-business message references (80.693553ms)
|
||||||
|
✔ authority never transfers between action, run, target, unresolved or replaced role holder (119.344537ms)
|
||||||
|
✔ task projection uses schema current view, skipping earlier and equal-start polls (63.000167ms)
|
||||||
|
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (105.963726ms)
|
||||||
|
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (59.240762ms)
|
||||||
|
✔ both arbiters require human resolution when their cross-role route is themselves (95.888631ms)
|
||||||
|
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.641801ms)
|
||||||
|
✔ only validated broker references load; returned data and exceptions cannot expose a known token (3.700265ms)
|
||||||
|
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (5.697539ms)
|
||||||
|
✔ expiry refuses use and env references never become client data (1.073265ms)
|
||||||
|
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (2.120409ms)
|
||||||
|
✔ opaque tokens shorter than 16 characters refuse before use (0.43432ms)
|
||||||
|
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.445442ms)
|
||||||
|
✔ process reader gets own kernel identity without exposing environment values (0.777039ms)
|
||||||
|
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (2.360178ms)
|
||||||
|
✔ real pid 1 remains inspectable when its environment is protected (0.479225ms)
|
||||||
|
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (183.010225ms)
|
||||||
|
✔ startup token refusal returns safe code without value or partial listening broker (46.446663ms)
|
||||||
|
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (157.60812ms)
|
||||||
|
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (150.794865ms)
|
||||||
|
✔ trusted host registers later launches; socket clients never have a registration verb (147.923936ms)
|
||||||
|
✔ runtime excludes declared project roots even when host supplies no repoRoots (48.612501ms)
|
||||||
|
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (146.769057ms)
|
||||||
|
✔ v3b prototype refusals, views and append-only mutations (881.545888ms)
|
||||||
|
✔ creates private WAL store and excludes a second writer until explicit close (103.52694ms)
|
||||||
|
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (170.510621ms)
|
||||||
|
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (141.33725ms)
|
||||||
|
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (142.218029ms)
|
||||||
|
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (81.918208ms)
|
||||||
|
✔ async transactions refuse before invoking their function (64.829609ms)
|
||||||
|
✔ socket capability stamps launch identity; shared views use wire, no SQL client (139.594133ms)
|
||||||
|
✔ two wire claims serialize; a lost reply never automatically retries (153.392792ms)
|
||||||
|
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (90.253752ms)
|
||||||
|
✔ client preserves UTF-8 when a response divides a multibyte character (12.675617ms)
|
||||||
|
✔ committed mutation followed by dropped reply reports unknown and is never retried (110.705929ms)
|
||||||
|
ℹ tests 43
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 43
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 1679.066298
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
v26.8.1
|
||||||
|
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (138.901031ms)
|
||||||
|
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (225.790118ms)
|
||||||
|
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (266.266044ms)
|
||||||
|
✔ launch events require a human CLI capability; generic emit cannot forge authority events (190.815244ms)
|
||||||
|
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (173.510957ms)
|
||||||
|
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (116.366573ms)
|
||||||
|
✔ task action subjects and linked decision trail are complete and ordered (85.875561ms)
|
||||||
|
✔ launch binding is durable and reconnecting requires the identical trusted record (48.582074ms)
|
||||||
|
✔ business isolation includes inherited object names and cross-business message references (86.215424ms)
|
||||||
|
✔ authority never transfers between action, run, target, unresolved or replaced role holder (120.666191ms)
|
||||||
|
✔ task projection uses schema current view, skipping earlier and equal-start polls (58.840197ms)
|
||||||
|
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (96.298649ms)
|
||||||
|
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (60.71768ms)
|
||||||
|
✔ both arbiters require human resolution when their cross-role route is themselves (112.352052ms)
|
||||||
|
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.905483ms)
|
||||||
|
✔ only validated broker references load; returned data and exceptions cannot expose a known token (5.577536ms)
|
||||||
|
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.303784ms)
|
||||||
|
✔ expiry refuses use and env references never become client data (1.353573ms)
|
||||||
|
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.848422ms)
|
||||||
|
✔ opaque tokens shorter than 16 characters refuse before use (0.370908ms)
|
||||||
|
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.303165ms)
|
||||||
|
✔ process reader gets own kernel identity without exposing environment values (1.698768ms)
|
||||||
|
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.235204ms)
|
||||||
|
✔ real pid 1 remains inspectable when its environment is protected (0.470908ms)
|
||||||
|
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (185.421326ms)
|
||||||
|
✔ startup token refusal returns safe code without value or partial listening broker (47.484669ms)
|
||||||
|
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (165.943844ms)
|
||||||
|
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (179.207621ms)
|
||||||
|
✔ trusted host registers later launches; socket clients never have a registration verb (184.516304ms)
|
||||||
|
✔ runtime excludes declared project roots even when host supplies no repoRoots (39.558799ms)
|
||||||
|
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (161.839535ms)
|
||||||
|
✔ v3b prototype refusals, views and append-only mutations (1051.689327ms)
|
||||||
|
✔ creates private WAL store and excludes a second writer until explicit close (128.588098ms)
|
||||||
|
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (146.442339ms)
|
||||||
|
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (160.892644ms)
|
||||||
|
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (171.292568ms)
|
||||||
|
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (104.224826ms)
|
||||||
|
✔ async transactions refuse before invoking their function (102.362634ms)
|
||||||
|
✔ socket capability stamps launch identity; shared views use wire, no SQL client (128.936609ms)
|
||||||
|
✔ two wire claims serialize; a lost reply never automatically retries (156.189584ms)
|
||||||
|
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (98.289776ms)
|
||||||
|
✔ client preserves UTF-8 when a response divides a multibyte character (13.417854ms)
|
||||||
|
✔ committed mutation followed by dropped reply reports unknown and is never retried (121.332995ms)
|
||||||
|
ℹ tests 43
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 43
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 1872.076149
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// A CLI reparented to pid 1 with a clean environment: verifyHuman never reads pid 1, so the walk ends at the CLI.
|
||||||
|
const nonce = 'a'.repeat(64), cliPath = '/fixture/human-cli.mjs';
|
||||||
|
for (const tree of ['s2-review', 's2-r2']) {
|
||||||
|
const { verifyHuman } = await import(`/home/jwoltje/darkwing-scratch/${tree}/packages/bus/src/human.mjs`);
|
||||||
|
const read = (pid) => { if (pid === 4242) return { pid, ppid: 1, startTime: '1', uid: process.getuid(), argv: ['node', cliPath], env: { MOSAIC_BUS_CLI_NONCE: nonce } }; throw Object.assign(new Error('unexpected read ' + pid), { code: 'x' }); };
|
||||||
|
let out; try { out = JSON.stringify(verifyHuman({ pid: 4242, startTime: '1', nonce, business: 'demo' }, { cliPath, readProcess: read })); } catch (e) { out = e.code; }
|
||||||
|
console.log(tree === 's2-review' ? 'round 1:' : 'round 2:', 'CLI with ppid 1, clean env ->', out);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
round 1: CLI with ppid 1, clean env -> {"business":"demo"}
|
||||||
|
round 2: CLI with ppid 1, clean env -> {"business":"demo"}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
P1 revoked pm reclaims -> "run-revoked"
|
||||||
|
P1 replacement pm claim -> {"role":"pm","run":"pm-next"}
|
||||||
|
P2 cto raise route_to human
|
||||||
|
P3 authorize 0 {"class":"gated","decision":"ebd52fe9-1676-46ed-bdf9-d3d93e71939c"}
|
||||||
|
P3 authorize 1 {"class":"gated","decision":"ebd52fe9-1676-46ed-bdf9-d3d93e71939c"}
|
||||||
|
P3 authorize 2 {"class":"gated","decision":"ebd52fe9-1676-46ed-bdf9-d3d93e71939c"}
|
||||||
|
P4 events added by 10 refusals: 10
|
||||||
|
P5 -> "invalid-request"
|
||||||
|
✔ P1 revoked run reclaims its role with its old capability (101.410494ms)
|
||||||
|
✖ P2 arbiter resolves its own cross-role decision (69.554373ms)
|
||||||
|
✔ P3 one approval authorizes repeatedly (79.923114ms)
|
||||||
|
✔ P4 refusals from a reader and a non-holder append events (106.267135ms)
|
||||||
|
✔ P5 empty in_reply_to (54.604374ms)
|
||||||
|
P6 boot true
|
||||||
|
P6 first bind true
|
||||||
|
P6 duplicate bind {"ok":false,"error":"duplicate-run"}
|
||||||
|
P6 exit code after refused bind null
|
||||||
|
✔ P6 a refused bindLaunch over IPC stops the broker process (3104.287659ms)
|
||||||
|
ℹ tests 6
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 5
|
||||||
|
ℹ fail 1
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 3577.172243
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at dw/probes.test.mjs:41:1
|
||||||
|
✖ P2 arbiter resolves its own cross-role decision (69.554373ms)
|
||||||
|
Error: human-required
|
||||||
|
at fail (file:///home/jwoltje/darkwing-scratch/s2-r2/packages/bus/src/broker.mjs:48:9)
|
||||||
|
at #human (file:///home/jwoltje/darkwing-scratch/s2-r2/packages/bus/src/broker.mjs:138:65)
|
||||||
|
at #dispatch (file:///home/jwoltje/darkwing-scratch/s2-r2/packages/bus/src/broker.mjs:513:55)
|
||||||
|
at file:///home/jwoltje/darkwing-scratch/s2-r2/packages/bus/src/broker.mjs:321:38
|
||||||
|
at Store.transaction (file:///home/jwoltje/darkwing-scratch/s2-r2/packages/bus/src/store.mjs:156:22)
|
||||||
|
at Broker.request (file:///home/jwoltje/darkwing-scratch/s2-r2/packages/bus/src/broker.mjs:319:26)
|
||||||
|
at call (file:///home/jwoltje/darkwing-scratch/s2-r2/packages/bus/dw/probes.test.mjs:26:45)
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/s2-r2/packages/bus/dw/probes.test.mjs:46:3)
|
||||||
|
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||||
|
at Test.run (node:internal/test_runner/test:1402:25) {
|
||||||
|
code: 'human-required'
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// Darkwing round 2 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' }];
|
||||||
|
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'] } } } } };
|
||||||
|
const call = (b, cap, verb, args = {}) => b.request(cap, { verb, args });
|
||||||
|
const code = (f) => { try { return JSON.stringify(f()); } catch (e) { return e.code ?? String(e); } };
|
||||||
|
const raise = (b, cap, action, extra = {}) => call(b, cap, 'decision.raise', { action, question: 'Allow?', options, recommendation: 'no', blocking: false, ...extra });
|
||||||
|
function open(root) {
|
||||||
|
const store = new Store(root);
|
||||||
|
const b = new Broker({ store, businesses });
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
function setup(t) {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'dw-bus-'));
|
||||||
|
const o = open(root);
|
||||||
|
t.after(() => { try { o.store.close(); } catch {} rmSync(root, { recursive: true, force: true }); });
|
||||||
|
return { root, ...o };
|
||||||
|
}
|
||||||
|
for (const [raiser, other, action, extra] of [['cto', 'pm', 'task.scope.change', { domain: 'technical' }], ['pm', 'cto', 'task.priority.change', {}]]) {
|
||||||
|
test(`R3 ${raiser} self-arbiter routes to human`, (t) => {
|
||||||
|
const { b, agent, human } = setup(t), me = agent(raiser), them = agent(other);
|
||||||
|
call(b, me, 'role.claim'); call(b, them, 'role.claim');
|
||||||
|
const d = raise(b, me, action, { ...extra, target: 'x1' });
|
||||||
|
console.log(`R3 ${raiser} raise class=${d.class} route_to=${d.route_to}`);
|
||||||
|
console.log(`R3 ${raiser} self resolve ->`, code(() => call(b, me, 'decision.resolve', { id: d.id, choice: 'yes' })));
|
||||||
|
console.log(`R3 ${raiser} other role resolve ->`, code(() => call(b, them, 'decision.resolve', { id: d.id, choice: 'yes' })));
|
||||||
|
console.log(`R3 ${raiser} authorize before ->`, code(() => b.authorize(me, action, { decision: d.id, target: 'x1' })));
|
||||||
|
console.log(`R3 ${raiser} human resolve ->`, code(() => call(b, human, 'decision.resolve', { id: d.id, choice: 'yes' }).status));
|
||||||
|
console.log(`R3 ${raiser} authorize after ->`, code(() => b.authorize(me, action, { decision: d.id, target: 'x1' })));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
test('R3 non-arbiter coder still routes to cto', (t) => {
|
||||||
|
const { b, agent } = setup(t), c = agent('coder'), cto = agent('cto');
|
||||||
|
call(b, c, 'role.claim'); call(b, cto, 'role.claim');
|
||||||
|
const d = raise(b, c, 'task.scope.change', { domain: 'technical', target: 'x1' });
|
||||||
|
console.log(`R3 coder raise class=${d.class} route_to=${d.route_to}`);
|
||||||
|
console.log('R3 cto resolves coder ->', code(() => call(b, cto, 'decision.resolve', { id: d.id, choice: 'yes' }).status));
|
||||||
|
console.log('R3 coder authorize ->', code(() => b.authorize(c, 'task.scope.change', { decision: d.id, target: 'x1' })));
|
||||||
|
});
|
||||||
|
test('R2 revoked run across a broker restart', (t) => {
|
||||||
|
const { root, b, store, 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 });
|
||||||
|
store.close();
|
||||||
|
const o = open(root);
|
||||||
|
t.after(() => o.store.close());
|
||||||
|
const p1 = o.agent('pm');
|
||||||
|
console.log('R2 revoked pm-run after restart ->', code(() => call(o.b, p1, 'role.claim')));
|
||||||
|
const p2 = o.agent('pm', 'pm-next');
|
||||||
|
console.log('R2 replacement after restart ->', code(() => call(o.b, p2, 'role.claim')));
|
||||||
|
console.log('R2 revoked pm-run still other verbs ->', code(() => call(o.b, p1, 'message.send', { to: 'cto', body: 'x' })));
|
||||||
|
});
|
||||||
|
test('bindLaunch run key after a refused record', (t) => {
|
||||||
|
const { b, agent } = setup(t);
|
||||||
|
agent('pm', 'r9');
|
||||||
|
const { store } = { store: null };
|
||||||
|
console.log('dup ->', code(() => b.bindLaunch({ business: 'demo', role: 'pm', run: 'r9', harness: 'pi' })));
|
||||||
|
console.log('bad harness then good ->', code(() => b.bindLaunch({ business: 'demo', role: 'pm', run: 'r10', harness: 'x' })), code(() => typeof b.bindLaunch({ business: 'demo', role: 'pm', run: 'r10', harness: 'pi' })));
|
||||||
|
});
|
||||||
|
test('R4 broker keeps serving after a refused bind, protocol error exits 2', 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 }); });
|
||||||
|
const ask = async (msg) => { const m = once(child, 'message'); child.send(msg); return (await m)[0]; };
|
||||||
|
console.log('R4 boot', (await ask({ op: 'boot', config: { dataRoot: root, businesses, launches: [] } })).ok);
|
||||||
|
const rec = { business: 'demo', role: 'cto', run: 'r1', harness: 'pi', pid: process.pid, startTime: '1' };
|
||||||
|
console.log('R4 first', (await ask({ op: 'bindLaunch', record: rec })).ok);
|
||||||
|
console.log('R4 duplicate', JSON.stringify(await ask({ op: 'bindLaunch', record: rec })));
|
||||||
|
console.log('R4 malformed record', JSON.stringify(await ask({ op: 'bindLaunch', record: { nope: 1 } })));
|
||||||
|
const r2 = await ask({ op: 'bindLaunch', record: { ...rec, run: 'r2' } });
|
||||||
|
console.log('R4 later bind', r2.ok, r2.launch?.run);
|
||||||
|
const exit = once(child, 'exit');
|
||||||
|
console.log('R4 second boot', JSON.stringify(await ask({ op: 'boot', config: {} })));
|
||||||
|
const timer = setTimeout(() => child.kill('SIGKILL'), 5000);
|
||||||
|
console.log('R4 exit after protocol error', (await exit)[0]); clearTimeout(timer);
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
R3 cto raise class=cross-role route_to=human
|
||||||
|
R3 cto self resolve -> human-required
|
||||||
|
R3 cto other role resolve -> human-required
|
||||||
|
R3 cto authorize before -> decision-not-approved
|
||||||
|
R3 cto human resolve -> undefined
|
||||||
|
R3 cto authorize after -> {"class":"cross-role","decision":"4381b2ea-f8bb-4614-89a2-c3b58dcc9478"}
|
||||||
|
R3 pm raise class=cross-role route_to=human
|
||||||
|
R3 pm self resolve -> human-required
|
||||||
|
R3 pm other role resolve -> human-required
|
||||||
|
R3 pm authorize before -> decision-not-approved
|
||||||
|
R3 pm human resolve -> undefined
|
||||||
|
R3 pm authorize after -> {"class":"cross-role","decision":"228defd1-0385-47dc-a562-14cc7a1fe158"}
|
||||||
|
R3 coder raise class=cross-role route_to=cto
|
||||||
|
R3 cto resolves coder -> undefined
|
||||||
|
R3 coder authorize -> {"class":"cross-role","decision":"134e5879-d92a-4694-9f57-99d3511ae7f5"}
|
||||||
|
R2 revoked pm-run after restart -> run-revoked
|
||||||
|
R2 replacement after restart -> {"role":"pm","run":"pm-next"}
|
||||||
|
R2 revoked pm-run still other verbs -> not-holder
|
||||||
|
dup -> duplicate-run
|
||||||
|
bad harness then good -> invalid-harness "string"
|
||||||
|
✔ R3 cto self-arbiter routes to human (94.464351ms)
|
||||||
|
✔ R3 pm self-arbiter routes to human (94.142635ms)
|
||||||
|
✔ R3 non-arbiter coder still routes to cto (77.105785ms)
|
||||||
|
✔ R2 revoked run across a broker restart (116.284091ms)
|
||||||
|
✔ bindLaunch run key after a refused record (50.576946ms)
|
||||||
|
R4 boot true
|
||||||
|
R4 first true
|
||||||
|
R4 duplicate {"ok":false,"error":"duplicate-run"}
|
||||||
|
R4 malformed record {"ok":false,"error":"invalid-launch-process"}
|
||||||
|
R4 later bind true r2
|
||||||
|
R4 second boot {"ok":false,"error":"invalid-host-request"}
|
||||||
|
R4 exit after protocol error 2
|
||||||
|
✔ R4 broker keeps serving after a refused bind, protocol error exits 2 (97.430903ms)
|
||||||
|
ℹ tests 6
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 6
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 592.244641
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# Row 37, S2 bus and broker core, round 2 review (Darkwing)
|
||||||
|
|
||||||
|
Issue #1519. Candidate: Rocko's `candidate-manifest.sha256` (sha256
|
||||||
|
`6151905968ea5db80ed19ab44aebdb37e433f1803a6d8b022fa35b31c91f6ffa`), the
|
||||||
|
24 files under `packages/bus/`. Packet `agents/rocko/work/slice1-s2-r2/`,
|
||||||
|
`BUILD.md` sha256 `cfcef300…`, `build.patch` sha256 `40d7e838…`. Round 1
|
||||||
|
is `review-r1.md` in this directory.
|
||||||
|
|
||||||
|
Verdict: **approve.** R1 to R5 are fixed, and so are notes 4, 5 and 6. I
|
||||||
|
reran every round 1 probe and added new ones, and each fix holds. One
|
||||||
|
finding needs Sage rather than Rocko. R1's fix, which I asked for, lets an
|
||||||
|
agent run on this machine pass the human proof with two commands. That
|
||||||
|
is inside the limit the README states, but round 1 blocked the same
|
||||||
|
command, and my round 1 review said the fix wouldn't weaken anything. That
|
||||||
|
was wrong. Details under "For Sage".
|
||||||
|
|
||||||
|
## What I checked
|
||||||
|
|
||||||
|
- The three packet hashes match Rocko's message. `build.patch` applies at
|
||||||
|
`fef4b362` and adds only `packages/bus/`; the manifest checks 24/24.
|
||||||
|
- Nine files changed from round 1: `README.md`, four in `src/`
|
||||||
|
(`broker`, `credentials`, `human`, `process`) and their four test files.
|
||||||
|
I read every source and README hunk.
|
||||||
|
- Tests: 43/43 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 in the
|
||||||
|
repository layout, also 43/43 (`node24-r2.txt`).
|
||||||
|
- Rocko's round 1 contract script passes against S1 round 2 plus this
|
||||||
|
candidate (`s1r2-contract-r2.txt`).
|
||||||
|
|
||||||
|
## Required items from round 1
|
||||||
|
|
||||||
|
**R1, real terminals.** An environ read that fails with EACCES now sets
|
||||||
|
`env` to null. The walk skips only the marker check for that ancestor; the
|
||||||
|
command check and the launch-registry check still run. Any other read
|
||||||
|
error still refuses, and the CLI process itself still needs a readable
|
||||||
|
nonce. `terminal-walk-r2.txt` walks the real ancestry of terminal 8357 up
|
||||||
|
to `plasmalogin` 2173, with only the CLI hop synthetic, and passes.
|
||||||
|
`readProcess(1)` returns `env: null`. Rocko's test reads the real pid 1.
|
||||||
|
|
||||||
|
**R2, revoked run.** `role.claim` refuses a run with a `revoke` row. In my
|
||||||
|
round 1 probe P1, the revoked `pm-run` now gets `run-revoked` and the
|
||||||
|
replacement claims (`probes-r1-rerun.txt`). After a store close and a new
|
||||||
|
Broker on the same data, the revoked run still gets `run-revoked` and the
|
||||||
|
replacement still claims (`probes-r2.txt`).
|
||||||
|
|
||||||
|
**R3, self-arbiter.** When a cross-role route names the raiser, it goes
|
||||||
|
to `human` and the class stays `cross-role`. For both cto
|
||||||
|
(`task.scope.change`, technical) and pm (`task.priority.change`,
|
||||||
|
delivery): `route_to` is `human`, the raiser and the other role both get
|
||||||
|
`human-required` on resolve, and `authorize` refuses with
|
||||||
|
`decision-not-approved` until the human resolves. After that it
|
||||||
|
authorizes. A coder's `task.scope.change` still routes to cto, and cto's
|
||||||
|
resolution authorizes it. Round 1's P2 now fails at the self-resolve,
|
||||||
|
which is the intended result.
|
||||||
|
|
||||||
|
**R4, `bindLaunch` over IPC.** A duplicate run gets
|
||||||
|
`{ok:false,error:'duplicate-run'}`, a malformed record gets
|
||||||
|
`invalid-launch-process`, and a later valid bind still succeeds. A second
|
||||||
|
`boot` still exits 2 (`probes-r2.txt`).
|
||||||
|
|
||||||
|
**R5, mutation evidence.** Rocko's new runner keeps the repository
|
||||||
|
layout, requires a clean baseline and a no-op control, and counts only
|
||||||
|
new failures. The packet retracts round 1's 11/11 claim in writing.
|
||||||
|
`mutations-dw2.py` runs Rocko's 17 cases plus six of mine against a clean
|
||||||
|
baseline. The no-op is not killed, all 17 of Rocko's are killed, and five
|
||||||
|
of my six are killed (`mutations-dw2.txt`):
|
||||||
|
|
||||||
|
| My mutant | Result |
|
||||||
|
|---|---|
|
||||||
|
| every environ read error skips markers, not only EACCES | killed |
|
||||||
|
| command check skipped for a null-env ancestor | killed |
|
||||||
|
| launch-registry check skipped for a null-env ancestor | killed |
|
||||||
|
| `in_reply_to` and `corrects` skip `identifier()` | killed |
|
||||||
|
| token minimum 15 instead of 16 | killed |
|
||||||
|
| the self-route rule applied to every class, not only cross-role | survives |
|
||||||
|
|
||||||
|
The survivor is note 2 below.
|
||||||
|
|
||||||
|
## Notes 4 to 6 from round 1
|
||||||
|
|
||||||
|
- 4: `in_reply_to: ""` returns `invalid-request` (P5).
|
||||||
|
- 5: tokens shorter than 16 characters refuse, after the LF trim.
|
||||||
|
- 6: a failed refusal-evidence write throws `storage-unavailable`.
|
||||||
|
Rocko's `refusal-error` mutant shows a test catches it.
|
||||||
|
|
||||||
|
## For Sage
|
||||||
|
|
||||||
|
**The human proof and a reparented CLI.** With R1 in place, an agent run
|
||||||
|
on this machine passes the proof by running
|
||||||
|
|
||||||
|
setsid -f env -i PATH="$PATH" node human-cli.mjs <socket>
|
||||||
|
|
||||||
|
`escape-r2.txt` ran it from my own session, which has `CLAUDECODE` set,
|
||||||
|
against a scratch broker. The CLI got `[]` back from `inbox`, so the
|
||||||
|
broker bound it as Jason. `setsid -f` moves the CLI under
|
||||||
|
`systemd --user` (1274), the nearest subreaper above my session
|
||||||
|
(`agent-ancestry-r2.txt`). Its environ is unreadable, so the marker check
|
||||||
|
skips it, its command is `systemd`, and its parent is pid 1. `env -i`
|
||||||
|
strips the markers from the CLI. Without `env -i`, or without `setsid`,
|
||||||
|
the CLI is still refused.
|
||||||
|
|
||||||
|
Round 1 refused that same command only because of `systemd --user`'s
|
||||||
|
unreadable environ, so that refusal was incidental. A CLI reparented
|
||||||
|
straight to pid 1 with a clean environment passed in round 1 too: the
|
||||||
|
walk stops at a parent of 1 and never reads it (`pid1-reparent.txt`, both
|
||||||
|
rounds). An agent with no subreaper above it had this path all along.
|
||||||
|
|
||||||
|
The README already places reparenting inside the "cooperative trust
|
||||||
|
limit". The slice 1 brief and REQ-DEC-3 say human resolution comes "only
|
||||||
|
from a CLI process outside any agent run", and no lead decision records
|
||||||
|
that the requirement holds only against agents that don't try to escape.
|
||||||
|
I don't think a same-UID `/proc` check can do better. Every process an
|
||||||
|
agent starts can drop its markers, and each ancestor test I can think of
|
||||||
|
has a way around it. Narrowing the EACCES skip to other-UID
|
||||||
|
ancestors would close the `systemd --user` path here, but terminals that
|
||||||
|
run under `systemd --user` would then fail as they did in round 1, and
|
||||||
|
the pid 1 path stays open.
|
||||||
|
|
||||||
|
My recommendation: record a lead decision that REQ-DEC-3 in slice 1 is
|
||||||
|
cooperative, meaning it stops an agent that runs `mosaic decide`
|
||||||
|
directly but not one that deliberately escapes. S6 should launch managed
|
||||||
|
agents where this can't work, for example a separate PID namespace or
|
||||||
|
UID with the broker socket not mounted. The T3 seats run unsandboxed and
|
||||||
|
can do this today. This doesn't block S2: Rocko built what the review
|
||||||
|
asked for, and the README states the limit.
|
||||||
|
|
||||||
|
**Still open from round 1.** One gated approval still authorizes the same
|
||||||
|
action without limit (note 1, P3 unchanged). It needs a ruling before S3
|
||||||
|
and S6 call `authorize`. R3 implements my default, `human`; Rocko is
|
||||||
|
waiting on your ruling if you want the other arbiter instead.
|
||||||
|
|
||||||
|
## Notes, not blocking
|
||||||
|
|
||||||
|
1. Rocko's packet includes two R2 contract lines ("loadBusiness refuses
|
||||||
|
launch.by…" and "narrowed launch is null…") whose script isn't in the
|
||||||
|
packet. The round 1 script reproduces; those two lines don't.
|
||||||
|
2. No test checks `route_to` on a within-role decision. With the
|
||||||
|
`cls === 'cross-role'` guard removed, a cto within-role decision records
|
||||||
|
`route_to: human` instead of `cto` and all 43 tests pass
|
||||||
|
(`within-role-route.txt`). One assertion fixes it.
|
||||||
|
3. `Broker.bindLaunch` adds the run key before the secret check and the
|
||||||
|
launch-record comparison (unchanged from round 1). A record refused on
|
||||||
|
either check uses up the run name, and a corrected retry gets
|
||||||
|
`duplicate-run`. That fails closed. S6 should know it before it
|
||||||
|
writes retries. I found this by reading the code; I didn't run it.
|
||||||
|
4. The broad suite receipts are round 1's, as `BUILD.md` says. Sage's
|
||||||
|
integration gate still covers every `scripts/test-*.sh`.
|
||||||
|
5. Round 1 notes 2, 3, 7 and 8 stand as written. Note 7's S3 work stays
|
||||||
|
with me.
|
||||||
|
|
||||||
|
## Files added for round 2
|
||||||
|
|
||||||
|
- `review-r2.md`, `comment-r2.md`
|
||||||
|
- `node26-r2.txt`, `node24-r2.txt`, `s1r2-contract-r2.txt`
|
||||||
|
- `probes-r1-rerun.txt`: round 1's P1 to P6 against this candidate
|
||||||
|
- `probes-r2.test.mjs`, `probes-r2.txt`: R2 across restart, R3 for both
|
||||||
|
arbiters and a coder, R4 later binds and protocol exit
|
||||||
|
- `terminal-walk-r2.txt`: R1 on the real ancestry, and `readProcess(1)`
|
||||||
|
- `escape-r2.mjs`, `escape-r2.txt`, `agent-ancestry-r2.txt`,
|
||||||
|
`pid1-reparent.mjs`, `pid1-reparent.txt`: the reparenting finding
|
||||||
|
- `mutations-dw2.py`, `mutations-dw2.txt`
|
||||||
|
- `within-role-route.mjs`, `within-role-route.txt`: note 2
|
||||||
|
|
||||||
|
The probe scripts run from a `dw/` directory beside `packages/bus/src`;
|
||||||
|
`pid1-reparent.mjs` and `within-role-route.mjs` use absolute scratch
|
||||||
|
paths.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
s2-manifest-ok
|
||||||
|
PASS actual S1 validate/resolve -> S2 normalize/start -> socket claim/message; scoped fixture token callback; launch classification
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
readProcess 8357 ok alacritty
|
||||||
|
readProcess 4295 ok Hyprland
|
||||||
|
readProcess 4152 ok /usr/bin/start-hyprland
|
||||||
|
readProcess 4136 ok /usr/lib/plasmalogin-helper
|
||||||
|
readProcess 2173 ok /usr/bin/plasmalogin
|
||||||
|
verifyHuman from a real alacritty ancestry: {"business":"demo"}
|
||||||
|
readProcess(1) /usr/lib/systemd/systemd env null uid 0
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { mkdtempSync } from 'node:fs'; import { join } from 'node:path';
|
||||||
|
import { Store } from '/home/jwoltje/darkwing-scratch/s2-r2/packages/bus/src/store.mjs';
|
||||||
|
import { Broker } from '/home/jwoltje/darkwing-scratch/s2-r2/packages/bus/src/broker.mjs';
|
||||||
|
const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'cto' }, roles: { cto: { authority: { withinRole: ['message.send', 'task.create'], crossRole: [] } } } } };
|
||||||
|
const store = new Store(mkdtempSync('/home/jwoltje/darkwing-scratch/tmp/wr-')); const b = new Broker({ store, businesses });
|
||||||
|
const c = b.bindLaunch({ business: 'demo', role: 'cto', run: 'r', harness: 'pi' }); b.request(c, { verb: 'role.claim', args: {} });
|
||||||
|
const d = b.request(c, { verb: 'decision.raise', args: { action: 'task.create', question: 'q', options: [{ key: 'yes', text: 'y' }, { key: 'no', text: 'n' }], recommendation: 'yes', blocking: false, choice: 'yes' } });
|
||||||
|
console.log('within-role class', d.class, 'route_to', d.route_to, 'status', d.status ?? '(n/a)');
|
||||||
|
store.close();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
within-role class within-role route_to cto status (n/a)
|
||||||
|
mutant: within-role class within-role route_to human status (n/a)
|
||||||
Reference in New Issue
Block a user