From d218e14bd988154c6ffdce37e66cfb90cb2f4822 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Wed, 21 Oct 2026 13:55:41 -0500 Subject: [PATCH] gander: record row 196 rebase gate stop and row 252 design --- agents/gander/work/queue-196/PROGRESS.md | 1 + agents/gander/work/queue-196/r2/REBASE.md | 55 + agents/gander/work/queue-196/r2/batch.log | 23 + agents/gander/work/queue-196/r2/busy.py | 15 + agents/gander/work/queue-196/r2/driver.mjs | 66 ++ .../gander/work/queue-196/r2/mutants/M1.txt | 54 + .../gander/work/queue-196/r2/mutants/M10.txt | 33 + .../gander/work/queue-196/r2/mutants/M11.txt | 33 + .../gander/work/queue-196/r2/mutants/M12.txt | 31 + .../gander/work/queue-196/r2/mutants/M13.txt | 55 + .../gander/work/queue-196/r2/mutants/M14.txt | 93 ++ .../gander/work/queue-196/r2/mutants/M15.txt | 40 + .../gander/work/queue-196/r2/mutants/M16.txt | 69 ++ .../gander/work/queue-196/r2/mutants/M17.txt | 67 ++ .../gander/work/queue-196/r2/mutants/M18.txt | 42 + .../gander/work/queue-196/r2/mutants/M19.txt | 24 + .../gander/work/queue-196/r2/mutants/M2.txt | 32 + .../gander/work/queue-196/r2/mutants/M20.txt | 53 + .../gander/work/queue-196/r2/mutants/M21.txt | 32 + .../gander/work/queue-196/r2/mutants/M22.txt | 33 + .../gander/work/queue-196/r2/mutants/M23.txt | 45 + .../gander/work/queue-196/r2/mutants/M3.txt | 32 + .../gander/work/queue-196/r2/mutants/M4.txt | 31 + .../gander/work/queue-196/r2/mutants/M5.txt | 35 + .../gander/work/queue-196/r2/mutants/M6.txt | 31 + .../gander/work/queue-196/r2/mutants/M7.txt | 30 + .../gander/work/queue-196/r2/mutants/M8.txt | 33 + .../gander/work/queue-196/r2/mutants/M9.txt | 33 + .../work/queue-196/r2/mutants/results.json | 309 +++++ .../work/queue-196/r2/patch-id-after.txt | 1 + .../work/queue-196/r2/patch-id-before.txt | 1 + agents/gander/work/queue-196/r2/paths.txt | 2 + .../gander/work/queue-196/r2/range-diff.txt | 1036 +++++++++++++++++ agents/gander/work/queue-252/DESIGN.md | 229 ++++ 34 files changed, 2699 insertions(+) create mode 100644 agents/gander/work/queue-196/r2/REBASE.md create mode 100644 agents/gander/work/queue-196/r2/batch.log create mode 100644 agents/gander/work/queue-196/r2/busy.py create mode 100644 agents/gander/work/queue-196/r2/driver.mjs create mode 100644 agents/gander/work/queue-196/r2/mutants/M1.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M10.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M11.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M12.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M13.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M14.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M15.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M16.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M17.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M18.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M19.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M2.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M20.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M21.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M22.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M23.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M3.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M4.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M5.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M6.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M7.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M8.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/M9.txt create mode 100644 agents/gander/work/queue-196/r2/mutants/results.json create mode 100644 agents/gander/work/queue-196/r2/patch-id-after.txt create mode 100644 agents/gander/work/queue-196/r2/patch-id-before.txt create mode 100644 agents/gander/work/queue-196/r2/paths.txt create mode 100644 agents/gander/work/queue-196/r2/range-diff.txt create mode 100644 agents/gander/work/queue-252/DESIGN.md diff --git a/agents/gander/work/queue-196/PROGRESS.md b/agents/gander/work/queue-196/PROGRESS.md index 9cb06a5d1..7fb41a9ef 100644 --- a/agents/gander/work/queue-196/PROGRESS.md +++ b/agents/gander/work/queue-196/PROGRESS.md @@ -18,3 +18,4 @@ sage-1425: complete static self-review before slot, document refused caller outc Static review complete: peer/helper lifecycle, runtime, verifier walk, tester socket, Console spawn, Unix client, release/recovery/preflight and fixtures inspected. Found release path preflight missing before stop: long-path fixture RED -> GREEN with preflight; unsafe parent also refuses before any stop. release-path-{red,green}.txt. M29 prepared. Restored unchanged legacy tester-port-invalid wording. Prepared batch now M1-M29, still unexecuted. No further implementation changes planned before Sage's go. sage-1464 (2026-10-20T20:40Z): renewed bus/WebUI/CLI and M1-M29 slot is fifth, after rows 192, 201, 204 and 200. Continue holding until Sage's go. Fleet notice #426(a), mos-1086, live 20:32:55Z: no branch deletion; any rebase must use a new branch name; leave pr-merge.sh -d off. No rebase or deletion is planned for this row. sage-2051 / mos-1193 (2026-10-21T18:45Z) explicitly orders committing and fast-forward pushing the current row196 build before its deferred full verification, then rebasing onto a36523713 on new gander/row196-r2. This supersedes the earlier no-product-commit-before-suite gate for this preservation commit only. Full acceptance remains pending. No tests until Sage says clear. Subsequent batch order: M1-M29, bus, WebUI, CLI, every whole scripts/test-*.sh; TMPDIR must be created with mktemp under /mnt/storage/scratch/tmp, never /tmp. Preserve the old branch. Row252 design may proceed while waiting; no row252 code before Webby approves. +sage-2058 clear: clean detached 64539c96b worktree, short scratch TMPDIR, busy && before starts, pipefail and tee, mutation restoration hash and Git-status checks. M1-M22 killed; M23 rejected by broad runtime SyntaxError classification despite passing node --check, 3 pass/2 fail in killer. Stopped, restored clean, slot returned gander-0031 18:51:50Z. No suites started. sage-2063 calls stop 1; driver fix separately committed, no rerun before next clear. r2/REBASE.md records all ten conflict resolutions. Row252 design drafted, no code. diff --git a/agents/gander/work/queue-196/r2/REBASE.md b/agents/gander/work/queue-196/r2/REBASE.md new file mode 100644 index 000000000..208ee427d --- /dev/null +++ b/agents/gander/work/queue-196/r2/REBASE.md @@ -0,0 +1,55 @@ +# Row 196 r2 rebase and gate record + +Preservation: caf5c9b71b7ffe3e1a195c50a96439cb1d94bb0f, fast-forward pushed +on gander/row196 per sage-2051 / mos-1193. Stat against 4d26003f7: +93 files changed, 9940 insertions(+), 178 deletions(-). +Rebased candidate: 64539c96bb9eb34ff0cce6ab2e04410041a2a31e, on a36523713, +pushed as new gander/row196-r2. Old branch preserved. No force push or deletion. +range-diff.txt and patch-id-{before,after}.txt hold the exact comparison. +Stable patch ids differ: 294d45c6c8494e83e4276a8c554fcbe85674f3d0 before; +fb96c71db3715d7ea766b278377c4f215ba1537b after. + +## Merge reasoning for the ten conflicted files + +| File | Resolution and reason | +|---|---| +| BUILD-LOG.md | Retain upstream append-only phases followed by row196 entries. No historical entry removed or rewritten. | +| docs/TOOLS.md | Combine socket/legacy tester flags with upstream --sso and --no-send usage. Keep new operator modes. | +| packages/bus/src/runtime.mjs | Keep upstream host executor imports, run-directory sweep, grant reconciliation and host API. Add interpreter receipt and peer reader before listen, shared verifier and per-request binds. No second verifier. | +| packages/bus/src/server.mjs | Keep upstream HOST_VERBS/HOSTS_VERBS and host argument. Merge user proof and peer checks, with idempotent transient-cap release on host async completion too, avoiding duplicate disconnect. | +| packages/cli/tests/stack-release.test.mjs | Keep upstream extracted fixture import and all new upstream tests, append the whole row196 case outside upstream's closing block. Move row196 optional long data-root parameter into stack-release-fixture.mjs, where mk now lives. | +| packages/webui/src/cli.mjs | Retain SSO parsing, startup, summary and shutdown; replace only tester TCP with private Unix transport. Preserve testerNoSignIn when SSO is on. | +| packages/webui/src/serve.mjs | Retain SSO session/Host checks and signer. Add socket-only tester transport and fixed logical origin. Tester refuses authorization, session/login cookies and provider headers; it never uses the SSO signer. | +| packages/webui/tests/proof-cli.test.mjs | Retain upstream lane imports while moving the tester assertion to Unix HTTP. Close the fixture's socket before spawning the same-root CLI. No product relay. | +| scripts/stack-release.sh | Retain external-unit protocol, instance flags, SSO transformation, no-send and grant/config checks. Run socket migration on BASE_CONSOLE after sso_base. Preserve interpreter gate after external start-floor calculation. New socket flag participates in proof/SSO insertion and rollback carry, avoiding ordering and TCP-fallback regressions. External callers cannot use the new render-changing flag. | +| scripts/webui-service.sh | Retain instance selection, PORT_SET and SSO flag/status handling; add socket support checks and mode conflict. Status regex recognizes either legacy tester port or socket after send. | + +Follow-through outside the conflict blocks: migrate the new SSO tester fixture to +Unix HTTP while retaining every sign-in refusal; adapt M15 to BASE_CONSOLE; retain +approved DESIGN.md/PLAN.md, absent on the new base. No tests before Sage's clear. +The two reviewed row164 file hashes remain unchanged by this rebase. + +## First r2 slot, sage-2058 + +Clean detached worktree pinned to 64539c96b, linked node_modules. paths.txt +records it and the short mktemp TMPDIR under /mnt/storage/scratch/tmp. Product +tracked files stayed clean; node_modules is the expected untracked symlink. +The external driver creates an isolated Git snapshot for mutations and checks +both the original file hash and empty Git status after every restoration. +Every test start uses busy.py gated with &&, pipefail and tee. busy.py retains +canonical busy detections and excludes only its own process ancestors, which +would otherwise match the driver's command text. No other test process is +whitelisted. + +M1-M22 killed. M23 syntax preflight passed; the whole interpreter killer file +reported 3 pass/2 fail. The old driver rejected its runtime JSON SyntaxError as +invalid, so stopped before M24. M23 also made the FIFO child hit its own one +second timeout. No golden package or shell suite started. All 23 restorations +passed hash and clean-status checks. batch.log and mutants/results.json retain +the original classification, not retrospectively edited. Slot handed back in +gander-0031 at 2026-10-21T18:51:50Z. Sage calls this stop 1 (sage-2063). + +Next run requires Sage's next clear, after Morgana's row243 singles. M1-M22 +stand under sage-2063, pending the separate driver correction. Rerun M23-M29, +then whole bus/WebUI/CLI suites and all ten whole scripts/test-*.sh. A second +stop goes to Sage before any rerun; a third goes to Mos. No acceptance claim. diff --git a/agents/gander/work/queue-196/r2/batch.log b/agents/gander/work/queue-196/r2/batch.log new file mode 100644 index 000000000..6ea45c55f --- /dev/null +++ b/agents/gander/work/queue-196/r2/batch.log @@ -0,0 +1,23 @@ +M1 KILLED +M2 KILLED +M3 KILLED +M4 KILLED +M5 KILLED +M6 KILLED +M7 KILLED +M8 KILLED +M9 KILLED +M10 KILLED +M11 KILLED +M12 KILLED +M13 KILLED +M14 KILLED +M15 KILLED +M16 KILLED +M17 KILLED +M18 KILLED +M19 KILLED +M20 KILLED +M21 KILLED +M22 KILLED +M23 SURVIVED/INVALID diff --git a/agents/gander/work/queue-196/r2/busy.py b/agents/gander/work/queue-196/r2/busy.py new file mode 100644 index 000000000..f57624fda --- /dev/null +++ b/agents/gander/work/queue-196/r2/busy.py @@ -0,0 +1,15 @@ +import os,subprocess,sys +ancestors=set();pid=os.getpid() +while pid>1 and pid not in ancestors: + ancestors.add(pid) + try:pid=int(open('/proc/%s/stat'%pid).read().rsplit(')',1)[1].split()[1]) + except (OSError,ValueError):break +r=subprocess.run(['bash','/mnt/storage/scratch/sage-busy-now.sh'],capture_output=True,text=True) +other=[] +for line in r.stdout.splitlines(): + if line.startswith('BUSY '): + try:pid=int(line.split()[1].rstrip(':')) + except ValueError:other.append(line);continue + if pid not in ancestors:other.append(line) +if other:print('\n'.join(other));sys.exit(1) +print('CLEAR (current driver ancestors excluded)') diff --git a/agents/gander/work/queue-196/r2/driver.mjs b/agents/gander/work/queue-196/r2/driver.mjs new file mode 100644 index 000000000..6f80b6546 --- /dev/null +++ b/agents/gander/work/queue-196/r2/driver.mjs @@ -0,0 +1,66 @@ +// Authorized batch only. Mutate isolated scratch copies, never the candidate or installed tree. +import {mkdtempSync,cpSync,readFileSync,writeFileSync,rmSync,symlinkSync,existsSync,mkdirSync} from 'node:fs'; +import {tmpdir} from 'node:os';import {join,resolve} from 'node:path'; +import {spawnSync,execFileSync} from 'node:child_process';import {createHash} from 'node:crypto'; +import {fileURLToPath} from 'node:url'; +const root='/mnt/storage/scratch/tmp/g196-tree-tn651z'; +const out='/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-196/r2/mutants';mkdirSync(out,{recursive:true}); +const scratch=mkdtempSync(join(tmpdir(),'g196-mut-')); +const p='packages/bus/src/principal.mjs',h='packages/bus/src/human.mjs',w='packages/webui/src/tester-socket.mjs',s='packages/bus/src/server.mjs',peer='packages/bus/src/peercred.mjs'; +const principal='packages/bus/tests/principal.test.mjs',socket='packages/webui/tests/tester-socket.test.mjs',transport='packages/bus/tests/peer-transport.test.mjs',interpreter='packages/bus/tests/interpreter.test.mjs'; +const release=['--test-name-pattern=row196 explicit socket','packages/cli/tests/stack-release.test.mjs']; +const specs=[ + ['M1', 'packages/webui/src/serve.mjs','tester uses TCP',socket, text=>text.replace('if (tester) return bindTesterSocket(server, testerSocket);',"if (tester) return new Promise(r=>server.listen(0,'127.0.0.1',()=>r(server)));" )], + ['M2',w,'socket mode widened',socket,t=>t.replace('chmodSync(temporary, 0o600)','chmodSync(temporary, 0o666)')], + ['M3','packages/webui/src/serve.mjs','sign-in accepted on tester',socket,t=>t.replace("if (tester && (Object.hasOwn(req.headers, 'authorization')", "if (false && (Object.hasOwn(req.headers, 'authorization')")], + ['M4',p,'copied PID accepted',principal,t=>t.replace('peer.pid !== proof.pid','false')], + ['M5',peer,'helper parse error ignored','packages/bus/tests/peercred.test.mjs',t=>t.replace('} catch { finish(true); }','} catch { finish(false, {pid:2,uid:0,gid:0}); }')], + ['M6',p,'foreign uid accepted',principal,t=>t.replace('peer.uid !== process.getuid()','false')], + ['M7',w,'unsafe parent accepted',socket,t=>t.replace('(dir.mode & 0o077)','false')], + ['M8',h,'agent environment walk skipped',principal,t=>t.replace('current.env !== null && MARKERS.some','false && MARKERS.some')], + ['M9',h,'leaf identity not reread',principal,t=>t.replace('if (again.startTime !== proof.startTime || again.ppid !== p.ppid)','if (false)')], + ['M10',p,'Console direct-parent check removed',principal,t=>t.replace("if (parent.uid !== peer.uid || parent.argv[1] !== resolve(paths.console) || parent.argv[2] !== 'serve')",'if (false)')], + ['M11',p,'Console basename accepted',principal,t=>t.replace("import { resolve }", "import { resolve, basename }").replace('parent.argv[1] !== resolve(paths.console)','basename(parent.argv[1] ?? "") !== basename(paths.console)')], + ['M12',h,'unknown proof identity accepted',principal,t=>t.replace("Object.keys(proof).some((k) => !['pid', 'startTime', 'nonce', 'business'].includes(k))",'false')], + ['M13',s,'transient cap leaked',transport,t=>t.replace('broker.disconnect(boundCap)','void(boundCap)')], + ['M14',p,'tester walk used for user',principal,t=>t.replace("verifyHuman(checked, { cliPath: paths[kind], launches, readProcess,\n nonceKey: kind === 'user' ? 'MOSAIC_BUS_USER_NONCE' : 'MOSAIC_BUS_CLI_NONCE' })",'verifyTester(checked, {cliPath:paths[kind],launches,readProcess,readNonce:()=>checked.nonce})')], + ['M15','scripts/stack-release.sh','TCP retained during conversion',release,t=>t.replace('RUN_CONSOLE=("${without_tester[@]}" --tester-socket)','RUN_CONSOLE=("${BASE_CONSOLE[@]}" --tester-socket)')], + ['M16','scripts/stack-release.sh','TCP reopened on downgrade',release,t=>t.replace('RUN_CONSOLE=("${without_tester[@]}") DROP_TESTER="--tester-socket','RUN_CONSOLE=("${without_tester[@]}" --tester-port 7333) DROP_TESTER="--tester-socket')], + ['M17','packages/bus/src/broker.mjs','tester switch ignored','packages/bus/tests/tester.test.mjs',t=>t.replace("if (this.#business(record.business).vars?.['bus.tester'] !== true)",'if (false)')], + ['M18',p,'user kind becomes owner',principal,t=>t.replace("return { kind: 'user', business, id: proof.user }","return { kind: 'owner', business, id: proof.user }")], + ['M19','packages/cli/src/tester-http.mjs','selected instance root ignored',socket,t=>t.replace('socketPath: testerSocketPath(dataRoot)','socketPath: testerSocketPath(dataRoot + "/wrong-instance")')], + ['M20',peer,'helper gets another pending socket','packages/bus/tests/peercred.test.mjs',t=>t.replace('export function createPeerReader','function originalPeerReader')+`\nexport function createPeerReader(options){const base=originalPeerReader(options),queue=[];let scheduled=false;return{read(socket){return new Promise((resolve,reject)=>{queue.push({socket,resolve,reject});if(!scheduled){scheduled=true;queueMicrotask(()=>{scheduled=false;const jobs=queue.splice(0);jobs.forEach((job,i)=>base.read(jobs[(i+1)%jobs.length].socket).then(job.resolve,job.reject));});}});},close(){base.close();}};}\n`], + ['M21','packages/bus/src/interpreter.mjs','user-owned interpreter accepted',interpreter,t=>t.replace('s.uid !== 0 || (s.mode & 0o022)','false || (s.mode & 0o022)')], + ['M22',s,'cap requests invoke helper',transport,t=>t.replace(' if (kind) {',' if (r.cap) await peers.read(socket);\n if (kind) {')], + ['M23','packages/bus/src/interpreter.mjs','restart retains stale identity',interpreter,t=>t.replace('export function recordInterpreterStart(directory, record) {','export function recordInterpreterStart(directory, record) {\n try { readFileSync(join(directory,"peercred-starts.jsonl")); return; } catch {}')], + ['M24',w,'listener binds final path directly',socket,t=>t.replace("const temporary = join(parent, '.' + randomBytes(4).toString('hex') + '.s');",'const temporary = path;')], + ['M25','packages/bus/src/interpreter.mjs','FIFO receipt blocks before type refusal',['--test-name-pattern=non-regular startup receipt',interpreter],t=>t.replaceAll(' | constants.O_NONBLOCK','')], + ['M26',peer,'missing helper accepted at startup',['--test-name-pattern=missing or non-regular peer helper','packages/bus/tests/peercred.test.mjs'],t=>t.replace('if (!s.isFile() || s.isSymbolicLink()) throw Error();','if (false) throw Error();').replace('catch { throw refused(); }','catch {}')], + ['M27',peer,'shutdown returns before helpers are reaped',['--test-name-pattern=shutdown waits','packages/bus/tests/peercred.test.mjs'],t=>t.replace(" await Promise.all([...children].map(child => new Promise(resolve => {\n child.once('close', resolve);\n child.kill('SIGKILL');\n })));"," for (const child of children) child.kill('SIGKILL');")], + ['M28','scripts/stack-release.sh','proof pair placed after tester socket flag',release,t=>t.replace('--send | --tester-port | --tester-socket) break','--send | --tester-port) break')], + ['M29','scripts/stack-release.sh','unsafe tester path reaches service stop',release,t=>t.split('\n').filter(line=>!line.includes('|| check_refuse tester-socket-path')).join('\n')], +]; +const hash=x=>createHash('sha256').update(x).digest('hex'); +const results=[]; +try { + for(const name of ['packages','scripts','adapters','roles','contracts','RELEASE','package.json','package-lock.json','.gitignore'])if(existsSync(join(root,name)))cpSync(join(root,name),join(scratch,name),{recursive:true,filter:src=>!src.includes('/node_modules/')&&!src.endsWith('/node_modules')}); + symlinkSync(join(root,'node_modules'),join(scratch,'node_modules')); + for(const args of [['init','-q'],['add','.'],['-c','user.name=fixture','-c','user.email=fixture@invalid','-c','commit.gpgsign=false','-c','core.hooksPath=/dev/null','commit','-qm','candidate snapshot']])execFileSync('git',args,{cwd:scratch}); + for(const [id,file,description,test,mutate] of specs) { + const at=join(scratch,file),original=readFileSync(at,'utf8'),before=hash(original),changed=mutate(original); + if(changed===original)throw Error(id+' did not change bytes'); + writeFileSync(at,changed); + const syntax=spawnSync(file.endsWith('.sh')?'bash':process.execPath,[file.endsWith('.sh')?'-n':'--check',at],{encoding:'utf8'}); + if(syntax.status!==0)throw Error(id+' invalid mutation: '+syntax.stderr); + const args=['--test',...(Array.isArray(test)?test:[test])]; + const run=spawnSync('bash',['-c','set -o pipefail; python3 "$G196_BUSY" && "$G196_NODE" "$@" 2>&1 | tee "$G196_LOG"','--',...args],{cwd:scratch,env:{...process.env,G196_NODE:process.execPath,G196_LOG:join(out,id+'.txt')},encoding:'utf8',timeout:60000,maxBuffer:2*1024*1024}); + const log=(run.stdout??'')+(run.stderr??'');writeFileSync(join(out,id+'.txt'),log); + writeFileSync(at,original);const clean=execFileSync('git',['status','--porcelain'],{cwd:scratch,encoding:'utf8'});if(clean.trim())throw Error(id+' dirty restored tree: '+clean);if(hash(readFileSync(at))!==before)throw Error(id+' restore hash failed'); + const failed=/[ℹ#]\s*(?:fail|tests failed)\s+[1-9]/.test(log) || /✖/.test(log); + const killed=run.status!==0&&failed&&!/SyntaxError|ERR_MODULE_NOT_FOUND/.test(log); + const result={id,description,test,args,status:run.status,signal:run.signal,killed,restoredSha256:before};results.push(result); + writeFileSync(join(out,'results.json'),JSON.stringify(results,null,2)+'\n'); + console.log(id+' '+(killed?'KILLED':'SURVIVED/INVALID')); + if(!killed) {process.exitCode=1;break;} + } +} finally {rmSync(scratch,{recursive:true,force:true});} diff --git a/agents/gander/work/queue-196/r2/mutants/M1.txt b/agents/gander/work/queue-196/r2/mutants/M1.txt new file mode 100644 index 000000000..0130937a3 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M1.txt @@ -0,0 +1,54 @@ +CLEAR (current driver ancestors excluded) +✖ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (49.694568ms) +✖ private tester refuses symlink, unsafe parent and long path without repairs (1.885024ms) +✔ tester cleanup must leave a replacement inode untouched (2.384441ms) +ℹ tests 3 +ℹ suites 0 +ℹ pass 1 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 121.753018 + +✖ failing tests: + +test at packages/webui/tests/tester-socket.test.mjs:13:1 +✖ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (49.694568ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + actual - expected + + + { + + address: '127.0.0.1', + + family: 'IPv4', + + port: 41395 + + } + - '/mnt/storage/scratch/tmp/gJ3j/g196-w-L7UCok/console/tester.sock' + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/webui/tests/tester-socket.test.mjs:16:9) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: { address: '127.0.0.1', family: 'IPv4', port: 41395 }, + expected: '/mnt/storage/scratch/tmp/gJ3j/g196-w-L7UCok/console/tester.sock', + operator: 'strictEqual', + diff: 'simple' + } + +test at packages/webui/tests/tester-socket.test.mjs:26:1 +✖ private tester refuses symlink, unsafe parent and long path without repairs (1.885024ms) + AssertionError [ERR_ASSERTION]: unsafe tester bind must refuse + at refused (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/webui/tests/tester-socket.test.mjs:11:9) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/webui/tests/tester-socket.test.mjs:29:2) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: '==', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M10.txt b/agents/gander/work/queue-196/r2/mutants/M10.txt new file mode 100644 index 000000000..83ccd233f --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M10.txt @@ -0,0 +1,33 @@ +CLEAR (current driver ancestors excluded) +✔ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.647432ms) +✖ user requires its own nonce and full walk plus immediate same-tree Console parent (0.71001ms) +✔ tester deliberately admits agent parents but rejects managed runs and another nonce (0.285297ms) +✔ human leaf identity is reread after walking the parents (0.167787ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 59.125304 + +✖ failing tests: + +test at packages/bus/tests/principal.test.mjs:22:1 +✖ user requires its own nonce and full walk plus immediate same-tree Console parent (0.71001ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:27:10) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /human-required/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M11.txt b/agents/gander/work/queue-196/r2/mutants/M11.txt new file mode 100644 index 000000000..1d64c12c3 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M11.txt @@ -0,0 +1,33 @@ +CLEAR (current driver ancestors excluded) +✔ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.685528ms) +✖ user requires its own nonce and full walk plus immediate same-tree Console parent (0.680266ms) +✔ tester deliberately admits agent parents but rejects managed runs and another nonce (0.286586ms) +✔ human leaf identity is reread after walking the parents (0.150943ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 51.004525 + +✖ failing tests: + +test at packages/bus/tests/principal.test.mjs:22:1 +✖ user requires its own nonce and full walk plus immediate same-tree Console parent (0.680266ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:27:10) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /human-required/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M12.txt b/agents/gander/work/queue-196/r2/mutants/M12.txt new file mode 100644 index 000000000..49d344098 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M12.txt @@ -0,0 +1,31 @@ +CLEAR (current driver ancestors excluded) +✖ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.927254ms) +✔ user requires its own nonce and full walk plus immediate same-tree Console parent (0.491561ms) +✔ tester deliberately admits agent parents but rejects managed runs and another nonce (0.278262ms) +✔ human leaf identity is reread after walking the parents (0.159989ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 53.494457 + +✖ failing tests: + +test at packages/bus/tests/principal.test.mjs:15:1 +✖ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.927254ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:19:9) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.start (node:internal/test_runner/test:1262:17) + at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /human-required/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M13.txt b/agents/gander/work/queue-196/r2/mutants/M13.txt new file mode 100644 index 000000000..babcc597f --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M13.txt @@ -0,0 +1,55 @@ +CLEAR (current driver ancestors excluded) +✖ proof receives actual peer, cap traffic never invokes helper and transient cap disconnects (9.052406ms) +✔ proof helper error and disconnect during verification never dispatch or bind (1.997863ms) +✔ Client receives explicit proof refusal codes without dispatch, not outcome-unknown (43.098804ms) +✖ asynchronous proof calls release their transient session on success, failure and timeout (2.35303ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 2 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 114.087067 + +✖ failing tests: + +test at packages/bus/tests/peer-transport.test.mjs:11:1 +✖ proof receives actual peer, cap traffic never invokes helper and transient cap disconnects (9.052406ms) + AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value: + + assert.ok(disconnected.includes('transient')) + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/peer-transport.test.mjs:18:52) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: '==', + diff: 'simple' + } + +test at packages/bus/tests/peer-transport.test.mjs:46:1 +✖ asynchronous proof calls release their transient session on success, failure and timeout (2.35303ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + + actual - expected + + + [] + - [ + - 'success' + - ] + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/peer-transport.test.mjs:56:11) + 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: [], + expected: [ 'success' ], + operator: 'deepStrictEqual', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M14.txt b/agents/gander/work/queue-196/r2/mutants/M14.txt new file mode 100644 index 000000000..71d8d2e46 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M14.txt @@ -0,0 +1,93 @@ +CLEAR (current driver ancestors excluded) +✖ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (2.090667ms) +✖ user requires its own nonce and full walk plus immediate same-tree Console parent (0.460285ms) +✔ tester deliberately admits agent parents but rejects managed runs and another nonce (0.249211ms) +✖ human leaf identity is reread after walking the parents (0.26504ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 1 +ℹ fail 3 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 62.713304 + +✖ failing tests: + +test at packages/bus/tests/principal.test.mjs:15:1 +✖ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (2.090667ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /human-required/. Input: + + 'Error: tester-required' + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:19:9) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.start (node:internal/test_runner/test:1262:17) + at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: Error: tester-required + at refuse (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/src/tester.mjs:13:9) + at verifyTester (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/src/tester.mjs:40:5) + at verifyPrincipal (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/src/principal.mjs:31:24) + at file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:19:20 + at getActual (node:assert:580:5) + at strict.throws (node:assert:728:24) + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:19:9) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.start (node:internal/test_runner/test:1262:17), + expected: /human-required/, + operator: 'throws', + diff: 'simple' + } + +test at packages/bus/tests/principal.test.mjs:22:1 +✖ user requires its own nonce and full walk plus immediate same-tree Console parent (0.460285ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:27:10) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /human-required/, + operator: 'throws', + diff: 'simple' + } + +test at packages/bus/tests/principal.test.mjs:44:1 +✖ human leaf identity is reread after walking the parents (0.26504ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /human-required/. Input: + + 'Error: tester-required' + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:47:9) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: Error: tester-required + at refuse (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/src/tester.mjs:13:9) + at verifyTester (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/src/tester.mjs:70:5) + at verifyPrincipal (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/src/principal.mjs:31:24) + at file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:47:20 + at getActual (node:assert:580:5) + at strict.throws (node:assert:728:24) + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:47:9) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18), + expected: /human-required/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M15.txt b/agents/gander/work/queue-196/r2/mutants/M15.txt new file mode 100644 index 000000000..ff5a82210 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M15.txt @@ -0,0 +1,40 @@ +CLEAR (current driver ancestors excluded) +✖ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (3916.205226ms) +ℹ tests 1 +ℹ suites 0 +ℹ pass 0 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 4304.679862 + +✖ failing tests: + +test at packages/cli/tests/stack-release.test.mjs:2277:1 +✖ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (3916.205226ms) + AssertionError [ERR_ASSERTION]: stack-release: interpreter: {"path":"/usr/bin/python3","sha256":"fixture"} + stack-release: captured the rollback set: /mnt/storage/scratch/tmp/gJ3j/sr-hy3kBG/data/state/stack-rollback/20261021T185103Z-1263a5e3af50 + stack-release: drop --tester-port 7333 (replaced by private Unix socket); the target Console does not support it + stack-release: stopped both units + stack-release: stopped store schema: 3g + stack-release: backup: /mnt/storage/scratch/tmp/gJ3j/sr-hy3kBG/data/backups/stack/20261021T185103Z-1263a5e3af50 + stack-release: rolling back: install-console + stack-release: stopped both units + stack-release: put back the unit files and the wrapper from /mnt/storage/scratch/tmp/gJ3j/sr-hy3kBG/data/state/stack-rollback/20261021T185103Z-1263a5e3af50 + stack-release: gate passed on 901e0b11e9113fbb981db30e6fbe83635540d1f3 + stack-release: install-console; rolled back to 901e0b11e9113fbb981db30e6fbe83635540d1f3, which passed the gate + + + 1 !== 0 + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/cli/tests/stack-release.test.mjs:2290:78) + at async Test.run (node:internal/test_runner/test:1409:7) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: 1, + expected: 0, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M16.txt b/agents/gander/work/queue-196/r2/mutants/M16.txt new file mode 100644 index 000000000..14601b773 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M16.txt @@ -0,0 +1,69 @@ +CLEAR (current driver ancestors excluded) +✖ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (5111.05339ms) +ℹ tests 1 +ℹ suites 0 +ℹ pass 0 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 5664.207303 + +✖ failing tests: + +test at packages/cli/tests/stack-release.test.mjs:2277:1 +✖ row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket (5111.05339ms) + AssertionError [ERR_ASSERTION]: The input was expected to not match the regular expression /--tester-(socket|port)/. Input: + + '# Mosaic Console: `node packages/webui/src/cli.mjs serve`, one instance.\n' + + '# Rendered by scripts/webui-service.sh from packages/webui/systemd/; /mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/copies/0d30b8dfbb41b6ca567206d291451b1f5b7d13bd,\n' + + '# /usr/local/bin:/usr/bin:/bin, /usr/bin/node and the serve flags below are filled in at install time.\n' + + '# /mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/copies/0d30b8dfbb41b6ca567206d291451b1f5b7d13bd is the read-only copy `install --copy ID` names, and the flags\n' + + '# pass the canonical checkout as --checkout for the queue read (row 77).\n' + + '# Row 178: optional --proof-header NAME --proof-file PATH checks proxy proof;\n' + + '# the value is read once by serve and never carried in this unit.\n' + + '# Edit the template and reinstall; do not edit the installed unit.\n' + + '#\n' + + '# The Console reads the bus through the human transport, so whoever starts\n' + + "# this unit holds the human's capability. Jason starts and restarts it\n" + + '# (`systemctl --user enable --now mosaic-webui`), and so may Mos under lead\n' + + '# decision 102, which accepts that sends then enter the trail as Jason. Any\n' + + '# other agent starting it works around the human proof (lead decisions 62\n' + + '# and 94). The unit sets no MOSAIC_*, CLAUDE*, CODEX* or PI_* variable.\n' + + '[Unit]\n' + + 'Description=Mosaic Console\n' + + 'Documentation=file:///mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/copies/0d30b8dfbb41b6ca567206d291451b1f5b7d13bd/packages/webui/README.md\n' + + 'StartLimitIntervalSec=600\n' + + 'StartLimitBurst=5\n' + + '\n' + + '[Service]\n' + + 'Type=simple\n' + + 'WorkingDirectory=/mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/copies/0d30b8dfbb41b6ca567206d291451b1f5b7d13bd\n' + + 'Environment=PATH=/usr/local/bin:/usr/bin:/bin\n' + + '# The server binds 127.0.0.1 only. Usage, config and listen refusals\n' + + '# (a port already in use included) exit 2 and are not retried; fix the\n' + + '# flags or free the port, then start it again.\n' + + 'ExecStart=/usr/bin/node /mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/copies/0d30b8dfbb41b6ca567206d291451b1f5b7d13bd/packages/webui/src/cli.mjs serve --checkout /mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/repo --port 41915 --business acme --public-host console.example --auth-header x-user --allow-user jason --proof-header x-console-proof --proof-file /mnt/storage/scratch/tmp/gJ3j/sr-K2QFsd/proof.187 --tester-port 7333\n' + + 'Restart=on-failure\n' + + 'RestartSec=15\n' + + 'RestartPreventExitStatus=2\n' + + '# `systemctl --user stop` sends SIGTERM; the server stops accepting and\n' + + '# closes. Open connections that outlast TimeoutStopSec are killed.\n' + + 'KillSignal=SIGTERM\n' + + 'TimeoutStopSec=30\n' + + 'NoNewPrivileges=yes\n' + + '\n' + + '[Install]\n' + + 'WantedBy=default.target\n' + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/cli/tests/stack-release.test.mjs:2292:77) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: "# Mosaic Console: `node packages/webui/src/cli.mjs serve`, one instance.\n# Rendered by scripts/webui-service.sh from packages/webui/systemd/; /mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/copies/0d30b8dfbb41b6ca567206d291451b1f5b7d13bd,\n# /usr/local/bin:/usr/bin:/bin, /usr/bin/node and the serve flags below are filled in at install time.\n# /mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/copies/0d30b8dfbb41b6ca567206d291451b1f5b7d13bd is the read-only copy `install --copy ID` names, and the flags\n# pass the canonical checkout as --checkout for the queue read (row 77).\n# Row 178: optional --proof-header NAME --proof-file PATH checks proxy proof;\n# the value is read once by serve and never carried in this unit.\n# Edit the template and reinstall; do not edit the installed unit.\n#\n# The Console reads the bus through the human transport, so whoever starts\n# this unit holds the human's capability. Jason starts and restarts it\n# (`systemctl --user enable --now mosaic-webui`), and so may Mos under lead\n# decision 102, which accepts that sends then enter the trail as Jason. Any\n# other agent starting it works around the human proof (lead decisions 62\n# and 94). The unit sets no MOSAIC_*, CLAUDE*, CODEX* or PI_* variable.\n[Unit]\nDescription=Mosaic Console\nDocumentation=file:///mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/copies/0d30b8dfbb41b6ca567206d291451b1f5b7d13bd/packages/webui/README.md\nStartLimitIntervalSec=600\nStartLimitBurst=5\n\n[Service]\nType=simple\nWorkingDirectory=/mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/copies/0d30b8dfbb41b6ca567206d291451b1f5b7d13bd\nEnvironment=PATH=/usr/local/bin:/usr/bin:/bin\n# The server binds 127.0.0.1 only. Usage, config and listen refusals\n# (a port already in use included) exit 2 and are not retried; fix the\n# flags or free the port, then start it again.\nExecStart=/usr/bin/node /mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/copies/0d30b8dfbb41b6ca567206d291451b1f5b7d13bd/packages/webui/src/cli.mjs serve --checkout /mnt/storage/scratch/tmp/gJ3j/stack-release-bKqva6/repo --port 41915 --business acme --public-host console.example --auth-header x-user --allow-user jason --proof-header x-console-proof --proof-file /mnt/storage/scratch/tmp/gJ3j/sr-K2QFsd/proof.187 --tester-port 7333\nRestart=on-failure\nRestartSec=15\nRestartPreventExitStatus=2\n# `systemctl --user stop` sends SIGTERM; the server stops accepting and\n# closes. Open connections that outlast TimeoutStopSec are killed.\nKillSignal=SIGTERM\nTimeoutStopSec=30\nNoNewPrivileges=yes\n\n[Install]\nWantedBy=default.target\n", + expected: /--tester-(socket|port)/, + operator: 'doesNotMatch', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M17.txt b/agents/gander/work/queue-196/r2/mutants/M17.txt new file mode 100644 index 000000000..881e766d9 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M17.txt @@ -0,0 +1,67 @@ +CLEAR (current driver ancestors excluded) +✖ the tester binds only when the business sets bus.tester to exactly true (17.53619ms) +✔ a business may not name a role tester, so the tester address is never a role (17.82968ms) +✔ every verb outside the tester list refuses tester-refused and is recorded against the tester (36.71909ms) +✔ a tester message is from tester, needs no prefix, and writes no human.input or authority (14.532304ms) +✔ a tester message to a role no run holds refuses tester-target-not-running and stores nothing (10.145587ms) +✔ a tester message is refused while the run is only launched, and routes once it claims (12.444532ms) +✔ a human message to a role no run holds still waits and tells launch.by (10.928459ms) +✔ a role answers a tester only in reply to a tester message, and the answer never reaches the human (20.898774ms) +✔ the tester reads the human views a reader reads (24.490999ms) +✔ tester proof binds its own CLI, start time and nonce, refuses a managed run and allows an agent (1.344138ms) +✔ neither CLI's nonce proves the other: each proof is bound to its own CLI and nonce variable (0.406093ms) +✔ testerNonce reads only the tester's variable and gives null on any read error (0.198658ms) +✔ an envelope carries exactly one of cap, human and tester; no tester authenticator refuses (118.437146ms) +✖ tester-cli.mjs binds through the runtime when the switch is on, and refuses off or under a run marker (1234.579269ms) +ℹ tests 14 +ℹ suites 0 +ℹ pass 12 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 1628.145277 + +✖ failing tests: + +test at packages/bus/tests/tester.test.mjs:44:1 +✖ the tester binds only when the business sets bus.tester to exactly true (17.53619ms) + AssertionError [ERR_ASSERTION]: Missing expected exception: null + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/tester.test.mjs:47:12) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.start (node:internal/test_runner/test:1262:17) + at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: { code: 'tester-off' }, + operator: 'throws', + diff: 'simple' + } + +test at packages/bus/tests/tester.test.mjs:427:1 +✖ tester-cli.mjs binds through the runtime when the switch is on, and refuses off or under a run marker (1234.579269ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + + actual - expected + + { + + err: '', + + out: '[]', + + status: 0 + - err: 'tester-off', + - out: '', + - status: 2 + } + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/tester.test.mjs:448: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: { status: 0, out: '[]', err: '' }, + expected: { status: 2, out: '', err: 'tester-off' }, + operator: 'deepStrictEqual', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M18.txt b/agents/gander/work/queue-196/r2/mutants/M18.txt new file mode 100644 index 000000000..6a3894ce0 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M18.txt @@ -0,0 +1,42 @@ +CLEAR (current driver ancestors excluded) +✔ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.996624ms) +✖ user requires its own nonce and full walk plus immediate same-tree Console parent (1.266948ms) +✔ tester deliberately admits agent parents but rejects managed runs and another nonce (0.350185ms) +✔ human leaf identity is reread after walking the parents (0.203698ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 76.258279 + +✖ failing tests: + +test at packages/bus/tests/principal.test.mjs:22:1 +✖ user requires its own nonce and full walk plus immediate same-tree Console parent (1.266948ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + + actual - expected + + { + business: 'demo', + id: 'owner', + + kind: 'owner' + - kind: 'user' + } + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:24:9) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: { kind: 'owner', business: 'demo', id: 'owner' }, + expected: { kind: 'user', business: 'demo', id: 'owner' }, + operator: 'deepStrictEqual', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M19.txt b/agents/gander/work/queue-196/r2/mutants/M19.txt new file mode 100644 index 000000000..d4195d579 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M19.txt @@ -0,0 +1,24 @@ +CLEAR (current driver ancestors excluded) +✖ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (56.676653ms) +✔ private tester refuses symlink, unsafe parent and long path without repairs (1.634686ms) +✔ tester cleanup must leave a replacement inode untouched (1.97195ms) +ℹ tests 3 +ℹ suites 0 +ℹ pass 2 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 131.19279 + +✖ failing tests: + +test at packages/webui/tests/tester-socket.test.mjs:13:1 +✖ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (56.676653ms) + Error: connect ENOENT /mnt/storage/scratch/tmp/gJ3j/g196-w-6wAD6F/wrong-instance/console/tester.sock + at PipeConnectWrap.afterConnect [as oncomplete] (node:net:2021:16) { + errno: -2, + code: 'ENOENT', + syscall: 'connect', + address: '/mnt/storage/scratch/tmp/gJ3j/g196-w-6wAD6F/wrong-instance/console/tester.sock' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M2.txt b/agents/gander/work/queue-196/r2/mutants/M2.txt new file mode 100644 index 000000000..cdca6064c --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M2.txt @@ -0,0 +1,32 @@ +CLEAR (current driver ancestors excluded) +✖ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (40.421617ms) +✔ private tester refuses symlink, unsafe parent and long path without repairs (0.920109ms) +✖ tester cleanup must leave a replacement inode untouched (0.939648ms) +ℹ tests 3 +ℹ suites 0 +ℹ pass 1 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 102.876749 + +✖ failing tests: + +test at packages/webui/tests/tester-socket.test.mjs:13:1 +✖ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (40.421617ms) + Error: tester-socket-unsafe + at bindTesterSocket (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/webui/src/tester-socket.mjs:29:102) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/webui/tests/tester-socket.test.mjs:15:12) + at async Test.run (node:internal/test_runner/test:1409:7) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) + +test at packages/webui/tests/tester-socket.test.mjs:36:1 +✖ tester cleanup must leave a replacement inode untouched (0.939648ms) + Error: tester-socket-unsafe + at bindTesterSocket (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/webui/src/tester-socket.mjs:29:102) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/webui/tests/tester-socket.test.mjs:42:15) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) diff --git a/agents/gander/work/queue-196/r2/mutants/M20.txt b/agents/gander/work/queue-196/r2/mutants/M20.txt new file mode 100644 index 000000000..a7628168f --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M20.txt @@ -0,0 +1,53 @@ +CLEAR (current driver ancestors excluded) +✖ T2 eight proof callers and a racing copier bind the exact accepted socket, which survives helper exit (292.257839ms) +✔ helper output and failures fail closed including unsafe gid (2.680068ms) +✔ helper timeout, capacity and shutdown refuse without leaving children (25.751457ms) +✔ missing or non-regular peer helper refuses at construction before opening the bus (0.36369ms) +✖ shutdown waits for killed helper close before returning (0.370388ms) +ℹ tests 5 +ℹ suites 0 +ℹ pass 3 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 401.085642 + +✖ failing tests: + +test at packages/bus/tests/peercred.test.mjs:15:1 +✖ T2 eight proof callers and a racing copier bind the exact accepted socket, which survives helper exit (292.257839ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + false !== true + + at file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/peercred.test.mjs:56:12 + at Array.forEach () + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/peercred.test.mjs:52:11) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: 'strictEqual', + diff: 'simple' + } + +test at packages/bus/tests/peercred.test.mjs:98:1 +✖ shutdown waits for killed helper close before returning (0.370388ms) + AssertionError [ERR_ASSERTION]: shutdown must await the helper close event + + 0 !== 1 + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/peercred.test.mjs:106:9) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: 0, + expected: 1, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M21.txt b/agents/gander/work/queue-196/r2/mutants/M21.txt new file mode 100644 index 000000000..fec2ed7ea --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M21.txt @@ -0,0 +1,32 @@ +CLEAR (current driver ancestors excluded) +✖ system Python is a resolved root-owned ELF, never a shim or home interpreter (2.489468ms) +✔ real system interpreter is available and safely inspected (22.481127ms) +✔ restart receipt appends actual replacement identity and refuses unsafe files (9.851443ms) +✔ activation gate compares fresh startup receipt to the current interpreter identity (1.482676ms) +✔ non-regular startup receipt refuses promptly without waiting on a FIFO peer (33.784543ms) +ℹ tests 5 +ℹ suites 0 +ℹ pass 4 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 128.30192 + +✖ failing tests: + +test at packages/bus/tests/interpreter.test.mjs:15:1 +✖ system Python is a resolved root-owned ELF, never a shim or home interpreter (2.489468ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/interpreter.test.mjs:31:31) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.start (node:internal/test_runner/test:1262:17) + at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /interpreter-unsafe/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M22.txt b/agents/gander/work/queue-196/r2/mutants/M22.txt new file mode 100644 index 000000000..4f6cd163e --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M22.txt @@ -0,0 +1,33 @@ +CLEAR (current driver ancestors excluded) +✖ proof receives actual peer, cap traffic never invokes helper and transient cap disconnects (7.03352ms) +✔ proof helper error and disconnect during verification never dispatch or bind (1.790604ms) +✔ Client receives explicit proof refusal codes without dispatch, not outcome-unknown (42.381026ms) +✔ asynchronous proof calls release their transient session on success, failure and timeout (28.479209ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 135.242751 + +✖ failing tests: + +test at packages/bus/tests/peer-transport.test.mjs:11:1 +✖ proof receives actual peer, cap traffic never invokes helper and transient cap disconnects (7.03352ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + 2 !== 1 + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/peer-transport.test.mjs:20:9) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 2, + expected: 1, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M23.txt b/agents/gander/work/queue-196/r2/mutants/M23.txt new file mode 100644 index 000000000..eb306c820 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M23.txt @@ -0,0 +1,45 @@ +CLEAR (current driver ancestors excluded) +✔ system Python is a resolved root-owned ELF, never a shim or home interpreter (2.259714ms) +✔ real system interpreter is available and safely inspected (20.668185ms) +✖ restart receipt appends actual replacement identity and refuses unsafe files (2.992321ms) +✔ activation gate compares fresh startup receipt to the current interpreter identity (1.363356ms) +✖ non-regular startup receipt refuses promptly without waiting on a FIFO peer (1005.398641ms) +ℹ tests 5 +ℹ suites 0 +ℹ pass 3 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 1086.382437 + +✖ failing tests: + +test at packages/bus/tests/interpreter.test.mjs:38:1 +✖ restart receipt appends actual replacement identity and refuses unsafe files (2.992321ms) + SyntaxError: "undefined" is not valid JSON + at JSON.parse () + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/interpreter.test.mjs:49:48) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) + +test at packages/bus/tests/interpreter.test.mjs:69:1 +✖ non-regular startup receipt refuses promptly without waiting on a FIFO peer (1005.398641ms) + AssertionError [ERR_ASSERTION]: unsafe FIFO must refuse without blocking startup + + null !== 0 + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/interpreter.test.mjs:75:9) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: null, + expected: 0, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M3.txt b/agents/gander/work/queue-196/r2/mutants/M3.txt new file mode 100644 index 000000000..4bb7b2b76 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M3.txt @@ -0,0 +1,32 @@ +CLEAR (current driver ancestors excluded) +✖ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (59.338551ms) +✔ private tester refuses symlink, unsafe parent and long path without repairs (1.388654ms) +✔ tester cleanup must leave a replacement inode untouched (1.72767ms) +ℹ tests 3 +ℹ suites 0 +ℹ pass 2 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 128.502051 + +✖ failing tests: + +test at packages/webui/tests/tester-socket.test.mjs:13:1 +✖ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (59.338551ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + 200 !== 403 + + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/webui/tests/tester-socket.test.mjs:21:10) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 200, + expected: 403, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M4.txt b/agents/gander/work/queue-196/r2/mutants/M4.txt new file mode 100644 index 000000000..5468eaf17 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M4.txt @@ -0,0 +1,31 @@ +CLEAR (current driver ancestors excluded) +✖ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.691534ms) +✔ user requires its own nonce and full walk plus immediate same-tree Console parent (0.594758ms) +✔ tester deliberately admits agent parents but rejects managed runs and another nonce (0.274537ms) +✔ human leaf identity is reread after walking the parents (0.160418ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 52.34153 + +✖ failing tests: + +test at packages/bus/tests/principal.test.mjs:15:1 +✖ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.691534ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:18:77) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.start (node:internal/test_runner/test:1262:17) + at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /human-required/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M5.txt b/agents/gander/work/queue-196/r2/mutants/M5.txt new file mode 100644 index 000000000..74a68763e --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M5.txt @@ -0,0 +1,35 @@ +CLEAR (current driver ancestors excluded) +✔ T2 eight proof callers and a racing copier bind the exact accepted socket, which survives helper exit (252.611828ms) +✖ helper output and failures fail closed including unsafe gid (1.307507ms) +✔ helper timeout, capacity and shutdown refuse without leaving children (26.237379ms) +✔ missing or non-regular peer helper refuses at construction before opening the bus (0.252246ms) +✖ shutdown waits for killed helper close before returning (4.713775ms) +ℹ tests 5 +ℹ suites 0 +ℹ pass 3 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 376.324408 + +✖ failing tests: + +test at packages/bus/tests/peercred.test.mjs:74:1 +✖ helper output and failures fail closed including unsafe gid (1.307507ms) + AssertionError [ERR_ASSERTION]: Missing expected rejection. + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/peercred.test.mjs:77:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /peercred-required/, + operator: 'rejects', + diff: 'simple' + } + +test at packages/bus/tests/peercred.test.mjs:98:1 +✖ shutdown waits for killed helper close before returning (4.713775ms) + [AssertionError [ERR_ASSERTION]: Missing expected rejection.] { generatedMessage: false, code: 'ERR_ASSERTION', actual: undefined, expected: /peercred-required/, operator: 'rejects', diff: 'simple' } diff --git a/agents/gander/work/queue-196/r2/mutants/M6.txt b/agents/gander/work/queue-196/r2/mutants/M6.txt new file mode 100644 index 000000000..088a8c68d --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M6.txt @@ -0,0 +1,31 @@ +CLEAR (current driver ancestors excluded) +✖ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.939462ms) +✔ user requires its own nonce and full walk plus immediate same-tree Console parent (0.501573ms) +✔ tester deliberately admits agent parents but rejects managed runs and another nonce (0.285393ms) +✔ human leaf identity is reread after walking the parents (0.163302ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 56.13375 + +✖ failing tests: + +test at packages/bus/tests/principal.test.mjs:15:1 +✖ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.939462ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:18:77) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.start (node:internal/test_runner/test:1262:17) + at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /human-required/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M7.txt b/agents/gander/work/queue-196/r2/mutants/M7.txt new file mode 100644 index 000000000..ea52fa26c --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M7.txt @@ -0,0 +1,30 @@ +CLEAR (current driver ancestors excluded) +✔ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (87.784551ms) +✖ private tester refuses symlink, unsafe parent and long path without repairs (2.258513ms) +✔ tester cleanup must leave a replacement inode untouched (2.916815ms) +ℹ tests 3 +ℹ suites 0 +ℹ pass 2 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 190.892552 + +✖ failing tests: + +test at packages/webui/tests/tester-socket.test.mjs:26:1 +✖ private tester refuses symlink, unsafe parent and long path without repairs (2.258513ms) + AssertionError [ERR_ASSERTION]: unsafe tester bind must refuse + at refused (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/webui/tests/tester-socket.test.mjs:11:9) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/webui/tests/tester-socket.test.mjs:29:2) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: '==', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M8.txt b/agents/gander/work/queue-196/r2/mutants/M8.txt new file mode 100644 index 000000000..b6b93bd0f --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M8.txt @@ -0,0 +1,33 @@ +CLEAR (current driver ancestors excluded) +✔ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.760997ms) +✖ user requires its own nonce and full walk plus immediate same-tree Console parent (0.759143ms) +✔ tester deliberately admits agent parents but rejects managed runs and another nonce (0.301518ms) +✔ human leaf identity is reread after walking the parents (0.176796ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 63.261715 + +✖ failing tests: + +test at packages/bus/tests/principal.test.mjs:22:1 +✖ user requires its own nonce and full walk plus immediate same-tree Console parent (0.759143ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:27:10) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /human-required/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/M9.txt b/agents/gander/work/queue-196/r2/mutants/M9.txt new file mode 100644 index 000000000..a74661492 --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/M9.txt @@ -0,0 +1,33 @@ +CLEAR (current driver ancestors excluded) +✔ principal binds kernel peer PID and uid, maps owner, and never takes identity or role from body (1.588487ms) +✔ user requires its own nonce and full walk plus immediate same-tree Console parent (0.483775ms) +✔ tester deliberately admits agent parents but rejects managed runs and another nonce (0.301471ms) +✖ human leaf identity is reread after walking the parents (0.520477ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 58.566309 + +✖ failing tests: + +test at packages/bus/tests/principal.test.mjs:44:1 +✖ human leaf identity is reread after walking the parents (0.520477ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///mnt/storage/scratch/tmp/gJ3j/g196-mut-crpRhv/packages/bus/tests/principal.test.mjs:47:9) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /human-required/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/gander/work/queue-196/r2/mutants/results.json b/agents/gander/work/queue-196/r2/mutants/results.json new file mode 100644 index 000000000..31f63903f --- /dev/null +++ b/agents/gander/work/queue-196/r2/mutants/results.json @@ -0,0 +1,309 @@ +[ + { + "id": "M1", + "description": "tester uses TCP", + "test": "packages/webui/tests/tester-socket.test.mjs", + "args": [ + "--test", + "packages/webui/tests/tester-socket.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "da624f188c69e016ff82e1d1dcae1c579e68a57f0f25d773bb5d8bca56b80ee1" + }, + { + "id": "M2", + "description": "socket mode widened", + "test": "packages/webui/tests/tester-socket.test.mjs", + "args": [ + "--test", + "packages/webui/tests/tester-socket.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "1c1fe0c795adac97f28502564a75f26d5666d9be5ae51169f7dd5bb7741fad16" + }, + { + "id": "M3", + "description": "sign-in accepted on tester", + "test": "packages/webui/tests/tester-socket.test.mjs", + "args": [ + "--test", + "packages/webui/tests/tester-socket.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "da624f188c69e016ff82e1d1dcae1c579e68a57f0f25d773bb5d8bca56b80ee1" + }, + { + "id": "M4", + "description": "copied PID accepted", + "test": "packages/bus/tests/principal.test.mjs", + "args": [ + "--test", + "packages/bus/tests/principal.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "32ef39d80b523bdd586c29620d7a752c309c8e1aa70befc1414125468f3411dd" + }, + { + "id": "M5", + "description": "helper parse error ignored", + "test": "packages/bus/tests/peercred.test.mjs", + "args": [ + "--test", + "packages/bus/tests/peercred.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "29c88655e8b9aed8db604573c9213fd9f3c40ef35ff04f4eda8fbe8f1b8a32d7" + }, + { + "id": "M6", + "description": "foreign uid accepted", + "test": "packages/bus/tests/principal.test.mjs", + "args": [ + "--test", + "packages/bus/tests/principal.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "32ef39d80b523bdd586c29620d7a752c309c8e1aa70befc1414125468f3411dd" + }, + { + "id": "M7", + "description": "unsafe parent accepted", + "test": "packages/webui/tests/tester-socket.test.mjs", + "args": [ + "--test", + "packages/webui/tests/tester-socket.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "1c1fe0c795adac97f28502564a75f26d5666d9be5ae51169f7dd5bb7741fad16" + }, + { + "id": "M8", + "description": "agent environment walk skipped", + "test": "packages/bus/tests/principal.test.mjs", + "args": [ + "--test", + "packages/bus/tests/principal.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "0c6d96abe4607cba0b02b0638a2fc217ff8135bac9a47761a0ef015fbe9d3079" + }, + { + "id": "M9", + "description": "leaf identity not reread", + "test": "packages/bus/tests/principal.test.mjs", + "args": [ + "--test", + "packages/bus/tests/principal.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "0c6d96abe4607cba0b02b0638a2fc217ff8135bac9a47761a0ef015fbe9d3079" + }, + { + "id": "M10", + "description": "Console direct-parent check removed", + "test": "packages/bus/tests/principal.test.mjs", + "args": [ + "--test", + "packages/bus/tests/principal.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "32ef39d80b523bdd586c29620d7a752c309c8e1aa70befc1414125468f3411dd" + }, + { + "id": "M11", + "description": "Console basename accepted", + "test": "packages/bus/tests/principal.test.mjs", + "args": [ + "--test", + "packages/bus/tests/principal.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "32ef39d80b523bdd586c29620d7a752c309c8e1aa70befc1414125468f3411dd" + }, + { + "id": "M12", + "description": "unknown proof identity accepted", + "test": "packages/bus/tests/principal.test.mjs", + "args": [ + "--test", + "packages/bus/tests/principal.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "0c6d96abe4607cba0b02b0638a2fc217ff8135bac9a47761a0ef015fbe9d3079" + }, + { + "id": "M13", + "description": "transient cap leaked", + "test": "packages/bus/tests/peer-transport.test.mjs", + "args": [ + "--test", + "packages/bus/tests/peer-transport.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "830faa9a76150b59189cde395ec8deeb768e4b0d45ed262a57bb6fc6e459abb4" + }, + { + "id": "M14", + "description": "tester walk used for user", + "test": "packages/bus/tests/principal.test.mjs", + "args": [ + "--test", + "packages/bus/tests/principal.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "32ef39d80b523bdd586c29620d7a752c309c8e1aa70befc1414125468f3411dd" + }, + { + "id": "M15", + "description": "TCP retained during conversion", + "test": [ + "--test-name-pattern=row196 explicit socket", + "packages/cli/tests/stack-release.test.mjs" + ], + "args": [ + "--test", + "--test-name-pattern=row196 explicit socket", + "packages/cli/tests/stack-release.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "b916a2793311d21e3ce7117e9b5e0768e6e33c841fa3aa39fb7ffc9c5974acd2" + }, + { + "id": "M16", + "description": "TCP reopened on downgrade", + "test": [ + "--test-name-pattern=row196 explicit socket", + "packages/cli/tests/stack-release.test.mjs" + ], + "args": [ + "--test", + "--test-name-pattern=row196 explicit socket", + "packages/cli/tests/stack-release.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "b916a2793311d21e3ce7117e9b5e0768e6e33c841fa3aa39fb7ffc9c5974acd2" + }, + { + "id": "M17", + "description": "tester switch ignored", + "test": "packages/bus/tests/tester.test.mjs", + "args": [ + "--test", + "packages/bus/tests/tester.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "e6f33bac95a40c9d52d67f297eec361cd0f30da5d3c28057468ee5d56c3ae7fc" + }, + { + "id": "M18", + "description": "user kind becomes owner", + "test": "packages/bus/tests/principal.test.mjs", + "args": [ + "--test", + "packages/bus/tests/principal.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "32ef39d80b523bdd586c29620d7a752c309c8e1aa70befc1414125468f3411dd" + }, + { + "id": "M19", + "description": "selected instance root ignored", + "test": "packages/webui/tests/tester-socket.test.mjs", + "args": [ + "--test", + "packages/webui/tests/tester-socket.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "4bd7bdf0898844684ebf9444b110d2c5b6fa874b2681183e54f1f62f97823192" + }, + { + "id": "M20", + "description": "helper gets another pending socket", + "test": "packages/bus/tests/peercred.test.mjs", + "args": [ + "--test", + "packages/bus/tests/peercred.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "29c88655e8b9aed8db604573c9213fd9f3c40ef35ff04f4eda8fbe8f1b8a32d7" + }, + { + "id": "M21", + "description": "user-owned interpreter accepted", + "test": "packages/bus/tests/interpreter.test.mjs", + "args": [ + "--test", + "packages/bus/tests/interpreter.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "d5d98a9133a1b449acc48143fd559d890a470c6ea210b30b9b70e491e6cd855a" + }, + { + "id": "M22", + "description": "cap requests invoke helper", + "test": "packages/bus/tests/peer-transport.test.mjs", + "args": [ + "--test", + "packages/bus/tests/peer-transport.test.mjs" + ], + "status": 1, + "signal": null, + "killed": true, + "restoredSha256": "830faa9a76150b59189cde395ec8deeb768e4b0d45ed262a57bb6fc6e459abb4" + }, + { + "id": "M23", + "description": "restart retains stale identity", + "test": "packages/bus/tests/interpreter.test.mjs", + "args": [ + "--test", + "packages/bus/tests/interpreter.test.mjs" + ], + "status": 1, + "signal": null, + "killed": false, + "restoredSha256": "d5d98a9133a1b449acc48143fd559d890a470c6ea210b30b9b70e491e6cd855a" + } +] diff --git a/agents/gander/work/queue-196/r2/patch-id-after.txt b/agents/gander/work/queue-196/r2/patch-id-after.txt new file mode 100644 index 000000000..02734bac8 --- /dev/null +++ b/agents/gander/work/queue-196/r2/patch-id-after.txt @@ -0,0 +1 @@ +fb96c71db3715d7ea766b278377c4f215ba1537b 0000000000000000000000000000000000000000 diff --git a/agents/gander/work/queue-196/r2/patch-id-before.txt b/agents/gander/work/queue-196/r2/patch-id-before.txt new file mode 100644 index 000000000..20892702e --- /dev/null +++ b/agents/gander/work/queue-196/r2/patch-id-before.txt @@ -0,0 +1 @@ +294d45c6c8494e83e4276a8c554fcbe85674f3d0 0000000000000000000000000000000000000000 diff --git a/agents/gander/work/queue-196/r2/paths.txt b/agents/gander/work/queue-196/r2/paths.txt new file mode 100644 index 000000000..5dd9cf889 --- /dev/null +++ b/agents/gander/work/queue-196/r2/paths.txt @@ -0,0 +1,2 @@ +/mnt/storage/scratch/tmp/g196-tree-tn651z +/mnt/storage/scratch/tmp/gJ3j diff --git a/agents/gander/work/queue-196/r2/range-diff.txt b/agents/gander/work/queue-196/r2/range-diff.txt new file mode 100644 index 000000000..6fb8ebf2f --- /dev/null +++ b/agents/gander/work/queue-196/r2/range-diff.txt @@ -0,0 +1,1036 @@ +1: caf5c9b71 ! 1: 64539c96b gander: preserve row 196 human boundary build for rebase + @@ Commit message + + ## BUILD-LOG.md ## + @@ BUILD-LOG.md: Before: managed project/thread verbs had no Console conversation area. After: fi + - 2026-10-20 19:16:20 UTC | Gander row 196 #1694 design r2 | Before: Duckworth r1 requested concurrent helper-fd evidence (B1) and selected O1 with interpreter, proof-only and image conditions. After: design adds eight concurrent genuine proof processes plus a racing copier, M20 swapped socket and post-helper socket survival, root-owned system ELF pinning and activation evidence, safe gid validation, cap traffic without helpers, and unchanged P19 R2 text. Mos accepted socket-only tester access (mos-1015 via sage-1363); any authenticated browser relay is a separate row. Documentation coverage checks passed. No product code, suites or live action. + - + - 2026-10-20 19:21:19 UTC | Gander row 196 #1694 design approval and N5 | Before: Duckworth approved r2 (duckworth-0009, cb6d840df) with a nonblocking interpreter-upgrade note. After: r3 chooses startup safety revalidation, appends actual interpreter identity to bus/peercred-starts.jsonl and retains old activation evidence. Operator re-pin uses the next reviewed copy, respecting already-active refusal. Added task-by-task PLAN.md for inline implementation and independent manifest review. Documentation checks passed. No product code, suites or live action. + + 2026-10-21T01:28:05Z | Darkwing | row 232 #1734 installer I1 build, round 1 in review | Before: stack-release.sh managed only user units (systemctl --user), and stack-copy.sh had no bundle copy. After: --units external (adopt, activate, rollback --to, status, confirm, revert, gate --since, AWAITING-UNITS, stale-start, external-pm, --plan --expect-from) and stack-copy check --units external, copy --bundle FILE SHA, wrapper ID --create; user mode byte-identical to the base by test. First build 0af0c73c6 on 00d4cc1f6; rebased onto row 200 (cb5fdb064) as a0ffabc3f on darkwing/row232-r1; --sso refused under external (D9), row 200 config_checks run under external (D10). Gate on Sage's slot (sage-1618): cli 561/0, every scripts/test-*.sh green; 17/17 mutants killed. Results commit 84f1a91e6 (pushed, seat branch only). Manifest 7a3b4ea1 (9 paths), queue revs 2595-2596, request comment 29825, reviewer Filbert. Packet agents/darkwing/work/queue-232/BUILD.md. + + 2026-10-21T04:50:44Z | Darkwing | row 233 #1735 installer I3 build, round 1 in review | Before: root had no upgrade driver for root-owned units and no host run sheet; stack-release.sh had no rehearse --backup last and no external plan refusals. After: scripts/stack-upgrade.sh upgrade and rollback (r6 A1-A5, A2f, R1-R4) with 20 tests, docs/guides/host-run-sheet.md, TOOLS.md lines, and on Sage's rulings stack-release.sh --backup last, BACKUP_STORE and the --plan refusals open-activation and migrate-required (3 tests). Base e65a3ab4d (row 232's landing). Gate on Sage's slot (sage-1704) on d99166ade: cli 616/0, every scripts/test-*.sh green, 1201/0 summed by awk; m1-m8 8/8 killed. Correction: the process sweep after the gate found an orphaned sleep 60 from the lock test's flock holder (N1); 08db6ac3f fixes it test only, Sage ruled the gate stands (sage-1707), rerun c0 20/0 and m1 killed with 0 left (sage-1708). Tip 1a1dc80ca (pushed, seat branch only). Manifest 464ce9eb (7 paths), queue revs 2644 and 2647, request comment 29901, reviewer Filbert. Packet agents/darkwing/work/queue-233/BUILD.md. + + 2026-10-21T06:37:41Z | Darkwing | row 240 #1746 installer I4 build, round 1 in review | Before: root had no host check for the bwrap AppArmor profile, its pins or a boot unit, and stack-upgrade.sh exited 1 args-failed when the args move after A5 failed, while exit 1 means the old copy runs. After: scripts/case1-hostcheck.sh (seven items, --check, --enforce, --upstream), scripts/case1-pins.sh, mosaic-hostcheck.service and 16 tests; the args move failure exits 5 args-failed, documented in TOOLS.md and run sheet section 6 and tested; run sheet section 3 puts the pinned npm on PATH and names the bin/claude.exe --claude path; sections 8 and 10 have precheck lines. Verification: slot run r1 on 47ec9fe2f failed at the singles (the new N1 test's after hook threw ENOENT after the fixture cleanup removed the root); b45852daa drops the hook; r2 on b45852daa green: singles 16/0 and 21/0, c0 plus M1-M8 8/8 killed, cli 639/0, scripts 585/0. Candidate darkwing/row240-r3 ad1b23ab2 on c771a36f4, manifest 573ece95 (9 paths), revs 2694 and 2695, comment 29943. + ++ + ++2026-10-20 19:09:44 UTC | Gander row 196 #1694 design phase | Before: tester TCP reaches every local uid and human proofs do not bind the socket peer. After: design draft at agents/gander/work/queue-196/DESIGN.md inventories listeners, proposes private tester HTTP and SO_PEERCRED binding, aligns the per-request principal contract with Morgana and Gizmoduck, and lists migration, tests, mutants and same-uid limits. Python versus other credential options and Console provenance require Duckworth ruling. Documentation only. No product code, suite, live probe, secret access or deployment. + ++ + ++2026-10-20 19:16:20 UTC | Gander row 196 #1694 design r2 | Before: Duckworth r1 requested concurrent helper-fd evidence (B1) and selected O1 with interpreter, proof-only and image conditions. After: design adds eight concurrent genuine proof processes plus a racing copier, M20 swapped socket and post-helper socket survival, root-owned system ELF pinning and activation evidence, safe gid validation, cap traffic without helpers, and unchanged P19 R2 text. Mos accepted socket-only tester access (mos-1015 via sage-1363); any authenticated browser relay is a separate row. Documentation coverage checks passed. No product code, suites or live action. + ++ + ++2026-10-20 19:21:19 UTC | Gander row 196 #1694 design approval and N5 | Before: Duckworth approved r2 (duckworth-0009, cb6d840df) with a nonblocking interpreter-upgrade note. After: r3 chooses startup safety revalidation, appends actual interpreter identity to bus/peercred-starts.jsonl and retains old activation evidence. Operator re-pin uses the next reviewed copy, respecting already-active refusal. Added task-by-task PLAN.md for inline implementation and independent manifest review. Documentation checks passed. No product code, suites or live action. + + + +2026-10-20 19:34:28 UTC | Gander row 196 build paused at design mismatch | Before: approved design promised Unix tester cleanup only for the owned inode. After: scratch test shows Node/libuv close unlinks a replacement inode before JS close handlers. Evidence agents/gander/work/queue-196/evidence/tester-cleanup-blocker.txt. Sent Sage/Duckworth gander-0016/0017, recommending explicit same-uid residual with private 0700 parent retained. Interpreter, concurrent peer, principal, human, peer-transport, transport, tester and process single files passed. Partial code uncommitted, tester transport not wired. No suites, mutant batches or live actions. + + + @@ agents/gander/work/queue-196/BUILD.md (new) + +change. Review covered all changed trust paths and consumers; no further code + +changes are planned before the batch. M1-M29 remain prepared and unexecuted. + + + ## agents/gander/work/queue-196/DESIGN.md (new) ## + +@@ + ++# Row 196: the human boundary across uids and humans + ++ + ++Gander, 2026-10-20. Revision 3, recording approved r2 and N5. Gitea #1694, P19 333. + ++Base: `96f243faf`, branch `gander/row196`. Design approval latest-by 2026-10-22, landing + ++latest-by 2026-10-24 and before federation R3. This document authorizes no live action. + ++ + ++## 1. Outcome and authority + ++ + ++Other Linux users must not reach the tester's data or writes. A bus human proof must describe + ++the process that actually connected. Console user calls must also pass the human walk and + ++come directly from this bus tree's Console. Row 193 and federation R3 use that one verifier. + ++ + ++This is not isolation from a hostile process on the instance's own uid. Keeping the human + ++terminal CLI means that an unmarked process on that uid still has the owner's authority. + ++Process ancestry cannot prove that a process was never an agent. Sections 4 and 8 state this + ++limit without treating it as an enforcement guarantee. + ++ + ++Inputs: COMMON at `53bb59462`, the row brief at `96f243faf`, Q175 sections 1, 6 and 8, + ++federation r2 at `60b9b112a` section 3.6, Duckworth's B1-B5 at `b402197ff`, row 190's + ++DESIGN.md and the row 190b brief. Q175-Q178 and the MVP plan set the order. Row 190b is on + ++hold pending the host decision. This row does not assume that a separate uid is already live. + ++ + ++Sage's `sage-1343` accepts the design direction and requires Duckworth to rule on the peer + ++credential implementation. `sage-1345` keeps the control board inventory-only. Morgana's + ++`morgana-0003` supplies the principal contract in section 4. Sage registers this session. + ++Mos accepted removal of direct tester browser access in mos-1015, relayed by sage-1363. + ++Agents test through the private socket. Any later browser view is an authenticated relay + ++behind SSO, designed and reviewed in its own row. This row adds no relay. + ++ + ++## 2. Inventory from code, not a live probe + ++ + ++"Same uid" includes untrusted local processes. "Other uid" excludes root, which is outside + ++this boundary. SSO below means Q175 is active on that instance, not merely installed. + ++ + ++| Entry | Same uid today | Other uid today | Effect of SSO and this row | + ++|---|---|---|---| + ++| Main Console, default 7332, bare serve default 7330 | Loopback reads. With preview/send, matching Host, header and origin can write as owner | Same TCP reach and header forgery | Q175 SSO verifies login and refuses non-public Host except GET /healthz. This row does not change public ingress | + ++| Tester TCP, default installation 7333, optional per instance | Reads and the tester instance, route, name and chat writes | Identical reach, no uid check | Q175 deliberately leaves tester separate. This row replaces TCP with a private filesystem Unix socket | + ++| `/bus/broker.sock` | Connects, then needs cap or proof. Claimed human PID is not the socket peer | Normally refused by bus/ 0700 and socket 0600 | SSO changes nothing. Add peer uid and PID binding, retain file checks | + ++| Human wrapper to tree's human-cli.mjs | Owner if CLI path, nonce and ancestry pass. Agent ancestors refuse | Cannot connect to protected socket, and proof uid refuses | Preserve terminal owner path, add peer binding. SSO does not authenticate terminal users | + ++| Tester CLI to broker.sock | Tester if its proof passes and bus.tester is exactly true. T3 agents are allowed, managed runs are not | Filesystem modes normally refuse | Same tester policy, plus peer binding. No owner/user upgrade | + ++| `/bus-host/launch.sock` | Requires run cap and launcher checks | bus-host/ 0700, socket 0600 | No human proof endpoint. Unchanged | + ++| `/bus-host/git.sock` | Git capability protocol and broker authorization | bus-host/ 0700, socket 0600 | No human proof endpoint. Unchanged | + ++| Discord connector | Outbound Discord gateway/REST, no inbound local listener. Local process could tamper with same-uid files/process | No local port to call. File separation depends on uid and modes | Existing guild, user, channel, bot and webhook checks remain. Discord is not a new bus human transport | + ++| Row 190 additions | Separate configs, roots, unit names, wrappers and Console ports, on one uid | Console/tester TCP still reachable | Adds no new class of listener. Row 196 derives its socket per dataRoot. Row 190 alone supplies no hostile-process isolation | + ++| Control board, default 7331 | Unauthenticated transcript/board reads, POST /api/reply and /api/seen after request checks | Same loopback reach. A local client can supply matching request headers | Independent development service, outside instance SSO. Inventory only per sage-1345. Its fix needs another row | + ++ + ++Evidence: `packages/webui/src/{cli,serve,bus}.mjs`, `packages/bus/src/{server,runtime,human, + ++tester,store}.mjs`, `packages/cli/src/{launcher,git-socket,transport}.mjs`, + ++`packages/discord/src/{gateway,authorize}.mjs`, `packages/control-board/src/serve.mjs`. + ++Store checks bus/ with `safe(directory,true)`, including owner, symlink and mode checks. + ++The dataRoot check itself does not require 0700 today. Do not describe that as a checked mode. + ++ + ++## 3. D1: private tester HTTP socket + ++ + ++Add `serve --tester-socket`, a boolean selecting `/console/tester.sock`. No arbitrary + ++path or new system config field. Create console/ as 0700. Refuse a symlink, wrong owner, + ++non-directory or any group/other bits on an existing console/. Refuse a pre-existing socket + ++or other entry rather than deleting it blindly. A stale socket needs an operator's diagnosis. + ++Refuse a path longer than Linux's 107-byte pathname capacity before binding. + ++ + ++Bind HTTP on the Unix path, set socket mode 0600, then verify type, owner and mode before + ++declaring ready. The private parent covers the bind-to-chmod interval. Cleanup only the owned + ++device/inode, as broker.sock does. Never open a TCP compatibility bridge or an abstract socket. + ++Readiness failure closes the main listener too, rather than leaving a partially started Console. + ++ + ++The tester handler keeps its fixed principal, header/cookie refusals and exact write list. + ++Use a fixed logical origin `http://mosaic-tester.invalid` for URL and same-origin checks. It + ++does not resolve that name. Clients send that Host and Origin plus the existing Fetch Metadata + ++when required. A Host string is not authentication. Unix socket access supplies that boundary. + ++The main listener still refuses x-mosaic-tester. No tester /auth route exists. + ++ + ++`bus.tester` remains Jason's switch. Socket reach never enables tester authority by itself. + ++Turning it off must refuse every tester bus call. This row adds no user or owner capability + ++to the tester. Tester reads retain today's scope, including any local Console reads that are + ++not bus calls. This row does not turn bus.tester into a separate HTTP read switch. + ++ + ++### Consumers and migration paths + ++ + ++| Consumer of 7333 | New path | + ++|---|---| + ++| Agent tester HTTP tools and curl probes in authorized window packets | `curl --unix-socket /console/tester.sock http://mosaic-tester.invalid/...`, with the fixed Origin for writes. Add one repository helper for bounded tester HTTP requests so callers do not copy path/origin logic | + ++| `scripts/mosaic ... --tester` | Already uses TESTER_CLI and broker.sock, not TCP. Keep its verbs, output and switch | + ++| Browser tester page in row 176 and later window checks | Ordinary browsers cannot navigate a Unix HTTP socket. Replace automated tester HTTP checks with the socket helper. Browser visual QA uses scratch fixtures only. No local TCP relay is added | + ++| `webui-service.sh` render/install and installed unit | Carry --tester-socket for new copies. Refuse a target without support. Units running old copies retain their old flags until a window | + ++| `stack-release.sh` activate, plan, rehearsal and rollback | Add --tester-socket. Replace inherited --tester-port N only when explicitly requested. Refuse incompatible inherited flags before stopping units. Plan names the transport and every dropped pair. Rehearsal uses a scratch socket | + ++| Gate/window Console checks, including agents' console-check scripts | Extend checks to use scratch Unix HTTP and expect no tester TCP bind. Sage owns live packet updates, including Q175 phase 2's old --tester-port 7333 instructions | + ++| `docs/TOOLS.md`, webui/cli/bus READMEs and examples | Document socket invocation, fixed origin, loss of direct browser navigation and per-instance path. Historical window receipts stay unchanged | + ++ + ++Repository search found no non-test hardcoded 7333 caller in tools/ or the harness code. The + ++active windows and external seat commands still need Sage's consumer audit before re-pin. + ++The build manifest will record the full search, rather than treating this draft as that audit. + ++ + ++## 4. D2: one verified principal per bus request + ++ + ++Internal interface: `verifyPrincipal(request, connection) -> {kind,business,id}` or refusal. + ++Kinds are owner, user and tester. The connection metadata is supplied by the trusted server, + ++never deserialized from JSON. No role or authority is returned. The broker looks up live policy. + ++ + ++1. Read and validate the bounded envelope first. For human, tester and user proofs, read Linux + ++ SO_PEERCRED on that accepted Unix connection before dispatch. Require a valid peer PID + ++ and the bus uid. Refuse unavailable credentials, unsupported platform and errors. Cap + ++ traffic keeps its capability protocol behind the validated 0700 directory and 0600 socket. + ++2. Require exactly one cap, human, tester or future user envelope. Human/tester/user proof.pid + ++ must equal the kernel peer PID. Verify startTime, nonce and CLI path against that process. + ++ Keep the second process read after the walk. Missing, changed or exited processes refuse. + ++3. Owner terminal: run the full existing human walk. Map the business to businesses[b].human. + ++ No supplied human id, role or uid selects the principal. Keep today's terminal invocation. + ++4. Tester: run verifyTester, retaining its deliberate difference from the human walk. An agent + ++ seat may be tester. A managed-run marker or registered managed ancestor refuses. Then check + ++ bus.tester. The full human walk must never be weakened to admit a session user. + ++5. Session user: use the full human walk with the path-bound user-cli entry and its own nonce + ++ variable, MOSAIC_BUS_USER_NONCE. Parameterize the nonce reader without admitting a human + ++ or tester nonce as a user proof. Require its immediate parent to be the same tree's resolved webui/src/cli.mjs, + ++ argv[2] `serve`. Check parent uid, startTime and ppid again before binding. Both child and + ++ parent remain alive for the call. A different checkout or wrapper parent refuses. + ++6. For user calls, the Console puts its mapped access-file user id in the verified proof's + ++ user field, as federation 3.6 specifies. The id comes only from claims Q175 verified for + ++ this request's live session, never from an identity cached past that session. It never + ++ takes that id from a form field or verb + ++ args. Return kind user, proof.business and proof.user as id only after the provenance + ++ checks. R3 then validates that mapping and the live grant. Bad access.json denies users, + ++ not the terminal owner. The verifier does not implement access-file policy. + ++7. Owner Console calls keep the verified Q175 owner subject path. Under SSO only a subject + ++ in console.sso.subjects reaches owner bus calls. They use human-cli and the same peer/walk + ++ checks. A terminal owner still needs no SSO token. No anonymous main-listener fallback. + ++ + ++The bus verifies which process supplied the session identity. It relies on that Console's + ++Q175 implementation to have verified OIDC. It does not independently verify an ID token in + ++this option. A provider/sub field alone, from another process, is not enough. + ++ + ++The Console must spawn user-cli directly with its nonce already in the launch environment. + ++It must not use human-cli's existing self-respawn pattern, which makes the CLI's parent a CLI. + ++Apply the same initial-nonce spawn to Console human calls for consistent tests. Terminal + ++human-cli can retain its self-respawn since no Console-parent condition applies there. + ++Never strip agent markers from inherited environments to make a walk pass. + ++ + ++The server binds an internal cap for this request, dispatches, and disconnects in every + ++success/error/timeout path, including asynchronous task and web calls. It never returns the + ++cap or a reusable principal handle. A proof copied by another process has a different kernel + ++peer PID and fails. One verified CLI may make only its one request on its one connection. + ++The session may hold s.user for that request. No PID or principal cache survives the connection. + ++ + ++### Fit with rows 193 and R3 + ++ + ++Row 196 owns peer metadata, proof/walk verification and the internal principal interface. + ++It supplies the Console provenance verifier and tests before R3 adds user-cli and user sessions. + ++R3 owns access.json mapping and the bindUser integration, using this interface. It must not + ++add a second ancestry implementation. Row 193 adds viewer and share policy to the same live + ++grant check. An owner id is the business human. A user id is the access-file id, with stored + ++owner string `user:`. Neither row takes role from the proof. + ++ + ++No live grant, unknown subject, duplicate owner/user mapping and invalid access file all + ++deny user binding through R3's policy. Per-request authorization, access.reload and the + ++30-second recheck remain R3's responsibility. Share visibility uses that check in row 193. + ++Binding provenance is not a grant and never widens one. R3's user dispatch must run before + ++names falls through to #agent. R3 must not land before this contract and its tests are green. + ++Gizmoduck's gizmoduck-0015 and Gander's gander-0007 agree on this split. R3 keeps the reserved + ++user-name mutant. Row 196 tests kind separation even if a user id resembles the owner name. + ++ + ++### OIDC alternative + ++ + ++The bus could verify the signed ID token itself and map its issuer/sub. That reduces reliance + ++on the Console's subject assertion, but requires JWKS/provider handling, nonce or challenge + ++binding, replay handling, login and logout/session lifetimes in the bus. Reusing an ID token + ++on every request either creates a replay credential or expires a Console session with the + ++token. A bus-issued session needs a separate lifecycle design. It also leaves the terminal + ++owner and hostile same-uid limit intact. Recommend the Console provenance contract for this + ++slice. Duckworth approved this option in duckworth-0006, with the live-session condition above + ++and the requirement to carry section 8 R2 into P19 333 word for word. + ++ + ++## 5. D3: obtaining kernel credentials, O1 approved with conditions + ++ + ++All options must bind the accepted connection, not a PID a caller picked. None gets a silent + ++fallback to the current proof. Sage required this comparison. Duckworth selected O1 in + ++duckworth-0006, with the interpreter, proof-only and image conditions below. O4 remains the + ++fallback if Python is unavailable, and needs a revised build manifest before use. + ++ + ++| Option | What it proves and costs | Recommendation | + ++|---|---|---| + ++| O1 Python stdlib helper on inherited accepted socket | Node passes the accepted net.Socket as fd 3 using documented spawn stdio stream passing. Python socket(fileno=3) reads SO_PEERCRED and returns only pid,uid,gid on stdout. No socket reads, no shell, no credential values. Adds Python to bus hosts and any future bus image, with a process per admitted proof connection. Copies carry the helper and depend on the system interpreter | Selected by Duckworth, with conditions below | + ++| O2 /proc/net/unix plus inode and /proc/PID/fd scans | The table identifies Unix socket entries, not an authenticated peer PID for each accepted connection. Finding a process holding an inode is racy, may have several holders and can be unreadable. The server endpoint's inode is not the client's inode | Reject as a replacement for SO_PEERCRED. No claimed proof PID may choose the scan target | + ++| O3 plain Node fd passing or abstract sockets | A private inherited IPC channel can authenticate a child that this parent spawned. The bus did not spawn the independently started Console or terminal CLI. Moving the bus into the Console's child tree changes lifecycle and ownership. An abstract name supplies no filesystem mode boundary and no peer uid check by itself. Node's public net API supplies no SO_PEERCRED accessor | Reject as a drop-in. A larger bus-spawned transport design could use fd capabilities, but is outside this slice | + ++| O4 small compiled helper or Node-API addon | Calls getsockopt directly, with no Python runtime. A helper can use the same inherited fd. Adds compiler/build closure and platform artifacts to stack-copy and containers. An addon also needs a supported way to obtain the accepted socket fd and Node version coverage | Viable alternative if Python is refused. Prefer a small helper over undocumented Node handles | + ++ + ++For O1, pin the system interpreter `/usr/bin/python3`, resolving its system symlink to an + ++absolute ELF binary. Require root ownership and no group/other write on that executable and + ++its path components. Refuse scripts, PATH shims and any resolved path under an OS account's + ++home, including /home and /root. A pyenv shim can choose an interpreter before -I -S applies. + ++No PATH lookup, caller-selected interpreter or helper. Invoke the resolved binary with + ++`-I -S`, a minimal environment and the absolute helper path in the same pinned copy. + ++ + ++Activation preflight records the interpreter's resolved path, version and file identity/hash + ++in its gate evidence. The bus validates and pins the resolved executable at each startup + ++before opening broker.sock. The activation gate must report the same interpreter used by + ++that bus. Missing, unsafe or changed resolution during that activation refuses it, never + ++falling back to PATH. No secret values are recorded. Test system-owned interpreter checks + ++through injected metadata. + ++ + ++N5 decision: a later bus restart repeats the safety/version/support checks and accepts a + ++safe system interpreter installed by an OS upgrade. It does not compare against an old + ++activation hash. Before opening the listener, append a startup receipt with timestamp, + ++copy path, resolved interpreter path, version and file identity/hash to + ++`/bus/peercred-starts.jsonl` (0600 in the checked 0700 bus directory). Refuse an + ++unsafe receipt file or failed append. Never edit the old activation evidence. The startup + ++receipt identifies the binary now in use, so the old receipt remains a historical fact. + ++If an upgraded interpreter fails the checks, startup fails closed until a safe supported + ++system interpreter is restored by the operator. There is no shim or O4 auto-fallback. + ++ + ++Operator re-pin after an interpreter change: in the next authorized window, prepare the + ++next reviewed copy with `scripts/stack-copy.sh copy NEW`, then run + ++`scripts/stack-release.sh activate NEW BUSINESS --tester-socket` with that instance's existing + ++flags and selector, first with --plan. NEW must differ from the active copy because activate + ++refuses already-active. Record the interpreter in the new activation evidence and compare it + ++with the bus startup receipt. A routine OS update alone does not authorize a re-pin or unit + ++restart. A bus restart does not rewrite config, pointer or activation history. + ++ + ++Preflight helper and SO_PEERCRED support too. The helper lives under packages/bus/src so + ++stack-copy's closure includes it. Copies remain immutable. Document Python >=3.10 on bus + ++hosts. Today's Containerfile builds the worker image and runs run-agent.sh, not the bus. + ++It needs no Python change for this row. A future bus-host image must explicitly include + ++the pinned system Python or use reviewed O4. No install through the node_modules symlink. + ++ + ++Pause each proof connection while reading credentials. Limit outstanding helpers + ++to 16, timeout to one second, output to one strict JSON record of at most 256 bytes. Refuse + ++nonzero exit, extra output, malformed integers, missing fd and timeout. Validate pid, uid and + ++gid as safe integers, pid >=2 and uid/gid >=0. Only pid and uid select the principal. gid is + ++validated but grants no authority. Kill and reap helpers on shutdown. Cap envelopes never + ++spawn this helper. Reading the proof envelope first is sound because SO_PEERCRED describes + ++the peer at connect time. Keep the accepted Socket object owned by its connection closure, + ++not a shared current-fd variable. Only the helper's duplicate fd is closed when it exits. + ++ + ++References: [Node stdio stream passing](https://nodejs.org/api/child_process.html#optionsstdio), + ++[Node Unix socket support](https://nodejs.org/api/net.html#ipc-support), + ++[Python socket and getsockopt](https://docs.python.org/3/library/socket.html), + ++[Linux Unix credentials and permissions](https://man7.org/linux/man-pages/man7/unix.7.html), + ++[Linux proc network table fields](https://man7.org/linux/man-pages/man5/proc_pid_net.5.html). + ++These support the APIs. The connection-binding behavior still needs T2 on supported Node versions. + ++ + ++## 6. Migration and rollback + ++ + ++No config schema or store schema changes. Old copies and units keep working until each + ++instance re-pins. No new verifier is loaded into a running old broker. Do not pair a new + ++Console with an old bus and call it protected. Same-tree paths intentionally refuse mixtures. + ++ + ++The authorized window must pin bus, Console and human wrapper to one candidate. New tools + ++offer `activate ID BUSINESS --tester-socket`. Plan must show the old tester-port pair removed + ++and the new socket flag added, and tell Jason that direct tester browser navigation is lost. + ++Mos accepted that change in mos-1015 (sage-1363). Agents test through the socket. A later + ++authenticated browser relay behind SSO belongs to its own row. Without the explicit + ++conversion, a candidate that no longer + ++serves tester TCP refuses inherited --tester-port before stopping anything. New serve refuses + ++--tester-port with a migration error. It never silently changes that flag's meaning. + ++ + ++Rollback to a copy lacking --tester-socket drops tester enablement and reports it, as today's + ++tools drop unsupported flags. It does not turn the socket back into TCP. The old main Console + ++and owner CLI can run with their known old limits. A failed activation's recovery may restore + ++the old saved unit, including its TCP listener. Report that recovery as restoration of the old + ++security state, never as a hardened deployment. After rollback, R3 and second-human access are + ++not accepted until prerequisites are restored. Older operator tools cannot manage the new + ++flag safely. The window must use the reviewed candidate tool blob for rollback as well. + ++ + ++The Q173 window supplies real uid-denial evidence and the final consumer audit. No second uid, + ++live socket, unit, config or secret is touched in this row's tests. Row 190b or a separate host + ++still owns cross-instance file isolation. It is not replaced by SSO or this socket change. + ++ + ++## 7. Acceptance tests and mutants + ++ + ++All fixtures use scratch roots and ephemeral sockets, no live 7332/7333, public origin or + ++provider. No real second uid. Foreign uid checks use injected metadata and name that limit. + ++Use short TMPDIR paths so the longest Unix socket path stays under 108 bytes. + ++ + ++| Test | Required evidence | Mutant killed | + ++|---|---|---| + ++| T1 private tester | HTTP over scratch socket reads and writes as tester only. parent 0700, socket 0600. No tester TCP listen. Wrong Host/origin and auth cookies refuse | M1 bind loopback TCP as well, M2 remove socket mode check, M3 accept owner cookie | + ++| T2 real peer binding | At least 8 concurrent proof connections from distinct scratch client processes, each with its own live proof, plus a copier racing one live CLI. Barrier the helpers so connections overlap. Each genuine call must bind its own peer PID and pass, the copier must refuse. Repeat on Node 24 and 26. A lower-level helper fixture exchanges bytes both ways on the accepted socket after the helper exits, and each real bus call receives its response after helper exit. Helper failure cases all refuse | M4 use proof.pid as peer PID, M5 continue after helper error, M20 give the helper another pending connection's socket | + ++| T3 uid/path refusal | Inject foreign peer uid, wrong file owner/modes, symlink parent, existing socket and long path. Inject interpreter script, user-owned ELF, home path, writable ancestor and missing executable. Refuse before dispatch, no repaired paths. Unsafe pid/uid/gid and extra helper output refuse | M6 ignore uid, M7 accept unsafe parent | + ++| T4 ancestry | Agent env, binary and registered launch above owner/user refuse. Missing/reused PID, changed parent and cycles refuse. Clean terminal owner works | M8 omit walk, M9 omit identity re-read | + ++| T5 Console parent | Direct CLI child from exact tree Console fixture works. Same CLI under shell, other copy, wrapper, agent ancestor or CLI respawn refuses | M10 remove direct-parent check, M11 accept any Console basename | + ++| T6 principal shape | No human id or role from request body. Exactly one proof. No returned cap. Disconnect on synchronous and async failure, timeout and close. Tester agent allowed, managed tester refused | M12 accept extra identity, M13 omit disconnect, M14 use tester walk for user | + ++| T7 consumer migration | Render/plan/activate/rehearse fixture carries socket flag in unit and pointer. Explicit conversion removes port. Unsupported/inherited mismatch refuses before unit calls. Rollback disables unsupported tester, never opens TCP | M15 keep inherited tester port, M16 restore TCP on downgrade | + ++| T8 gates and identity | bus.tester false refuses tester calls. SSO owner/user wiring uses only claims verified for that request's live session. Expired/logged-out or replaced session cannot reuse an earlier mapped id. User kind never becomes owner from its id. R3 integration maps subject, denies missing/invalid grants and owner overlap, broken access file leaves terminal owner working | M17 bypass tester switch, M18 turn user kind into owner by id | + ++| T9 isolation limits | Two scratch instances have different sockets and matching tree paths. Foreign-uid stand-in refuses. Record that no real uid boundary was exercised | M19 choose default instance socket | + ++| T10 interpreter and cap path | Activation evidence records the same pinned root-owned system ELF that the bus uses. Scripts, home paths and unsafe owners/modes refuse. Simulated OS replacement followed by startup accepts a safe replacement and appends its actual identity without changing the old receipt, refuses an unsafe replacement or unsafe/unwritable receipt. Cap calls complete without invoking the helper, proven with a helper spy that fails if called | M21 bypass interpreter safety, M22 invoke helper for cap traffic, M23 retain stale interpreter identity after restart | + ++ + ++Tests for new policy stay in the bus, WebUI and CLI packages. R3 provides the real user/grant + ++half of T8 in its candidate, with its own gate. This row tests the shared verifier's user + ++provenance without shipping placeholder user access. Mutants run on scratch copies, + ++restore the original bytes and verify hashes. Two failures in a gate run stop and are reported. + ++Single test files run freely. Ask Sage for the slot before any suite. Final acceptance is + ++Duckworth's approved manifest plus Sage's bus, WebUI, CLI and scripts/test-*.sh gate. + ++ + ++## 8. Closure, limits and review decisions + ++ + ++R1. Same uid remains trusted OS authority. A hostile process may detach, clear its markers, + ++modify argv, read same-uid pipes or capabilities, edit policy files, or interfere with the + ++Console where host policy permits. SO_PEERCRED proves the connecting process identity at + ++connection time. It does not prove human intent, protect memory, or track a socket passed to + ++another process. Ordinary agent children fail the walk. Hostile detached agents are not + ++claimed contained. Separate uids narrow cross-instance reach, not reach within one instance. + ++ + ++R2. The Console provenance option trusts verified code and process metadata, not an independent + ++bus-side OIDC exchange. That trade must be approved, not hidden behind the phrase zero trust. + ++ + ++R3. A Unix tester endpoint removes direct browser navigation. No bridge is in this design. + ++The 7331 development board remains a separately reported reachability and impersonation risk. + ++ + ++P19 333 can record the following after the implementation gate and authorized re-pin: + ++"Tester ingress is private to the instance uid. Bus human proofs are tied to the actual + ++Unix peer and retain the human walk. Session-user calls require the same-tree Console as + ++direct parent. Q175 phase 2 separately protects main Console ingress with verified OIDC. + ++Hostile processes on the instance uid, root, and the independent development control board + ++remain outside these guarantees. Cross-instance file isolation requires the uid/host gate." + ++ + ++Duckworth requires that P19 333 also carry R2 word for word: + ++"The Console provenance option trusts verified code and process metadata, not an independent + ++bus-side OIDC exchange. That trade must be approved, not hidden behind the phrase zero trust." + ++ + ++Do not mark the historical task fully closed on a design or commit. Closure needs live + ++Q175 phase 2, this row's re-pin and the recorded residual accepted through Mos. A promise of + ++hostile same-uid isolation would require separate OS authority for agents and a new design. + ++ + ++Review receipt: duckworth-0006, notes at `86be181a2:agents/duckworth/work/row196/REVIEW.md`. + ++Q1 selects O1 with the conditions incorporated in section 5. Q2 approves Console provenance + ++with the live-session and unchanged P19 R2 conditions. B1 adds concurrent T2, M20 and socket + ++survival checks. N2's detached-Console limit remains in R1. N3's gid validation and N4's + ++operator window warning are included. Mos's tester-access ruling is recorded above. No + ++product code begins before design approval. + ++Duckworth approved revision 2 in duckworth-0009, review at + ++`cb6d840df:agents/duckworth/work/row196/REVIEW-r2.md`. N5 permits either restart policy. + ++Revision 3 selects safety revalidation with a new append-only startup receipt, and gives the + ++operator a re-pin path that respects the existing already-active refusal. + + + ## agents/gander/work/queue-196/MANIFEST.md (new) ## + @@ + +# Row 196 review manifest + @@ agents/gander/work/queue-196/MANIFEST.md (new) + +The Console provenance option trusts verified code and process metadata, not an independent + +bus-side OIDC exchange. That trade must be approved, not hidden behind the phrase zero trust. + + + ## agents/gander/work/queue-196/PLAN.md (new) ## + +@@ + ++# Row 196 Human Boundary Implementation Plan + ++ + ++> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking. + ++ + ++**Goal:** Bind human proofs to their actual Unix peers and make tester HTTP private to the instance uid. + ++ + ++**Architecture:** The bus reads kernel credentials through a pinned system Python helper only for proof envelopes. One principal verifier keeps the human walk and supplies Console provenance for federation R3. Tester HTTP moves to a private filesystem socket. Release tools migrate the flags explicitly. + ++ + ++**Tech Stack:** Linux, Node 24 and 26, Python >=3.10 standard library, existing Node test runner and shell release tools. + ++ + ++**Spec:** `agents/gander/work/queue-196/DESIGN.md`, approved r2 at 5fe259f16, with N5 restart policy added in r3. + ++ + ++## Global Constraints + ++ + ++- Work in the existing gander/row196 worktree. Plain node_modules symlink. No new package dependency. + ++- No live files, credentials, listeners, units, second uid or external authentication service. + ++- Helpers: proof envelopes only, at most 16 concurrent, one second timeout, 256-byte strict output. + ++- Root-owned system Python ELF at resolved /usr/bin/python3, never PATH or a home/shim path. + ++- console/ 0700, tester.sock 0600, Unix pathname at most 107 bytes. + ++- One principal and transient cap per request. R3 owns user access policy and user-cli integration. + ++- Mos accepted socket-only tester access. No TCP relay or control-board change. + ++- Single test files run freely. Ask Sage before any suite. Stop and report two failures in a gate run. + ++- Commit code only after applicable suites pass. Sage owns the final gate and manifest queue moves. + ++ + ++## Review Focus + ++ + ++- Connection overlap and disconnect: helpers must retain the correct Socket object and release capacity on every exit. + ++- OS interpreter replacement: restart records the actual safe binary, while old receipts remain unchanged. + ++- Long or unsafe instance paths: refuse before binding or stopping any service. + ++- Rollback/recovery: distinguish intentional tester disablement from restoration of an old saved TCP unit. + ++- CLI nonce respawn and session expiry: direct-parent provenance must survive correct Console spawning and refuse stale identity. + ++ + ++## Task 1: kernel credentials and interpreter evidence + ++ + ++Files: create `packages/bus/src/peercred.mjs`, `packages/bus/src/peercred.py`, + ++`packages/bus/src/interpreter.mjs`; tests in `packages/bus/tests/peercred.test.mjs` and + ++`packages/bus/tests/interpreter.test.mjs`, with subprocess fixtures under `packages/bus/fixtures/`. + ++ + ++Interfaces: `inspectInterpreter()` returns a validated immutable record of path, version, + ++device, inode and sha256. `recordInterpreterStart(directory, record)` appends the actual startup + ++identity safely. `createPeerReader({interpreter})` returns `{read(socket),close()}`. read resolves + ++to `{pid,uid,gid}` from that exact socket, or refuses. Test-only injected OS/process operations + ++do not become product flags or environment overrides. + ++ + ++- [ ] Write interpreter safety, receipt replacement and cap-independent helper tests (T3/T10). + ++- [ ] Run each new test file alone and record the expected missing-module failures. + ++- [ ] Implement system interpreter validation and startup receipt append with no-follow, owner/type/mode checks. + ++- [ ] Write T2's eight-process barrier plus racing copier, wrong-fd case and post-helper socket exchange. + ++- [ ] Implement fd-3 helper, strict result validation, bounded async reader and helper shutdown. + ++- [ ] Run `node --test packages/bus/tests/interpreter.test.mjs` and separately `node --test packages/bus/tests/peercred.test.mjs`. + ++- [ ] Confirm process/socket cleanup, eight distinct kernel PIDs, copier refusal and original receipt preservation. + ++ + ++## Task 2: principal provenance and bus dispatch + ++ + ++Files: modify `packages/bus/src/{human,tester,server,runtime}.mjs`, create + ++`packages/bus/src/principal.mjs`, add `packages/bus/tests/principal.test.mjs`, and extend + ++`packages/bus/tests/human.test.mjs`. Extend server/runtime test files after inspecting their fixtures. + ++ + ++Interfaces: `verifyPrincipal(request, {peer,...trustedPathsAndProcessReaders})` returns + ++`{kind,business,id}`. Human maps to the business human, tester to tester, user to its verified + ++Console proof id. No roles or grants. Server obtains peer metadata from Task 1 only after + ++envelope validation and only for proofs. Existing cap dispatch never invokes the helper. + ++Future user binding stays refused without R3's trusted integration. + ++ + ++- [ ] Write T4/T5/T6/T8 process-table tests, including changed ancestry, distinct nonce namespaces and kind confusion. + ++- [ ] Run the focused new file and confirm expected failure before implementing the verifier. + ++- [ ] Parameterize the nonce/path portion of the existing human walk and retain all marker/binary/launch checks. + ++- [ ] Wire peer metadata, interpreter startup receipt and one-request cleanup into runtime/server. + ++- [ ] Test cap requests with a failing helper spy (M22), and test async refusal/close cleanup. + ++- [ ] Run each affected bus test file alone. Record any base fixture changes needed for real peer identity. + ++ + ++## Task 3: tester Unix HTTP and Console nonce spawning + ++ + ++Files: modify `packages/webui/src/{cli,serve,bus}.mjs`, create + ++`packages/webui/src/tester-socket.mjs` and `packages/cli/src/tester-http.mjs`; add focused + ++`packages/webui/tests/tester-socket.test.mjs` and `packages/cli/tests/tester-http.test.mjs`. + ++Extend existing WebUI transport tests. + ++ + ++Interfaces: `startTesterSocket({dataRoot,...handlerOptions})` owns the private listener and + ++cleanup. `testerRequest({dataRoot,method,path,body})` uses the fixed logical origin and bounded + ++Unix HTTP. Console human child spawning supplies its initial nonce, preserving agent markers. + ++R3 reuses that spawn pattern for its user-cli. + ++ + ++- [ ] Write T1/T3/T9 socket permission, unsafe-path, wrong-origin and instance separation tests. + ++- [ ] Confirm the new focused files fail, then implement --tester-socket and refusal of --tester-port. + ++- [ ] Keep tester principal, write allowlist and cookie/header refusals intact. Add no browser relay. + ++- [ ] Write and implement the initial-nonce Console spawn test and the helper request timeout test. + ++- [ ] Run each new or changed test file alone, verifying cleanup and no tester TCP bind. + ++ + ++## Task 4: release migration and operator evidence + ++ + ++Files: modify `scripts/{webui-service,stack-release}.sh`, inspect + ++`scripts/stack-copy.sh` closure, extend `packages/cli/tests/stack-release.test.mjs` and its + ++render fixtures. Update `docs/TOOLS.md` and package READMEs with the implementation. + ++ + ++Interfaces: `activate ID BUSINESS --tester-socket` explicitly replaces inherited tester-port + ++flags. Pointer and unit argv agree. Candidate support and interpreter preflight run before + ++any unit stop. Activation gate evidence records the same interpreter as the bus startup receipt. + ++ + ++- [ ] Write T7 explicit conversion, inherited mismatch and unsupported-target refusal fixtures. + ++- [ ] Run only the affected test file, then implement flag rendering, plan output and rollback disabling unsupported tester. + ++- [ ] Preserve old-copy recovery behavior and report restored old security state accurately. + ++- [ ] Add rehearsal scratch socket and T10 interpreter gate identity comparison. + ++- [ ] Verify same-copy activation refusal is preserved. Document N5's next-reviewed-copy re-pin path. + ++- [ ] Run the affected file alone, shell syntax checks, and inspect remaining tester-port references by purpose. + ++ + ++## Task 5: mutants, approved suites and manifest + ++ + ++Files: add the row's mutation runner and receipts under `agents/gander/work/queue-196/`. + ++Append BUILD-LOG phase receipts. No SESSIONS or queue file edits. + ++ + ++- [ ] Exercise M1-M23 against isolated scratch copies. Record killer test, restored hash and cleanup for each. + ++- [ ] Run focused tests on Node 24 and 26, including the concurrent T2 case and socket lifetime checks. + ++- [ ] Send Sage the scope and estimated duration, obtain the suite slot, then run approved bus/WebUI/CLI suites. + ++- [ ] Let Sage own the scripts/test-*.sh gate. Stop on the repository's two-failure rule. + ++- [ ] Build the exact changed-path manifest with acceptance evidence, search results, limits and consumer audit. + ++- [ ] Commit/push the verified candidate only after applicable suites pass. Send Duckworth and Sage path, commit and hashes for manifest review. + ++ + ++Execution stays with Gander in this worktree. No new agent delegation. Duckworth reviews + ++the manifest independently. Push is only to gander/row196. No merge, re-pin or deployment + ++is authorized by this plan. + + + ## agents/gander/work/queue-196/PRODUCT-FILES.json (new) ## + @@ + +{ + @@ agents/gander/work/queue-196/mutants.mjs (new) + + ['M12',h,'unknown proof identity accepted',principal,t=>t.replace("Object.keys(proof).some((k) => !['pid', 'startTime', 'nonce', 'business'].includes(k))",'false')], + + ['M13',s,'transient cap leaked',transport,t=>t.replace('broker.disconnect(boundCap)','void(boundCap)')], + + ['M14',p,'tester walk used for user',principal,t=>t.replace("verifyHuman(checked, { cliPath: paths[kind], launches, readProcess,\n nonceKey: kind === 'user' ? 'MOSAIC_BUS_USER_NONCE' : 'MOSAIC_BUS_CLI_NONCE' })",'verifyTester(checked, {cliPath:paths[kind],launches,readProcess,readNonce:()=>checked.nonce})')], + -+ ['M15','scripts/stack-release.sh','TCP retained during conversion',release,t=>t.replace('RUN_CONSOLE=("${without_tester[@]}" --tester-socket)','RUN_CONSOLE=("${P_CONSOLE[@]}" --tester-socket)')], + ++ ['M15','scripts/stack-release.sh','TCP retained during conversion',release,t=>t.replace('RUN_CONSOLE=("${without_tester[@]}" --tester-socket)','RUN_CONSOLE=("${BASE_CONSOLE[@]}" --tester-socket)')], + + ['M16','scripts/stack-release.sh','TCP reopened on downgrade',release,t=>t.replace('RUN_CONSOLE=("${without_tester[@]}") DROP_TESTER="--tester-socket','RUN_CONSOLE=("${without_tester[@]}" --tester-port 7333) DROP_TESTER="--tester-socket')], + + ['M17','packages/bus/src/broker.mjs','tester switch ignored','packages/bus/tests/tester.test.mjs',t=>t.replace("if (this.#business(record.business).vars?.['bus.tester'] !== true)",'if (false)')], + + ['M18',p,'user kind becomes owner',principal,t=>t.replace("return { kind: 'user', business, id: proof.user }","return { kind: 'owner', business, id: proof.user }")], + @@ docs/TOOLS.md: reports checkout or copy identity and refactor drift, and writes + scripts/stack-release.sh adopt BUSINESS + -scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N] + +scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)] + - [--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan] + + [--proof-header NAME --proof-file PATH | --no-proof | --sso] [--no-send] [--timeout S] [--plan] + scripts/stack-release.sh pm-on BUSINESS [--timeout S] + scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan] + @@ docs/TOOLS.md: refuses `capture-failed` before it stops anything. Adopt uses the same rule. + @@ packages/bus/src/principal.mjs (new) + +} + + ## packages/bus/src/runtime.mjs ## + -@@ packages/bus/src/runtime.mjs: import { createLoader, hasModule } from '../../plugins/src/loader.mjs'; + - import { pluginSettings } from '../../business/src/plugin-settings.mjs'; + - import { instanceSettings } from '../../business/src/instance-settings.mjs'; + +@@ packages/bus/src/runtime.mjs: import { credentialVerbs } from './credential-verbs.mjs'; + import { webService } from './web.mjs'; + + import { hostExecutor, sweepRunDir } from './host-run.mjs'; + + import { grantKeys, hostGrant, hostReference, sweepHostKeys } from './host-grant.mjs'; + -import { verifyHuman } from './human.mjs'; + +import { verifyPrincipal } from './principal.mjs'; + +import { inspectInterpreter, recordInterpreterStart } from './interpreter.mjs'; + @@ packages/bus/src/runtime.mjs: import { createLoader, hasModule } from '../../plu + // Human limits writes use the shared organization writer behind the verified transport. + // `tasks` is S3's adapter factory: ({broker, credentials, businesses}) => {handle, timeout, close}. + @@ packages/bus/src/runtime.mjs: export async function startBroker({ + - throw new BusError('invalid-adapter'); + - } + - const web = await webServices(businesses); + + if (runRoot) sweepRunDir(runRoot); + + // Row 234 (r6 X3): end the enrollments a killed bus child left, before anyone can call. + + broker.reconcileGrants(); + + const interpreter = inspectInterpreter(); + + recordInterpreterStart(store.directory, interpreter); + + const peerReader = createPeerReader({ interpreter }); + @@ packages/bus/src/runtime.mjs: export async function startBroker({ + const path = join(store.directory, 'broker.sock'); + server = await serve({ + web: (business) => web.get(business) ?? null, + +@@ packages/bus/src/runtime.mjs: export async function startBroker({ + tasks: adapter ? { handle: adapter.handle, timeout: adapter.timeout } : null, + broker, + path, + @@ packages/bus/src/runtime.mjs: export async function startBroker({ + + return broker.bindTester({ business: p.business }); + }, + }); + - // S6 launcher operations. The host decides capacity and the instance list; the broker + + // Row 235 (r6 X3): the remote undo of interrupted host grants, once the bus is up, so a start doesn't + + ## packages/bus/src/server.mjs ## + @@ + import { createServer } from 'node:net'; + import { lstatSync, chmodSync, unlinkSync } from 'node:fs'; + - import { BusError, TASK_VERBS, WEB_VERBS, PROJECT_FILE_VERBS, PROJECT_SAVE_VERB, PROJECT_SAVE_REQUEST_MAX } from './broker.mjs'; + + import { BusError, TASK_VERBS, WEB_VERBS, HOST_VERBS, HOSTS_VERBS, PROJECT_FILE_VERBS, PROJECT_SAVE_VERB, PROJECT_SAVE_REQUEST_MAX } from './broker.mjs'; + +import { inspectInterpreter } from './interpreter.mjs'; + +import { createPeerReader } from './peercred.mjs'; + const LIMIT = 65536; + // Row 148: a document save may carry up to 1 MiB of text. Only a request that names that verb near + // its start (the proof object comes first) may pass LIMIT; the parsed verb is checked again below. + @@ packages/bus/src/server.mjs: const SAVE_VERB_AT_START = new RegExp(`"verb":"${PROJECT_SAVE_VERB.replaceAll('. + - // `web` is the trusted web service (web.mjs), or a function from a business id to one (or null); only + - // the web verbs reach it, and without one they refuse web-not-configured (or web-not-granted). + + // `host` (row 210) is the trusted host.run executor (host-run.mjs), or null: host.run then refuses + + // host-not-configured. + // A request carries exactly one of cap, human (row S4) and tester (row 110) proof. + --export async function serve({ broker, path, authenticateHuman = null, authenticateTester = null, timeout = 5000, tasks = null, web = null }) { + -+export async function serve({ broker, path, authenticateHuman = null, authenticateTester = null, authenticateUser = null, peerReader = null, timeout = 5000, tasks = null, web = null }) { + +-export async function serve({ broker, path, authenticateHuman = null, authenticateTester = null, timeout = 5000, tasks = null, web = null, host = null }) { + ++export async function serve({ broker, path, authenticateHuman = null, authenticateTester = null, authenticateUser = null, peerReader = null, timeout = 5000, tasks = null, web = null, host = null }) { + try { + lstatSync(path); + throw new BusError('socket-exists'); + @@ packages/bus/src/server.mjs: export async function serve({ broker, path, authent + ) + .finally(() => { + - if (own) broker.disconnect(own); + ++ if (own) release(); + + }); + + return; + + } + +@@ packages/bus/src/server.mjs: export async function serve({ broker, path, authenticateHuman = null, authentica + + (e) => finish({ ok: false, error: e instanceof BusError ? e.code : 'host-failed' }), + + ) + + .finally(() => { + +- if (own) broker.disconnect(own); + + if (own) release(); + }); + return; + @@ packages/cli/src/tester-http.mjs (new) + + } catch { process.stderr.write('tester-http-refused\n'); process.exitCode = 2; } + +} + + - ## packages/cli/tests/stack-release.test.mjs ## + -@@ packages/cli/tests/stack-release.test.mjs: function biz(e,plugins){ + + ## packages/cli/tests/stack-release-fixture.mjs ## + +@@ packages/cli/tests/stack-release-fixture.mjs: export function biz(e,plugins){ + writeFileSync(f,JSON.stringify(b,null,2)+'\n',{mode:0o600});chmodSync(f,0o600); + } + + --async function mk(t){ + -+async function mk(t,dataName='data'){ + +-export async function mk(t){ + ++export async function mk(t,dataName='data'){ + const root=realpathSync(mkdtempSync(join(tmpdir(),'sr-'))); + - const e={root,data:join(root,'data'),bin:join(root,'bin'),sd:join(root,'sd'),units:join(root,'xcfg','systemd','user'),cfg:join(root,'cfg','config.json'),claude:join(root,'claude-bin','claude')}; + + const e={root,data:join(root,dataName),bin:join(root,'bin'),sd:join(root,'sd'),units:join(root,'xcfg','systemd','user'),cfg:join(root,'cfg','config.json'),claude:join(root,'claude-bin','claude')}; + + // The default instance's names; named() sets NAME's (row 190). + + Object.assign(e,{flags:[],bus:BUS,tmpl:'mosaic-bus@.service',con:CON,wrapper:'mosaic'}); + Object.assign(e,{state:join(e.data,'state'),pointer:join(e.data,'state','stack-active.json'),log:join(e.data,'state','stack-activation-log.jsonl'),store:join(e.data,'bus','bus.sqlite'),lock:join(e.data,'state','stack-release.lock')}); + - for(const d of [join(root,'home'),dirname(e.cfg),join(e.data,'bus'),e.bin,dirname(e.claude),join(e.sd,'faults'),e.units,join(root,'xdata')])mkdirSync(d,{recursive:true}); + - chmodSync(join(e.data,'bus'),0o700); + -@@ packages/cli/tests/stack-release.test.mjs: test('R15 a rehearsal stopped by TERM, INT or HUP fails signal and still stops t + - assert.equal(existsSync(`/proc/${pid}`),false,`${sig}: host pid ${pid} is still there`); + - } + + + + ## packages/cli/tests/stack-release.test.mjs ## + +@@ packages/cli/tests/stack-release.test.mjs: test('T56 row 190: named instances side by side on one unit directory, $BIN and + + const r=await run(a,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err); + + }); + }); + + + +test('row196 explicit socket conversion refuses inherited TCP before stop and rollback disables socket',async t=>{ + @@ packages/webui/src/cli.mjs: import { statSync } from 'node:fs'; + import { isAbsolute } from 'node:path'; + import { ID_PATTERN } from '../../business/src/vocabulary.mjs'; + const args = process.argv.slice(2); + --const usage = 'node packages/webui/src/cli.mjs serve [--port N] [--tester-port N] [--checkout PATH] [--board http://127.0.0.1:7331] [--business ID] [--public-host NAME --auth-header HEADER --allow-user ID [--send] [--proof-header NAME --proof-file PATH]]'; + -+const usage = 'node packages/webui/src/cli.mjs serve [--port N] [--tester-socket] [--checkout PATH] [--board http://127.0.0.1:7331] [--business ID] [--public-host NAME --auth-header HEADER --allow-user ID [--send] [--proof-header NAME --proof-file PATH]]'; + +-const usage = 'node packages/webui/src/cli.mjs serve [--port N] [--tester-port N] [--checkout PATH] [--board http://127.0.0.1:7331] [--business ID] [--sso [--send]] [--public-host NAME --auth-header HEADER --allow-user ID [--send] [--proof-header NAME --proof-file PATH]]'; + ++const usage = 'node packages/webui/src/cli.mjs serve [--port N] [--tester-socket] [--checkout PATH] [--board http://127.0.0.1:7331] [--business ID] [--sso [--send]] [--public-host NAME --auth-header HEADER --allow-user ID [--send] [--proof-header NAME --proof-file PATH]]'; + try { + if (args.length === 1 && ['--help', '-h'].includes(args[0])) { console.log(usage); process.exit(0); } + if (args.shift() !== 'serve') throw new Error(usage); + const options = { board: DEFAULT_BOARD, port: 7330 }, preview = {}, proofFlags = {}; + -- let sendOn = false, testerPort = null; + -+ let sendOn = false, testerSocket = false; + +- let sendOn = false, ssoOn = false, testerPort = null; + ++ let sendOn = false, ssoOn = false, testerSocket = false; + while (args.length) { + const flag = args.shift(); + // Row 76 (#1570): the one flag without a value. + if (flag === '--send') { sendOn = true; continue; } + + // Q175 phase 1 (#1690): the Console signs people in itself, from console.sso in config.json. + + if (flag === '--sso') { ssoOn = true; continue; } + + if (flag === '--tester-socket') { testerSocket = true; continue; } + + if (flag === '--tester-port') throw new Error('--tester-port retired; activate explicitly with --tester-socket'); + const value = args.shift(); + @@ packages/webui/src/cli.mjs: try { + else throw new Error(`unknown option: ${flag}`); + } + - if (testerPort !== null && (testerPort === options.port || testerPort === 7332)) throw new Error('--tester-port must differ from --port and cannot be the forwarded port 7332'); + + if (ssoOn && (Object.keys(preview).length || Object.keys(proofFlags).length)) throw new Error('--sso cannot be combined with --public-host, --auth-header, --allow-user or --proof-*'); + options.preview = previewOptions(preview); + const proof = proofOptions(proofFlags, options.preview); + - if (sendOn && !options.preview) throw new Error('--send needs the preview mode (--public-host, --auth-header and --allow-user)'); + @@ packages/webui/src/cli.mjs: try { + - } catch (err) { + const why = redactor({ configDir: configDir() })(err.message); + + if (ssoOn) throw new Error(`--sso needs the system config (${why})`); + if (sendOn) throw new Error(`--send needs the bus configured (${why})`); + - if (testerPort !== null) throw new Error(`--tester-port needs the bus configured (${why})`); + + if (testerSocket) throw new Error(`--tester-socket needs the bus configured (${why})`); + busLine = `Bus: not configured (${why})`; + } + const reads = consoleReads({ root: REPO, queueRoot: options.checkout ?? REPO, system, configDir: configDir(), rolesDir: rolesDir(), business: options.business ?? null }); + - const server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, proof, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite }); + +@@ packages/webui/src/cli.mjs: try { + + server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, sso, proof, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite }); + + } catch (err) { sso?.close(); throw err; } + let testerServer = null; + - if (testerPort !== null) { + + if (testerSocket) { + @@ packages/webui/src/cli.mjs: try { + try { + - testerServer = await startServer({ host: '127.0.0.1', port: testerPort, board: options.board, + + testerServer = await startTesterSocket({ dataRoot: system.dataRoot, board: options.board, + - tester: true, testerProofHeader: proof?.header ?? null, testerAuthHeader: options.preview?.header ?? null, bus: testerBus, reads, + + tester: true, testerNoSignIn: ssoOn, testerProofHeader: proof?.header ?? null, testerAuthHeader: options.preview?.header ?? null, bus: testerBus, reads, + projectFiles: args => call('project.files', args), projectFile: args => call('project.file', args), + instancesWrite: args => call('instances.set', args), routesWrite: args => call('routes.set', args), nameWrite: args => call('name.set', args), + @@ packages/webui/src/cli.mjs: try { + - const previewLine = options.preview ? `\nPreview: https://${options.preview.host}/ through the proxy, signed-in user from ${options.preview.header}, ${send ? 'reads and human messages to roles (--send); one line per send below' : 'read-only'}` : ''; + - const proofLine = `\nProxy: proxy proof ${proof ? 'on' : 'off'}`; + + const health = sso?.health() ?? {}; + + const ssoLines = sso ? `\nSign-in: ${sso.publicOrigin}/ (--sso), ${send ? 'reads and human writes (--send)' : 'read-only'}` + sso.summary().map(p => `\nSign-in provider: ${p.id} issuer ${p.issuer} subjects ${p.subjects} (${health[p.id]})`).join('') : ''; + const queueLine = options.checkout ? '\nQueue: read from the --checkout directory' : ''; + -- console.log(`Mosaic Console: http://127.0.0.1:${server.address().port}/\nBoard: ${options.board}\n${busLine}${queueLine}${previewLine}${proofLine}${testerServer ? `\nTester Console: http://127.0.0.1:${testerServer.address().port}/ (binds the tester)` : ''}\nCtrl-C stops this server.`); + -+ console.log(`Mosaic Console: http://127.0.0.1:${server.address().port}/\nBoard: ${options.board}\n${busLine}${queueLine}${previewLine}${proofLine}${testerServer ? `\nTester Console: unix:${testerServer.address()} (binds the tester)` : ''}\nCtrl-C stops this server.`); + - for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => { server.close(); testerServer?.close(); }); + +- console.log(`Mosaic Console: http://127.0.0.1:${server.address().port}/\nBoard: ${options.board}\n${busLine}${queueLine}${previewLine}${ssoLines}${proofLine}${testerServer ? `\nTester Console: http://127.0.0.1:${testerServer.address().port}/ (binds the tester)` : ''}\nCtrl-C stops this server.`); + ++ console.log(`Mosaic Console: http://127.0.0.1:${server.address().port}/\nBoard: ${options.board}\n${busLine}${queueLine}${previewLine}${ssoLines}${proofLine}${testerServer ? `\nTester Console: unix:${testerServer.address()} (binds the tester)` : ''}\nCtrl-C stops this server.`); + + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => { sso?.close(); server.close(); testerServer?.close(); }); + } catch (err) { + console.error(`refused: ${err.message}`); + + @@ packages/webui/src/serve.mjs + import { timingSafeEqual } from 'node:crypto'; + import { resolve } from 'node:path'; + @@ packages/webui/src/serve.mjs: async function projectFileRead(res, read, path, search) { + - // `send` is the bus write (row 76): `args => Promise<{id}>`, only with the preview mode. `log` + + // `testerNoSignIn` makes the tester listener refuse anything that looks like a sign-in (--sso). `log` + // takes one line per send attempt that got past sign-in; never the body. `fieldsFile` is for + // tests; the field table is otherwise the committed packages/tasks/fields.json. + --export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, tester = false, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, log = line => console.log(line), fieldsFile = undefined } = {}) { + -+export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, tester = false, testerSocket = null, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, log = line => console.log(line), fieldsFile = undefined } = {}) { + +-export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerNoSignIn = false, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, log = line => console.log(line), fieldsFile = undefined } = {}) { + ++export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerSocket = null, testerNoSignIn = false, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, log = line => console.log(line), fieldsFile = undefined } = {}) { + if (!isLoopback(host)) throw new Error('refusing to bind to non-loopback host'); + if (host === 'localhost') host = '127.0.0.1'; + if (tester && preview !== null) throw new Error('tester listener cannot use preview mode'); + +@@ packages/webui/src/serve.mjs: export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF + if (tester && host !== '127.0.0.1') throw new Error('tester listener must bind 127.0.0.1'); + // Row 177 (#1675): chat is allowed. The tester listener's chat binds the tester, so the bus keeps it on tester threads. + if (tester && [send, settingsWrite, projectSave, assistantStop].some(value => value !== null)) throw new Error('tester listener permits only instance, route, name and chat writes'); + @@ packages/webui/src/serve.mjs: export async function startServer({ host = '127.0. + if (!tester && Object.hasOwn(req.headers, 'x-mosaic-tester')) return json(res, 403, { error: 'tester listener required' }); + if (tester && testerAuthHeader && Object.hasOwn(req.headers, testerAuthHeader.toLowerCase())) return json(res, 403, { error: 'proxy header refused' }); + if (tester && testerProofHeader && Object.hasOwn(req.headers, testerProofHeader.toLowerCase())) return json(res, 403, { error: 'proxy proof header refused' }); + -+ if (tester && (Object.hasOwn(req.headers, 'authorization') || /(?:^|;\s*)__Host-mosaic_(?:session|login)=/.test(req.headers.cookie ?? ''))) return json(res, 403, { error: 'tester listener takes no sign-in' }); + +- // Under --sso the tester listener reads no cookie and takes no sign-in of any kind (design section 6). + +- if (tester && testerNoSignIn && (Object.hasOwn(req.headers, 'authorization') || SIGN_IN_COOKIE.test(req.headers.cookie ?? '') || Object.keys(req.headers).some(h => h.startsWith('x-authentik-')))) + +- return json(res, 403, { error: 'tester listener takes no sign-in' }); + ++ if (tester && (Object.hasOwn(req.headers, 'authorization') || SIGN_IN_COOKIE.test(req.headers.cookie ?? '') || Object.keys(req.headers).some(h => h.startsWith('x-authentik-')))) return json(res, 403, { error: 'tester listener takes no sign-in' }); + let path, search; + // Node keeps the first of repeated Host headers; a second one (a loopback Host, then NAME) is + // never legitimate, so it is refused before any check reads the Host. + @@ packages/webui/src/serve.mjs: export async function startServer({ host = '127.0. + const values = req.headersDistinct[preview.header]; + if (!values || values.length !== 1 || values[0] !== preview.user) return json(res, 401, { error: 'sign-in required' }); + } + -- const writer = tester ? { tester: true, user: 'tester', origin: `http://127.0.0.1:${server.address().port}` } : preview; + -+ const writer = tester ? { tester: true, user: 'tester', origin: TESTER_ORIGIN } : preview; + +- const writer = tester ? { tester: true, user: 'tester', origin: `http://127.0.0.1:${server.address().port}` } : signer; + ++ const writer = tester ? { tester: true, user: 'tester', origin: TESTER_ORIGIN } : signer; + if (tester && !['GET', 'HEAD'].includes(req.method) && !['/api/instances', '/api/routes', '/api/name'].includes(path) && !TESTER_CHAT_WRITE.test(path)) + return json(res, 403, { error: 'read-only', sent: false }); + if(path==='/api/settings'&&req.method==='POST'){ + @@ packages/webui/tests/browser.mjs: export async function browser({ machine = true + + ## packages/webui/tests/proof-cli.test.mjs ## + @@ + + import { leasePort } from '../../../scripts/testing/lane-fixtures.mjs'; + + import { after as laneAfter } from 'node:test'; + +import { testerRequest } from '../../cli/src/tester-http.mjs'; + import test from 'node:test'; + import assert from 'node:assert/strict'; + @@ packages/webui/tests/proof-cli.test.mjs: test('serve argv carries only fixture p + test('CLI applies proof on the main listener and rejects that header on its tester listener', async t => { + const f = await testerFixture(t), file = join(f.root, 'proof.fixture'); + writeFileSync(file, VALUE, { mode: 0o600 }); + -- const free = createServer(); free.listen(0, '127.0.0.1'); await once(free, 'listening'); + -- const testerPort = free.address().port; await new Promise(r => free.close(r)); + +- const testerPort = await leasePort({after:laneAfter}, {closed:true}); + - const child = spawn(process.execPath, [CLI, 'serve', '--port', '0', '--board', 'http://127.0.0.1:9', '--tester-port', String(testerPort), '--business', 'acme', ...PREVIEW, '--proof-header', 'x-console-proof', '--proof-file', file], { env: f.f.env, stdio: ['ignore', 'pipe', 'pipe'] }); + + await new Promise(r=>f.web.close(r)); + + const child = spawn(process.execPath, [CLI, 'serve', '--port', '0', '--board', 'http://127.0.0.1:9', '--tester-socket', '--business', 'acme', ...PREVIEW, '--proof-header', 'x-console-proof', '--proof-file', file], { env: f.f.env, stdio: ['ignore', 'pipe', 'pipe'] }); + @@ packages/webui/tests/service.test.mjs: test('proxy proof status retains send and + + assert.equal(both.status,4); + +}); + + + ## packages/webui/tests/sso.test.mjs ## + +@@ packages/webui/tests/sso.test.mjs: import { tmpdir } from 'node:os'; + + import { join } from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { startServer } from '../src/serve.mjs'; + ++import { startTesterSocket } from '../src/tester-socket.mjs'; + ++import { testerRequest } from '../../cli/src/tester-http.mjs'; + + import { createSso, readSlot, SESSION_COOKIE, LOGIN_COOKIE } from '../src/sso.mjs'; + + import { close } from './fixture.mjs'; + + import { b64url, ecKey, fakeOidc, jwt, psKey, rsaKey } from './helpers/fake-oidc.mjs'; + +@@ packages/webui/tests/sso.test.mjs: test('11: the CLI refuses --sso with the old flags and without console.sso, and + + + + test('12: under --sso the tester listener refuses each kind of sign-in, has no /auth, and startServer refuses tester with sso', async t => { + + const env = await setup(t); + +- const tester = await startServer({ port: 0, board: 'http://127.0.0.1:9', bus: env.bus, tester: true, testerNoSignIn: true, log: () => {} }); + ++ const dataRoot = mkdtempSync(join(tmpdir(),'g196-sso-')); + ++ t.after(()=>rmSync(dataRoot,{recursive:true,force:true})); + ++ const tester = await startTesterSocket({ dataRoot, board: 'http://127.0.0.1:9', bus: env.bus, testerNoSignIn: true, log: () => {} }); + + t.after(() => close(tester)); + +- const port = tester.address().port, host = `127.0.0.1:${port}`; + ++ const callTester = options => testerRequest({dataRoot,...options}); + + const { session, g, begun } = await signIn(env); + + assert.ok(session); + +- const plain = await call(port, { path: '/api/config', host }); + ++ const plain = await callTester({ path: '/api/config' }); + + assert.equal(plain.status, 200, 'control: a request with no sign-in is served'); + + for (const headers of [ + + { cookie: session }, + +@@ packages/webui/tests/sso.test.mjs: test('12: under --sso the tester listener refuses each kind of sign-in, has no / + + { 'x-authentik-jwt': g.idToken }, + + { 'X-Authentik-Username': 'jason.woltje' }, + + ]) { + +- const r = await call(port, { path: '/api/config', host, headers }); + ++ const r = await callTester({ path: '/api/config', headers }); + + assert.equal(r.status, 403, JSON.stringify(Object.keys(headers))); + + assert.deepEqual(r.body, { error: 'tester listener takes no sign-in' }); + + } + +- assert.equal((await call(port, { path: '/auth/login', host })).status, 404); + ++ assert.equal((await callTester({ path: '/auth/login' })).status, 404); + + await assert.rejects(startServer({ port: 0, tester: true, sso: env.sso }), /tester listener cannot use sso/); + + }); + + + + + ## packages/webui/tests/tester-browser-fixture.mjs (new) ## + @@ + +// UI QA only: CDP fulfills this browser's synthetic-origin requests from a scratch Unix + @@ packages/webui/tests/tester-socket.test.mjs (new) + +}); + + ## scripts/stack-release.sh ## + -@@ scripts/stack-release.sh: COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies" + +@@ scripts/stack-release.sh: source scripts/instance.sh + + COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies" + BIN="$HOME/.npm-global/bin" + UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" + - CONSOLE_UNIT=mosaic-webui.service + -TIMEOUT=60 WANT_PM=0 AFTER_HARNESS=0 MIGRATE=0 PLAN=0 RESTORE="" EVENTS="" TESTER_PORT="" + +TIMEOUT=60 WANT_PM=0 AFTER_HARNESS=0 MIGRATE=0 PLAN=0 RESTORE="" EVENTS="" TESTER_PORT="" TESTER_SOCKET=0 EXPECTED_INTERPRETER="" + - PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 R_BACKUP="" R_PORT="" + + PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 NO_SEND=0 SSO=0 R_BACKUP="" R_PORT="" + + UNITS=user TO_FLAG="" EXPECT_FROM="" SINCE="" ROLLBACK_OLD="" CAUSE="" + + usage() { + cat >&2 <<'USAGE' + usage: stack-release.sh adopt BUSINESS + - stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N] + + stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)] + - [--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan] + + [--proof-header NAME --proof-file PATH | --no-proof | --sso] [--no-send] [--timeout S] [--plan] + stack-release.sh pm-on BUSINESS [--timeout S] + stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan] + @@ scripts/stack-release.sh: parse_opts() { + @@ scripts/stack-release.sh: parse_opts() { + --proof-header) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_HEADER="$2"; shift 2 ;; + --proof-file) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_FILE="$2"; shift 2 ;; + @@ scripts/stack-release.sh: parse_opts() { + - *) usage ;; + - esac + - done + + [ "$OP" != confirm ] && [ "$OP" != revert ] && [ "$OP" != gate ] && [ -z "$TO_FLAG" ] && [ -z "$EXPECT_FROM" ] || usage + + else + + [ "$OP" != rollback ] || [ -n "$TO_FLAG" ] || usage + +- [ "$WANT_PM" = 0 ] && [ -z "$TESTER_PORT" ] && [ -z "$PROOF_HEADER" ] && [ -z "$PROOF_FILE" ] && [ "$NO_PROOF" = 0 ] && [ "$NO_SEND" = 0 ] && [ "$SSO" = 0 ] || usage + ++ [ "$TESTER_SOCKET" = 0 ] && [ "$WANT_PM" = 0 ] && [ -z "$TESTER_PORT" ] && [ -z "$PROOF_HEADER" ] && [ -z "$PROOF_FILE" ] && [ "$NO_PROOF" = 0 ] && [ "$NO_SEND" = 0 ] && [ "$SSO" = 0 ] || usage + + fi + + [ "$OP" != gate ] || [ -n "$SINCE" ] || usage + + [ -z "$EXPECT_FROM" ] || [ "$PLAN" = 1 ] || usage + + [ "${ROLLBACK_OLD:+1}" = "${CAUSE:+1}" ] || usage + + [ "$TESTER_SOCKET" = 0 ] || [ -z "$TESTER_PORT" ] || usage + [ -z "$EVENTS" ] || [ -n "$RESTORE" ] || usage + [ "$OP" != rehearse ] || { [ -n "$R_BACKUP" ] && [ -n "$R_PORT" ]; } || usage + # A bad tester port is a usage error; --plan lists it as tester-port-invalid instead (row 180). + @@ scripts/stack-release.sh: gate_once() { + - local copy="$COPIES/$1" pm="$2" main out + - GATE=units-inactive GATE_WHY="" + - systemctl --user is-active --quiet "$BUS_UNIT" && systemctl --user is-active --quiet "$CONSOLE_UNIT" || return 1 + + [ "$out" -ge "$FLOOR" ] || { GATE_WHY="the bus started at @$out, before @$FLOOR"; return 1; } + + START_MS=$((out * 1000)) + + fi + + if [ -f "$copy/packages/bus/src/interpreter.mjs" ]; then + + GATE=interpreter-receipt + + node -e 'import(require("url").pathToFileURL(process.argv[1]).href).then(m=>{const expected=m.inspectInterpreter();if(process.argv[4]&&JSON.stringify(expected)!==process.argv[4])process.exit(1);m.verifyInterpreterReceipt(process.argv[2],expected,process.argv[3]);}).catch(()=>process.exit(1));' -- "$copy/packages/bus/src/interpreter.mjs" "$DATA/bus" "$START_MS" "$EXPECTED_INTERPRETER" || return 1 + + fi + GATE=bus-status + - main="$(systemctl --user show "$BUS_UNIT" -p MainPID | sed -n 's/^MainPID=//p')" || return 1 + + main="$(sc show "$BUS_UNIT" -p MainPID | sed -n 's/^MainPID=//p')" || return 1 + out="$("$copy/scripts/mosaic" bus status --json 2>/dev/null)" || { GATE_WHY="bus status failed"; return 1; } + @@ scripts/stack-release.sh: flags_drift() { + # unit still refuses drift and a failed attempt can restore exactly the captured stack. + @@ scripts/stack-release.sh: flags_drift() { + + local supported=0 socket_supported=0 main=7332 inherited="" inherited_socket=0 i + + local cli="$COPIES/$TO/packages/webui/src/cli.mjs" + local -a without_tester=() + + sso_base + - grep -q -- '--tester-port' "$COPIES/$TO/packages/webui/src/cli.mjs" && supported=1 + + if grep -q 'MOSAIC_TESTER_SOCKET_V1' "$cli"; then socket_supported=1 + + elif grep -q -- '--tester-port' "$cli"; then supported=1; fi + - for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do + -- if [ "${P_CONSOLE[i]}" = --tester-port ]; then + -- i=$((i + 1)); inherited="${P_CONSOLE[i]:-}" + + for ((i = 0; i < ${#BASE_CONSOLE[@]}; i++)); do + +- if [ "${BASE_CONSOLE[i]}" = --tester-port ]; then + +- i=$((i + 1)); inherited="${BASE_CONSOLE[i]:-}" + - else + -- without_tester+=("${P_CONSOLE[i]}") + -- if [ "${P_CONSOLE[i]}" = --port ]; then main="${P_CONSOLE[i+1]:-7332}"; fi + +- without_tester+=("${BASE_CONSOLE[i]}") + +- if [ "${BASE_CONSOLE[i]}" = --port ]; then main="${BASE_CONSOLE[i+1]:-7332}"; fi + - fi + -+ case "${P_CONSOLE[i]}" in + -+ --tester-port) i=$((i + 1)); inherited="${P_CONSOLE[i]:-}" ;; + ++ case "${BASE_CONSOLE[i]}" in + ++ --tester-port) i=$((i + 1)); inherited="${BASE_CONSOLE[i]:-}" ;; + + --tester-socket) inherited_socket=1 ;; + -+ *) without_tester+=("${P_CONSOLE[i]}") + -+ if [ "${P_CONSOLE[i]}" = --port ]; then main="${P_CONSOLE[i+1]:-7332}"; fi ;; + ++ *) without_tester+=("${BASE_CONSOLE[i]}") + ++ if [ "${BASE_CONSOLE[i]}" = --port ]; then main="${BASE_CONSOLE[i+1]:-7332}"; fi ;; + + esac + done + -- RUN_CONSOLE=("${P_CONSOLE[@]}") + +- RUN_CONSOLE=("${BASE_CONSOLE[@]}") + - DROP_TESTER="" + - if [ -n "$TESTER_PORT" ]; then + - # --plan lists both with its other refusals (row 180); outside it they end the run. + -+ RUN_CONSOLE=("${P_CONSOLE[@]}") DROP_TESTER="" EXPECTED_INTERPRETER="" + ++ RUN_CONSOLE=("${BASE_CONSOLE[@]}") DROP_TESTER="" EXPECTED_INTERPRETER="" + + if [ "$socket_supported" = 1 ]; then + + [ -z "$TESTER_PORT" ] || check_refuse tester-port-target "$TO retires tester TCP; use --tester-socket" + + if [ -n "$inherited" ] && [ "$TESTER_SOCKET" = 0 ]; then + @@ scripts/stack-release.sh: console_for_target() { + - DROP_TESTER="--tester-port $inherited" + + RUN_CONSOLE=("${without_tester[@]}") DROP_TESTER="--tester-port $inherited" + fi + - proof_for_target + - } + + if [ "$SSO" = 1 ]; then sso_for_target; else proof_for_target; fi + + send_for_target + +@@ scripts/stack-release.sh: sso_base() { + + case "${SERVE_FLAGS[i]}" in + + --send) ;; + + --tester-port) i=$((i + 1)) ;; + ++ --tester-socket) ;; + + *) BASE_CONSOLE+=("${SERVE_FLAGS[i]}") ;; + + esac + + done + +@@ scripts/stack-release.sh: sso_base() { + + case "${P_CONSOLE[i]}" in + + --send) BASE_CONSOLE+=(--send) ;; + + --tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port "${P_CONSOLE[i]:-}") ;; + ++ --tester-socket) BASE_CONSOLE+=(--tester-socket) ;; + + esac + + done + + SSO_PLAN="sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them" + +@@ scripts/stack-release.sh: sso_for_target() { + + done + + RUN_CONSOLE=() + + for ((i = 0; i < ${#kept[@]}; i++)); do + +- case "${kept[i]}" in --send | --tester-port) break ;; esac + ++ case "${kept[i]}" in --send | --tester-port | --tester-socket) break ;; esac + + RUN_CONSOLE+=("${kept[i]}") + + done + + RUN_CONSOLE+=(--sso "${kept[@]:i}") + @@ scripts/stack-release.sh: proof_for_target() { + fi + if [ -n "$PROOF_HEADER" ]; then + @@ scripts/stack-release.sh: proof_for_target() { + done + RUN_CONSOLE+=(--proof-header "$header" --proof-file "$file" "${without_proof[@]:i}") + @@ scripts/stack-release.sh: plan() { + - [ -z "$DROP_TESTER" ] || echo "drop $DROP_TESTER; the target Console does not support it" + - echo "$PROOF_PLAN" + - echo "Console: scripts/webui-service.sh install --copy $TO --copies $COPIES${RUN_CONSOLE[*]:+ ${RUN_CONSOLE[*]}}" + + echo "Console: scripts/webui-service.sh install $flags--copy $TO --copies $COPIES${RUN_CONSOLE[*]:+ ${RUN_CONSOLE[*]}}" + + fi + + echo "CLI: scripts/stack-copy.sh wrapper $TO $flags--copies $COPIES --bin $BIN" + + [ -z "$EXPECTED_INTERPRETER" ] || echo "interpreter: $EXPECTED_INTERPRETER" + - echo "CLI: scripts/stack-copy.sh wrapper $TO --copies $COPIES --bin $BIN" + local r + for r in "${PLAN_REFUSE[@]}"; do echo "refuses $r"; done + + [ "${#PLAN_REFUSE[@]}" = 0 ] || exit 3 + @@ scripts/stack-release.sh: plan() { + + # Steps 4.1 to 4.5 and the gate for target $TO. $1 is the store step: migrate or restore. + @@ scripts/webui-service.sh + # NAME --auth-header HEADER --allow-user ID, all three or none, and --send + # (row 83), taken only with the preview set: the unit's Console then sends + # human messages to roles and needs the system config at start. systemd + -@@ scripts/webui-service.sh: REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + - TEMPLATE="$REPO/packages/webui/systemd/mosaic-webui.service.in" + - UNIT_NAME="mosaic-webui.service" + - USAGE="usage: scripts/webui-service.sh copy COMMIT [--copies DIR] [--allow-worktree] | render [--copy ID [--copies DIR]] [FLAGS] | install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [--allow-worktree] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR] + -- FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-port N] [--proof-header NAME --proof-file PATH] + +@@ scripts/webui-service.sh: instance_select + + UNIT_NAME="$CONSOLE_UNIT" + + BUS_UNIT_PREFIX="${BUS_TEMPLATE%.service}" + + USAGE="usage: scripts/webui-service.sh copy COMMIT [--copies DIR] [--allow-worktree] | [--instance NAME] render [--copy ID [--copies DIR]] [FLAGS] | install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [--allow-worktree] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR] + +- FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-port N] [--proof-header NAME --proof-file PATH] + - --tester-port: 1024..65535, different from --port and never 7332; loopback tester listener. + -+ FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH] + -+ --tester-port: legacy copies only; 1024..65535, different from --port and never 7332. + ++ FLAGS: [--port N] [--business ID] [--board URL] [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]] [--tester-socket | --tester-port N (legacy copies)] [--proof-header NAME --proof-file PATH] + ++ --tester-port: legacy copies only; 1024..65535, different from --port and never 7332; loopback tester listener. + --allow-worktree is for the tests only: copy and install otherwise refuse outside the canonical checkout." + + die() { echo "webui-service: $*" >&2; exit 1; } + -@@ scripts/webui-service.sh: DIR_FLAGS=0 + - PORT=7332 + +@@ scripts/webui-service.sh: PORT=7332 + + PORT_SET=0 + TESTER_PORT="" + TESTER_SET=0 + +TESTER_SOCKET=0 + @@ scripts/webui-service.sh: DIR_FLAGS=0 + @@ scripts/webui-service.sh: parse_flags() { + --allow-worktree) ALLOW_WORKTREE=1; shift ;; + --no-reload) RELOAD=0; DIR_FLAGS=1; shift ;; + - --port) [ $# -ge 2 ] || usage; PORT="$2"; SERVE_SET=1; shift 2 ;; + + --port) [ $# -ge 2 ] || usage; PORT="$2"; PORT_SET=1; SERVE_SET=1; shift 2 ;; + + --tester-socket) TESTER_SOCKET=1; SERVE_SET=1; shift ;; + --tester-port) [ $# -ge 2 ] || usage; TESTER_PORT="$2"; TESTER_SET=1; SERVE_SET=1; shift 2 ;; + --business) [ $# -ge 2 ] || usage; BUSINESS="$2"; SERVE_SET=1; shift 2 ;; + @@ scripts/webui-service.sh: parse_flags() { + } + + @@ scripts/webui-service.sh: serve_flags() { + - fi + + [ "$SSO" = 0 ] || flags="$flags --sso" + [ "$SEND" = 0 ] || flags="$flags --send" + [ "$TESTER_SET" = 0 ] || flags="$flags --tester-port $TESTER_PORT" + + [ "$TESTER_SOCKET" = 0 ] || flags="$flags --tester-socket" + @@ scripts/webui-service.sh: render() { + node_dir="$(dirname "$node")" + case ":$path:" in *":$node_dir:"*) ;; *) path="$node_dir:$path" ;; esac + @@ scripts/webui-service.sh: status_unit() { + - # render puts --send after the allowed user and optional proof pair, + - # before the tester port. The allowed user + + # render puts --send after the allowed user and optional proof pair, or + + # after --sso, before the tester port. The allowed user + # starts with a letter or digit (N5), so it is never the value --send. + -- if grep -Eq '^ExecStart=.* --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)? --send( --tester-port [0-9]+)?$' "$target"; then + -+ if grep -Eq '^ExecStart=.* --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)? --send( --tester-port [0-9]+| --tester-socket)?$' "$target"; then + +- if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+)?$' "$target"; then + ++ if grep -Eq '^ExecStart=.*( --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)?| --sso) --send( --tester-port [0-9]+| --tester-socket)?$' "$target"; then + echo "send: on (--send; human messages to roles)" + else + echo "send: off (read-only)" + +@@ scripts/webui-service.sh: status_unit() { + fi + local tester_port + tester_port="$(sed -n 's/^ExecStart=.* --tester-port \([0-9][0-9]*\)$/\1/p' "$target")" diff --git a/agents/gander/work/queue-252/DESIGN.md b/agents/gander/work/queue-252/DESIGN.md new file mode 100644 index 000000000..11fd0df90 --- /dev/null +++ b/agents/gander/work/queue-252/DESIGN.md @@ -0,0 +1,229 @@ +# Row 252: Console inbox Approve and Deny + +Revision 1, 2026-10-21. Gander. Reviewer Webby. Gate owner Sage. +Vikunja #435 (J2), Gitea #1753. Design only, not approved for implementation. +Brief: 640aa03f2:docs/plans/2026-10-21_console-inbox-approve.md. +Design base: row 196 rebased build 64539c96b on a36523713. Row 196 verification +and Duckworth's manifest review remain pending. Row 252 code must wait for Webby +approval, the row 196 landing, and the PRD amendment described below. + +## 1. Decisions that get buttons + +The first cut covers grant requests only. Q188 and #435 ask for that case; +turning every decision into a Console write would widen this security slice. +Other decisions keep today's option text and copyable `mosaic decide ID KEY`. + +Eligibility is determined by the bus from the original authoritative raise: + +- The decision belongs to the selected business, is open and routed to human. +- Its action is credential.mint and the broker's original decision.raise event + has a validated grant binding. A question or option containing the word + grant, an action supplied by the browser, or a copied authorization object + does not qualify. +- There are exactly two distinct current option keys. The broker's recorded + approvalChoice names one. Approve uses that key; Deny uses the other key. + Both controls display the real option text. No guessed `yes` or `no` key. +- Missing binding, ambiguous options, a superseded/closed decision, a technical + or role-routed decision, and non-grant authorization requests get no buttons. + Existing terminal handling remains available where it is available today. + +The bus adds a read-only console-resolution description to the decision view: +eligibility, the exact two keys and labels, a safe grant summary, and a digest +of decision id, business, options, approvalChoice and original grant binding. +It contains no token or key material. This description helps the UI; the final +write recomputes every check from authoritative state in its transaction. +Approving records consent and starts the existing grant workflow. It does not +claim the external enrollment or credential operation has already succeeded. +Deny resolves with the non-approval key and starts no grant executor. + +## 2. Identity and the one bus verifier + +The resolving identity is (provider id, issuer, sub) from the request's live +row 192 OIDC session. Session.name is display text, never authority. Headers, +POST fields and a shared secret cannot select the human. ID or access tokens, +session cookies and client secrets never become bus credentials or audit text. + +Extend row 196 verifyPrincipal rather than create a second verifier. Add a +Console-owner variant of its human proof with a reviewed same-tree +console-cli.mjs path and a separate MOSAIC_BUS_CONSOLE_NONCE namespace. It must: + +1. Match the accepted Unix connection's SO_PEERCRED pid and instance uid. +2. Verify the fixed Console wrapper path and initial spawn nonce, using the + existing human walk and all marker, agent binary and launch-record refusals. +3. Require the exact same-tree webui cli.mjs serve as direct parent, and reread + child/parent identity as row 196 does for Console users. No CLI respawn. +4. Bind business and the provider/issuer/sub tuple to initial per-spawn metadata. + The Console constructs these only from the session verified for this request. + The wrapper refuses body overrides. The bus compares proof metadata to that + initial environment and to trusted current console.sso owner subjects. +5. Resolve to businesses[business].human only when the tuple is an allowed + owner subject in this instance's checked console.sso configuration. Unknown + subjects, changed mapping, missing/invalid configuration and user principals + refuse. A terminal owner still works if Console configuration is unavailable. + +The runtime receives the checked owner-subject map through its trusted startup +API, never through a socket verb. Provider id alone is insufficient: issuer and +sub must match. A configuration change requires the normal reviewed re-pin and +restart; logout, idle/absolute expiry and provider refusal use live Q175 checks +on each HTTP request. No Console principal or cap is cached between requests. +Each preview and final write spawns a fresh wrapper with a fresh nonce. + +The verifier returns owner provenance including via=console and the subject +only after those checks. A separate trusted binder issues an internal transient +Console cap restricted to the preview and Console resolution operations below. +Do not weaken today's bindHuman or general #human gate to accept via=console +for every owner operation. CLI, user, tester, reader, launch and seat caps cannot +use the Console resolution operation, and a Console cap cannot invoke other +owner writes. The cap never leaves the broker and ends with the connection. + +The proof trusts the verified Console's mapping, as row 196 explicitly permits. +The bus does not independently revalidate an ID token. A hostile process on the +instance uid can inspect memory or interfere with same-uid state; this is not a +wall against that process. Separate uids narrow cross-instance reach only. +The row 196 provenance statement remains unchanged: + +The Console provenance option trusts verified code and process metadata, not an independent +bus-side OIDC exchange. That trade must be approved, not hidden behind the phrase zero trust. + +## 3. Write path and evidence + +A signed-in human opens an eligible decision and chooses Approve or Deny. +POST /api/decisions/ID/confirmation asks for a confirmation view. After all +HTTP guards, a fresh Console proof calls a read-only decision.preview.console +bus operation. The broker returns the current eligibility and digest. The +Console creates a random 256-bit confirmation token held only in server memory +as a hash. It binds the token to this live session, business, decision, exact +choice and digest, expiring after 120 seconds. Bound the store to 1024 entries; +refuse a new confirmation at capacity, rather than evict a live confirmation. +No bus write, grant, seen event or resolution occurs at this step. + +The confirmation view shows the decision id, question, selected real option +text, grant method/target and consequences. The human must click a second, +explicit `Confirm approve` or `Confirm deny`. Cancel sends no resolution. +No modal or notification opening resolves a decision. Render all content as +text with the existing escaping rules, never as trusted markup. + +POST /api/decisions/ID/resolve takes only choice and confirmation. The Console +rechecks the live session and HTTP guards, matches and consumes that session's +confirmation once before awaiting the bus, then spawns a fresh wrapper. The +restricted operation decision.resolve.console carries id, choice and the +expected digest. The browser cannot submit via, human, provider, issuer, sub, +grant fields, a principal or a role. Those fields are refused, not ignored. + +The broker rechecks restricted Console identity, business, authoritative grant +eligibility, current option and digest, and open status in the same transaction +that appends the resolution. This operation shares the existing decision.resolve +transaction and notification behavior rather than duplicate that machinery. +Only its explicit provenance/eligibility gates differ. Existing CLI decide and +role resolution keep their current authority and evidence. + +Evidence is append-only. decision_events.by stays the configured business human +and decision_events.via becomes console. The accompanying human.input event in +the same transaction records decision, choice, op=resolved, via=console and the +subject object {provider, iss, sub}, with a reference to the decision event's +sequence. Trail output shows this provenance. Do not put the subject in a +browser-supplied note or replace the human id with a display name. No token, +nonce, confirmation value or cookie is logged. This uses existing event JSON; +no new schema column is required for the subject. + +Current #approvedGrant requires via=cli. It must explicitly accept via=console +only when that exact decision event has matching trusted Console human.input +provenance, owner and subject under this design. Merely widening its via string +check is insufficient. CLI proofs remain valid as before. Existing grant +single-consumption and executor rechecks remain authoritative, including the +host and host-enroll starts after commit. Deny, refusal and failed transactions +start no executor. Tests must cover this guard, since a UI success with no +usable grant would fail J2 just as an unauthorized grant would. + +## 4. Guards, concurrency and refusal contract + +The main listener requires --sso and --send for these routes. --no-send is a +release-tool choice that produces a unit without --send. Read-only mode rejects +both confirmation and resolve. The tester listener never offers either route. +Proxy preview mode, loopback owner access without SSO and a seat credential do +not enable them. Q175 session and Host guards run before any decision lookup. + +POST requires one exact Origin equal to the configured HTTPS public origin, +one Sec-Fetch-Site equal to same-origin, and application/json. No CORS, GET +mutation, form-encoded body, absent metadata or duplicate header acceptance. +Use the existing small JSON body limit and strict field validation. The +confirmation token is an additional session-bound action guard, not a reusable +identity credential. Neither confirmation nor resolution trusts a Referer. + +| Condition | HTTP/code | Message | +|---|---|---| +| Missing, expired, logged-out or refused session | 401 sign-in-required | Sign in again before answering. | +| Without --sso, without --send, or --no-send | 403 read-only | Console decision answers are not enabled. Use mosaic decide. | +| Wrong/repeated Origin, wrong/missing fetch metadata | 403 cross-origin | Answer from the signed-in Console page. | +| Tester route | 404 not-found | Not found. | +| Agent, seat, user, wrong parent/uid, unknown owner subject | 403 human-required | The bus refused this human identity. | +| Peer helper failure | 403 peercred-required | The bus could not verify this connection. | +| Wrong method on a main Console decision endpoint | 405 method-not-allowed | Use POST. | +| Wrong content type, malformed JSON, unknown/body identity fields | 400 invalid-request | Invalid decision answer. | +| Body over existing limit | 413 request-too-large | Decision answer is too large. | +| Decision absent or in another business | 404 decision-not-found | This decision is unavailable. | +| Open decision outside this grant-only slice | 403 decision-not-eligible | Answer this decision with mosaic decide. | +| Decision closed, superseded or concurrently resolved | 409 decision-closed | This decision is no longer open. Refresh the inbox. | +| Option removed/changed or digest changed | 409 decision-stale | The choices changed. Review the decision again. | +| Missing, expired, consumed or mismatched confirmation | 409 confirmation-required | Review and confirm this answer again. | +| Confirmation store full | 503 confirmation-unavailable | Confirmation is unavailable. Try again later. | +| No bus or lost response after submission | 503 outcome-unknown | The answer may have reached the bus. Refresh before trying again. | + +The public messages above are the endpoint contract; map existing bus refusal +names explicitly where they differ. No 200 for a refusal. A successful resolve +returns the recorded decision id, choice and resolution state, plus grant +workflow status if returned by the broker. The UI disables both action buttons +while submitting. A second click cannot reuse a consumed confirmation; a +concurrent fresh confirmation still loses at the broker's closed-state check. +Only one resolution event, human.input event, notification and grant start may +result. On uncertain transport, never retry automatically. Refresh reads the +actual result; a different unresolved answer requires fresh confirmation. + +Expiry/logout before either POST refuses. A logout racing an already-admitted +bus transaction cannot cancel an effect already committed; no claim to the +contrary is made. Session checks immediately before spawning bound this limit. + +## 5. Inbox without SSO and read-only presentation + +Hide the buttons completely without --sso. Keep the exact copyable mosaic decide +lines and explain that Console answers need sign-in and writes enabled. Under +SSO but without --send, also show the terminal lines and read-only explanation. +Do not present a disabled Approve button that suggests sign-in alone grants +write authority. With SSO and send enabled, only eligible grant decisions gain +the two controls. Non-grant decisions keep terminal lines. Closed decisions show +the recorded answer and provenance, with no action controls or confirmation. + +Server-provided UI capabilities are presentation only. Direct POSTs receive all +the same guards. A stale page cannot restore a session, write mode or decision. +The live instance stays unchanged until Jason's phase 2 window and an authorized +re-pin whose tree includes this row and its documented flag meaning. + +## 6. Stand-in tests, mutants and landing gates + +Use row 192's fake OIDC provider, synthetic subjects and scratch HTTP listeners; +use the real test bus and row 196 kernel-peer fixtures. Never use the live +provider, origin, ports, credentials, units or instance roots. All scratch state +uses the approved /mnt/storage/scratch/tmp TMPDIR. No real second uid. + +| Evidence | Required cases | Mutants | +|---|---|---| +| Eligibility | bound grant, two actual keys, other decisions/technical/roles/ambiguous options hidden and POST refused | J1 trust browser eligibility; J2 hardcode yes/no | +| Owner provenance | live fake OIDC owner works; header/name/body/seat/user impersonation, agent parent, other tree, bad nonce and wrong peer refuse | J3 use name/header; J4 skip Console-parent walk; J5 accept other nonce namespace | +| Request lifetime | new proof per preview/resolve; session replaced/expired/logged out refuses; cap never returned and releases on success/failure/close | J6 reuse prior session/principal; J7 leak cap | +| HTTP and confirmation | all refusal table cases; missing/duplicate Origin and metadata, token from another session/decision/choice, expiry, capacity, cancel and direct POST | J8 skip CSRF; J9 accept unconfirmed write; J10 unbind confirmation | +| Business and races | foreign business, closed decision, stale digest/key, parallel confirmations, double click and lost reply; exactly one durable result/notification | J11 skip business/digest/closed check; J12 permit confirmation reuse | +| Evidence and grant effect | via=console plus verified subject in same transaction, CLI unchanged; approval consumable once by existing grant path, deny/refusal no executor | J13 write cli or omit subject; J14 widen via without provenance; J15 execute on deny | +| UI/browser | hidden without SSO/send, terminal fallback, escaped text, confirm/cancel, pending state, stale/error refresh, no automatic retry | J16 enable without SSO/send; J17 auto-submit or retry | + +Browser tests use the repository's isolated fixture infrastructure. The reviewer +must inspect which runtime/UI boundary each mutant actually weakens. Record +restored hashes and clean Git status for each. Suites and mutant batches need +Sage's slot; stop/report under mos-0823. Final acceptance is Webby's approved +manifest and Sage's whole bus/WebUI/CLI and scripts/test-*.sh gate. + +Before implementation, Webby must approve this design and the REQ-WEB-2 / +REQ-DEC-3 amendment must be landed through Mos and Jason. The PRD currently says +human resolution is CLI-only. Decision 00ed58e2 reserves the Console amendment; +this design does not edit the PRD or treat its reservation as landed policy. +Row 252 code branches from row 196's reviewed landing and keeps its own review +and gate. No live enablement, reachability change or independent auth mechanism.