diff --git a/agents/darkwing/work/s6-review/r2/candidate-manifest.sha256 b/agents/darkwing/work/s6-review/r2/candidate-manifest.sha256 new file mode 100644 index 00000000..fbef2ba7 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/candidate-manifest.sha256 @@ -0,0 +1,42 @@ +863640ee44598a5bffd6d37d328dcddfd4bdc671f792d029ae2ee18b1055b28b adapters/README.md +8121e4e05b0559471e0d44fc0325fb08c8a883ab3db1bca451a956753cccdfe3 adapters/claude/adapter.sh +962255271e95cb30788e97cd9e86e17f384dd5f74cead435692e8b30e47af9cf adapters/pi/adapter.sh +009059ea86e1d14c5b12ed0fdad64e8cd501e19021ef6f4148a1463d55860125 docs/TOOLS.md +49dc3cf603358fdbce768faaa9ebe8b5e4359a1aa813c3ffeef0d96a01d37035 packages/bus/README.md +aa71088d656455de83d9c1a42745852041ee61e2d5eb6f4c1e48e8ae29623433 packages/bus/src/broker.mjs +0b51592777d2b035e79e2864d061615e81591bf99d43820ea9b3e52f8cf56559 packages/bus/src/process.mjs +12634ff6b6ef5b5a282bb306b30c9f02929d1fb6597e149d47d21069201399b5 packages/bus/src/runtime.mjs +5b06f799e18deba2ee2eb737322f0dcfdd4a8eeae8c92e465f5acdbb894483dd packages/bus/tests/end-launch.test.mjs +e5e41c8bef670daac0507d860f7104c446c736a3897d247cbacb5ab36b440d41 packages/cli/README.md +bd207b81a2b9965b9e46da66ab31ed9a587b87e8669e8293184d4b842e45bff5 packages/cli/src/cli.mjs +e9eeec4bef3384b5b15d1e7a2c9d913d2f3e329228c3e3e2cacb8f741aa21379 packages/cli/src/host.mjs +2d0b075982f295a88161607d2795aadc86f286994ab6cc31873b83b2daebf7fa packages/cli/src/launcher.mjs +9b30a3bfe96a5d7c6956b6c75196c08bc35ceec302859337915ca1cfe2a76b76 packages/cli/tests/fixtures/launch-host.mjs +436c15af25c48a8135d6b4bbb7df26d318b87da32fc6432455f23043a07e791c packages/cli/tests/host.test.mjs +670e3a8dd3e57b96b96f6932693853e1149e53da577299931cbe3d428ddb89da packages/cli/tests/launcher.test.mjs +709bd331bb0d76f28b464e518f4e1fc3bb0b303614e79d7e82479dcd679043cc packages/cli/tests/verbs.test.mjs +07f8033da769b18c194520356f900dd10a12d146d4d10f1a80e3c40a2a60655e packages/harness/README.md +34ef8212e27f052223443c66830839517f7a54ecb6ef7d85795b3e89c65b4d4c packages/harness/package.json +22efd0bed7991561920ad29f6bf9a1ab2d7384185fab4267684a037e026d8e85 packages/harness/src/bundle.mjs +32b5090760c95eea0e1232ff36ec13a1d9b58335b2eade621196dec1ebdbe784 packages/harness/src/claude-gate.mjs +570a1e64db8624b57eb972fd7599c7543e3c41324d0e9666cb5701d5491ec39b packages/harness/src/gate.mjs +71e00113b8e5b55b410c7aae6232f76e972fc77aa68afa893da45c6b78d297e2 packages/harness/src/mcp-server.mjs +d19753fa72f0b57e751749359644093713bcb650bfac566f55d2bc05cb817121 packages/harness/src/pi-extension.mjs +1047aa092080e92efde2044dddaf82bacf428ed4b143c3846693471481f3612e packages/harness/src/runner.mjs +92153d9e2161ceef4ee76f2ff992014760a8c62b0ea709b51e2ea0ce965d3edb packages/harness/src/tools.mjs +96796238b7effab78cc6356ed8ea163f02aa39999d8dfc97fef83d45309d6574 packages/harness/tests/bundle.test.mjs +96524da43b212e84d78108cbbf05eef324c934f3ad9e0d4cb2edcb0f3df256d4 packages/harness/tests/claude-gate.test.mjs +197ec0f6c842552bea65fb2ab4c8cb8615fd6e691a2d0a592e1465d18a45d75f packages/harness/tests/claude-session.test.mjs +8392172b243356932c974bdca9b4c449a312ae7a042ee7a22e0f22fbb98dbec9 packages/harness/tests/fixtures/fake-adapter.mjs +8576749388b3f962ed6cbd694d9af814665f3f491f278a3cdf9b9c5435388460 packages/harness/tests/gate.test.mjs +6e7509cfe6ad909440c25b750528360c0ba617c6c68b05d400bcd94b481c1f76 packages/harness/tests/helpers.mjs +793eb11f970e4a8eecddec4dc48c24331e4de795bc04bfadf806a2fc3a15a8b4 packages/harness/tests/mcp-server.test.mjs +ba907e3bee4547c97c8700b4b19febc46c19074a259dd113b727522d68115273 packages/harness/tests/pi-session.test.mjs +c8f23144316501c3e163cf5a5566ddee552b3c93dee13594ae8c1eac66aedc64 packages/harness/tests/runner.test.mjs +ef9130a3cb1ca3e278a8ed370060afd1145d1ceb211a915e83d75c8346b04931 packages/harness/tests/tools.test.mjs +4e34456187387b02fa6d996c270dea4076b5d57952cddaa01f7a04843b351a02 packages/seat/README.md +382cbf7e0ff336911e288ce858bdbfbec693bc2b69879720d1f0b4c7d8455198 packages/seat/src/proc.mjs +5e181204de871d4f1098f5a63b5f80d87a44f5c01bf150101a6690bb1ccdca3d packages/seat/src/session.mjs +9a505d255c61034b3be738d359bc4a10acf08916c65675ee14dab2e29c060b04 packages/seat/tests/session.test.mjs +482ad6167fc96bf86928e0b467b3e173b174508fd913e297a8164d73f334d031 scripts/agent-host-dev.sh +b37d673aa5ce72c10b49018d8ffd9460f393eff7b638f1aa2065eecd608dde77 scripts/mosaic diff --git a/agents/darkwing/work/s6-review/r2/files.txt b/agents/darkwing/work/s6-review/r2/files.txt new file mode 100644 index 00000000..67685134 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/files.txt @@ -0,0 +1,42 @@ +adapters/README.md +adapters/claude/adapter.sh +adapters/pi/adapter.sh +docs/TOOLS.md +packages/bus/README.md +packages/bus/src/broker.mjs +packages/bus/src/process.mjs +packages/bus/src/runtime.mjs +packages/bus/tests/end-launch.test.mjs +packages/cli/README.md +packages/cli/src/cli.mjs +packages/cli/src/host.mjs +packages/cli/src/launcher.mjs +packages/cli/tests/fixtures/launch-host.mjs +packages/cli/tests/host.test.mjs +packages/cli/tests/launcher.test.mjs +packages/cli/tests/verbs.test.mjs +packages/harness/README.md +packages/harness/package.json +packages/harness/src/bundle.mjs +packages/harness/src/claude-gate.mjs +packages/harness/src/gate.mjs +packages/harness/src/mcp-server.mjs +packages/harness/src/pi-extension.mjs +packages/harness/src/runner.mjs +packages/harness/src/tools.mjs +packages/harness/tests/bundle.test.mjs +packages/harness/tests/claude-gate.test.mjs +packages/harness/tests/claude-session.test.mjs +packages/harness/tests/fixtures/fake-adapter.mjs +packages/harness/tests/gate.test.mjs +packages/harness/tests/helpers.mjs +packages/harness/tests/mcp-server.test.mjs +packages/harness/tests/pi-session.test.mjs +packages/harness/tests/runner.test.mjs +packages/harness/tests/tools.test.mjs +packages/seat/README.md +packages/seat/src/proc.mjs +packages/seat/src/session.mjs +packages/seat/tests/session.test.mjs +scripts/agent-host-dev.sh +scripts/mosaic diff --git a/agents/darkwing/work/s6-review/r2/gate.sh b/agents/darkwing/work/s6-review/r2/gate.sh new file mode 100755 index 00000000..9dc2683a --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/gate.sh @@ -0,0 +1,14 @@ +#!/bin/bash +export TMPDIR=~/darkwing-scratch/tmp DOCKER_HOST=unix:///nonexistent.sock +cd ~/darkwing-scratch/r41b/wt +O=~/darkwing-scratch/r41b/out +: > $O/summary.txt +for p in harness seat cli bus business; do + node --test "packages/$p/tests/*.test.mjs" > $O/node-$p.txt 2>&1; e=$? + echo "node-$p exit=$e $(grep -E '^ℹ (pass|fail)' $O/node-$p.txt | tr '\n' ' ')" >> $O/summary.txt +done +for s in test-auth test-config test-conductor test-queue test-foundation test-extension-package test-release test-discord test-task; do + scripts/$s.sh > $O/$s.txt 2>&1; e=$? + echo "$s exit=$e $(grep -E 'passed, [0-9]+ failed' $O/$s.txt | tail -1)" >> $O/summary.txt +done +echo GATE-DONE >> $O/summary.txt diff --git a/agents/darkwing/work/s6-review/r2/interdiff.patch b/agents/darkwing/work/s6-review/r2/interdiff.patch new file mode 100644 index 00000000..01343d05 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/interdiff.patch @@ -0,0 +1,834 @@ +diff --git a/adapters/claude/adapter.sh b/adapters/claude/adapter.sh +index 2e29fe25..8a259540 100644 +--- a/adapters/claude/adapter.sh ++++ b/adapters/claude/adapter.sh +@@ -49,8 +49,10 @@ fi + # --restricted no user/project/local settings files; --settings + # (the gate hook) still applies; no code-running + # tool unless --tools names it; file tools confined +-# to the working directory. Defence in depth: the +-# S0 lines above don't depend on it. ++# to the working directory; no CLAUDE.md file or ++# auto-memory in the prompt. The S0 lines above ++# don't depend on it, but it is what keeps founder ++# and repository memory out; a test fails without it. + # --tools the built-in tool limit (S0 line 5); empty = none + # --allowedTools the same tools plus the mosaic MCP server, so + # --permission-mode dontAsk nothing waits on a prompt and anything else is denied +diff --git a/packages/bus/README.md b/packages/bus/README.md +index 29b4add7..fff96910 100644 +--- a/packages/bus/README.md ++++ b/packages/bus/README.md +@@ -63,6 +63,10 @@ underlying Broker's test-level binding API to bypass runtime process checks. + A rebind of a run that already has `session.ended` refuses with `run-ended`, + also after a restart, and a refused bind leaves the run unbound. + ++A request that carries a safe-integer `id` gets the same `id` on its reply, ++refusals included, so the host can match each reply to its request ++(`packages/cli/README.md`). A request without one gets a reply without one. ++ + S6 adds launch ops on the same IPC channel, one reply each + (`{ok:true,result}` or `{ok:false,error}`), none of them socket verbs: + +diff --git a/packages/bus/src/process.mjs b/packages/bus/src/process.mjs +index bf0ce4f1..68276a03 100644 +--- a/packages/bus/src/process.mjs ++++ b/packages/bus/src/process.mjs +@@ -10,6 +10,9 @@ function launchOp(m) { + if (m.op === 'endLaunch') return runtime.endLaunch(m.record); + return runtime.credentialStatus(m.business, m.role); + } ++// The host's request id comes back on the reply, so a late reply can't ++// answer a later request (host.mjs). A message without one gets none. ++const tag = (message, reply) => (Number.isSafeInteger(message?.id) ? { ...reply, id: message.id } : reply); + let runtime, + booted = false, + closing = false; +@@ -33,18 +36,18 @@ if (!process.send) { + try { + if (message?.op === 'bindLaunch' && runtime) { + try { +- process.send({ ok: true, launch: runtime.bindLaunch(message.record) }); ++ process.send(tag(message, { ok: true, launch: runtime.bindLaunch(message.record) })); + } catch (e) { +- process.send({ ok: false, error: e instanceof BusError ? e.code : 'bind-refused' }); ++ process.send(tag(message, { ok: false, error: e instanceof BusError ? e.code : 'bind-refused' })); + } + return; + } +- // S6 launcher ops, one reply each; the host sends them one at a time like bindLaunch. ++ // S6 launcher ops, one reply each, like bindLaunch. + if (LAUNCH_OPS.has(message?.op) && runtime) { + try { +- process.send({ ok: true, result: launchOp(message) }); ++ process.send(tag(message, { ok: true, result: launchOp(message) })); + } catch (e) { +- process.send({ ok: false, error: e instanceof BusError ? e.code : 'launch-op-refused' }); ++ process.send(tag(message, { ok: false, error: e instanceof BusError ? e.code : 'launch-op-refused' })); + } + return; + } +@@ -69,7 +72,7 @@ if (!process.send) { + } + process.send({ ok: true, path: runtime.path, launches: runtime.launches, readers: runtime.readers }); + } catch (e) { +- process.send?.({ ok: false, error: e instanceof BusError ? e.code : 'startup-refused' }, () => ++ process.send?.(tag(message, { ok: false, error: e instanceof BusError ? e.code : 'startup-refused' }), () => + close(2), + ); + } +diff --git a/packages/bus/tests/end-launch.test.mjs b/packages/bus/tests/end-launch.test.mjs +index 574129b7..68720d71 100644 +--- a/packages/bus/tests/end-launch.test.mjs ++++ b/packages/bus/tests/end-launch.test.mjs +@@ -175,6 +175,11 @@ test('broker process: launch ops authorize role.launch, record refusals and end + assert.deepEqual((await ask(child, { op: 'credentialStatus', business: 'demo', role: 'pm' })).result, []); + const end = await ask(child, { op: 'endLaunch', record: { business: 'demo', run: 'pm-1', reason: 'stopped', exitCode: 0 } }); + assert.deepEqual(end, { ok: true, result: { released: true } }); ++ // The host's request id comes back on every launch-op and bind reply, refusals too. ++ assert.equal((await ask(child, { op: 'identity', cap: cto, id: 7 })).id, 7); ++ assert.deepEqual(await ask(child, { op: 'identity', cap: 'f'.repeat(64), id: 8 }), { ok: false, error: 'unauthenticated', id: 8 }); ++ assert.equal((await ask(child, { op: 'bindLaunch', record: record('coder', 'coder-1'), id: 9 })).id, 9); ++ assert.deepEqual(await ask(child, { op: 'bindLaunch', record: record('coder', 'coder-1'), id: 10 }), { ok: false, error: 'duplicate-run', id: 10 }); + await assert.rejects(new Client({ path: ready.path, cap: pm }).call('agents'), /unauthenticated/); + const trail = await reader.call('trail', { subject: 'pm-1' }); + assert.deepEqual( +diff --git a/packages/cli/README.md b/packages/cli/README.md +index be9e6531..32977afa 100644 +--- a/packages/cli/README.md ++++ b/packages/cli/README.md +@@ -62,6 +62,9 @@ mosaic launches list [--json] + - `launches off` and `on` are the broker's `launch.revoke` and + `launch.restore`. While off, every `role.launch` refuses with + `launch-revoked`; running sessions keep running. Stop them with `stop`. ++ `bus start --pm` doesn't ask the broker (`launchPm` skips ++ `authorizeLaunch`): the human launches the PM, so `launches off` doesn't ++ stop it. The other checks in "Sessions" below still apply. + - `stop` and `launches list` read `/bus-host/sessions.json`, not + the bus (see "Sessions" below). `stop` refuses inside an agent run; + `launches list` does not, because the file is readable to the same user +@@ -106,8 +109,12 @@ uses: `bindLaunch(record)` binds a launched run's process identity + one of the broker process's launch ops (`identity`, `authorizeLaunch`, + `refuse`, `endLaunch`, `credentialStatus`); `beforeClose(fn)` runs `fn` + before the broker closes, so the launcher stops its sessions first. +-Requests to the broker process go one at a time, because its replies carry +-no request id. ++Requests to the broker process go one at a time. Each carries an id, and ++the broker echoes it on the reply. If a reply doesn't arrive within 10 s, or ++arrives with an id no request is waiting for, the channel is broken: the ++waiting request refuses with `broker-channel-broken`, so does every later ++one, and the host stops with exit 1 for the unit to restart. A late reply ++never answers a later request, and a launch waiting on one refuses. + + SIGTERM or SIGINT stops the notifier after its poll in flight, then closes + the broker and removes `host.json`. If either child dies on its own, the +@@ -166,7 +173,9 @@ directory; `launches/.jsonl` (0600, append-only) logs every launch, + refusal and end; `workspaces///` (0700) is kept across + launches; `bus-host/sessions.json` (0600) lists the running sessions. + +-When the host closes, the launcher stops taking requests, sends SIGTERM to ++When the host closes, the launcher stops taking requests and drops every ++open `launch.sock` connection, so a client that never ends its side can't ++hold the close. It sends SIGTERM to + every runner (again each second, see "Limits"), waits up to 30 s, then + SIGKILLs what is left and ends those launches as `killed`. + +diff --git a/packages/cli/src/host.mjs b/packages/cli/src/host.mjs +index 35c44f58..0073091b 100644 +--- a/packages/cli/src/host.mjs ++++ b/packages/cli/src/host.mjs +@@ -25,6 +25,7 @@ const NOTIFIER = fileURLToPath(new URL("./notifier-process.mjs", import.meta.url + export const BOOT_TIMEOUT_MS = 30000; + const START_TIMEOUT_MS = 15000; + const CLOSE_TIMEOUT_MS = 20000; ++const REQUEST_TIMEOUT_MS = 10000; + + export const hostDir = (dataRoot) => join(dataRoot, "bus-host"); + export const hostFile = (dataRoot) => join(hostDir(dataRoot), "host.json"); +@@ -94,8 +95,9 @@ export function watchChildren(children, onDeath) { + + // boot: the {op:'boot'} config from bootConfig(). notifier: null, or + // {binding, base?, pollMs?}; base and pollMs exist for the tests, and the +-// command line never sets them. Resolves once both children are up. +-export async function startHost({ boot, business, notifier = null, bootTimeoutMs = BOOT_TIMEOUT_MS, log = (l) => process.stderr.write(`mosaic-bus: ${l}\n`) }) { ++// command line never sets them, nor requestTimeoutMs. Resolves once both ++// children are up. ++export async function startHost({ boot, business, notifier = null, bootTimeoutMs = BOOT_TIMEOUT_MS, requestTimeoutMs = REQUEST_TIMEOUT_MS, log = (l) => process.stderr.write(`mosaic-bus: ${l}\n`) }) { + const dataRoot = boot.dataRoot; + const prior = readHostState(dataRoot); + if (prior?.live) throw new CliError(`a bus host already runs for ${prior.business} (pid ${prior.pid})`, 3); +@@ -152,14 +154,52 @@ export async function startHost({ boot, business, notifier = null, bootTimeoutMs + const done = new Promise((r) => (finish = r)); + const hooks = []; + +- // Replies from process.mjs carry no request id, so requests go one at a time. ++ // Requests go one at a time, each with an id that process.mjs echoes. A ++ // reply that misses the wait, or carries an id no request is waiting for, ++ // breaks the channel: the waiting request and every later one refuse, and ++ // the host stops with exit 1 so the unit restarts it. A late reply can ++ // never answer a later request. + let queue = Promise.resolve(); ++ let seq = 0; ++ let pending = null; ++ let broken = null; ++ const fail = (code, text) => Object.assign(new CliError(text, 1), { code }); ++ function breakChannel(why) { ++ if (broken) return; ++ broken = why; ++ if (pending) { ++ clearTimeout(pending.timer); ++ pending.reject(fail("broker-channel-broken", `broker channel broken: ${why}`)); ++ pending = null; ++ } ++ if (stopping) return; ++ log(`broker channel broken (${why}); stopping the host`); ++ close(1); ++ } ++ broker.on("message", (m) => { ++ if (pending && m?.id === pending.id) { ++ clearTimeout(pending.timer); ++ const { resolve } = pending; ++ pending = null; ++ resolve(m); ++ } else breakChannel(pending ? "reply for another request" : "reply with no request waiting"); ++ }); ++ broker.once("exit", () => { ++ if (!pending) return; ++ clearTimeout(pending.timer); ++ pending.reject(fail("broker-exited", "broker exited before it replied")); ++ pending = null; ++ }); + function request(message, { what, pick }) { + const run = queue.then(async () => { +- if (closing || !broker.connected) throw Object.assign(new CliError("bus host is closing", 1), { code: "host-closing" }); +- const r = firstReply(broker, 10000, "broker"); +- broker.send(message); +- const m = await r; ++ if (broken) throw fail("broker-channel-broken", `broker channel broken: ${broken}`); ++ if (closing || !broker.connected) throw fail("host-closing", "bus host is closing"); ++ const id = ++seq; ++ const m = await new Promise((resolve, reject) => { ++ const timer = setTimeout(() => breakChannel(`no reply within ${Math.round(requestTimeoutMs / 1000)} s`), requestTimeoutMs); ++ pending = { id, resolve, reject, timer }; ++ broker.send({ ...message, id }); ++ }); + if (m?.ok !== true) { + const code = typeof m?.error === "string" ? m.error : `${what}-refused`; + throw Object.assign(new CliError(`${what} refused: ${code}`, 3), { code }); +diff --git a/packages/cli/src/launcher.mjs b/packages/cli/src/launcher.mjs +index ec01a2c3..b07ae333 100644 +--- a/packages/cli/src/launcher.mjs ++++ b/packages/cli/src/launcher.mjs +@@ -83,8 +83,8 @@ export function claudeVersion(env = process.env) { + } + + // host: startHost()'s handle. tracker: true when the broker has task verbs for +-// the business. adapters, namespace, sessionDefaults and versions exist for +-// the tests; the command line never sets them. ++// the business. adapters, namespace, sessionDefaults, versions and ++// stopTimeoutMs exist for the tests; the command line never sets them. + export function createLauncher({ + host, + system, +@@ -94,6 +94,7 @@ export function createLauncher({ + namespace = true, + sessionDefaults = {}, + versions = null, ++ stopTimeoutMs = STOP_TIMEOUT_MS, + log = (l) => process.stderr.write(`mosaic-bus: ${l}\n`), + }) { + const dataRoot = system.dataRoot; +@@ -114,6 +115,7 @@ export function createLauncher({ + let chain = Promise.resolve(); + let closed = false; + let server = null; ++ const sockets = new Set(); + + const record = (entry) => { + try { +@@ -335,6 +337,8 @@ export function createLauncher({ + rmSync(path); + } + server = createServer((socket) => { ++ sockets.add(socket); ++ socket.once("close", () => sockets.delete(socket)); + let buf = ""; + let answered = false; + const reply = (obj) => { +@@ -377,7 +381,11 @@ export function createLauncher({ + async function close() { + closed = true; + if (server) { +- await new Promise((r) => server.close(r)); ++ // server.close waits for every connection, and a client that never ++ // ends its side (or never sends) would hold it; so they go first. ++ const stopped = new Promise((r) => server.close(r)); ++ for (const socket of sockets) socket.destroy(); ++ await stopped; + rmSync(launchSocketPath(dataRoot), { force: true }); + } + await chain; +@@ -399,7 +407,7 @@ export function createLauncher({ + if (startTimeOf(s.pid) === s.startTime) process.kill(s.pid, "SIGKILL"); + } catch {} + } +- }, STOP_TIMEOUT_MS); ++ }, stopTimeoutMs); + await Promise.all(pending); + // The exit handlers run on the same tick as the close events; let them finish. + while (children.size) await new Promise((r) => setTimeout(r, 20)); +diff --git a/packages/cli/tests/host.test.mjs b/packages/cli/tests/host.test.mjs +index f8f11ec9..f8914479 100644 +--- a/packages/cli/tests/host.test.mjs ++++ b/packages/cli/tests/host.test.mjs +@@ -203,6 +203,65 @@ test("a second host for the same data root refuses with exit 3 while the first r + assert.equal(await host.close(0), 0); + }); + ++test("a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1", { timeout: 30000 }, async (t) => { ++ const root = tmp(t); ++ const f = fixture(root); ++ makeDeployment(root); ++ const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env }); ++ const logs = []; ++ const host = await startHost({ boot, business: "acme", requestTimeoutMs: 1000, log: (l) => logs.push(l) }); ++ // Hooks run in order: the broker resumes before the close, which a ++ // stopped broker would hold. ++ t.after(() => { ++ try { ++ process.kill(host.pids.broker, "SIGCONT"); ++ } catch {} ++ }); ++ t.after(() => host.close(0)); ++ const record = (role, run) => ({ business: "acme", role, run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) }); ++ const coder = await host.bindLaunch(record("coder", "coder-run")); ++ assert.equal((await host.op({ op: "identity", cap: coder.cap })).role, "coder"); ++ ++ // Stall the broker with a request waiting and a bind queued behind it. ++ process.kill(host.pids.broker, "SIGSTOP"); ++ const code = (p) => p.then((v) => ({ answered: v }), (e) => e.code); ++ const first = code(host.op({ op: "identity", cap: "bogus" })); ++ const second = code(host.bindLaunch(record("reviewer", "reviewer-run"))); ++ assert.equal(await first, "broker-channel-broken"); ++ assert.equal(await second, "broker-channel-broken", "a queued request never reaches the broker"); ++ process.kill(host.pids.broker, "SIGCONT"); ++ ++ // The broker answers the stalled request late; nothing takes that reply. ++ assert.equal(await host.done, 1); ++ assert.ok(logs.some((l) => /^broker channel broken \(no reply within 1 s\); stopping the host$/.test(l)), logs.join("\n")); ++ assert.equal(await code(host.op({ op: "identity", cap: coder.cap })), "broker-channel-broken"); ++}); ++ ++test("a broker reply with another request's id, or none, breaks the channel and the host exits 1", { timeout: 30000 }, async (t) => { ++ // The broker echoes whatever id it's sent, so changing the id on the way ++ // out gives the host the reply a confused broker would send. ++ let what, tamper; ++ spySends(t, (m) => m?.cap === "tamper" && tamper(m)); ++ for ([what, tamper] of [ ++ ["another id", (m) => (m.id += 1000)], ++ ["no id", (m) => delete m.id], ++ ]) { ++ const root = tmp(t); ++ const f = fixture(root); ++ makeDeployment(root); ++ const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env }); ++ const logs = []; ++ const host = await startHost({ boot, business: "acme", log: (l) => logs.push(l) }); ++ t.after(() => host.close(0)); ++ const record = (role, run) => ({ business: "acme", role, run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) }); ++ const coder = await host.bindLaunch(record("coder", "coder-run")); ++ await assert.rejects(host.op({ op: "identity", cap: "tamper" }), (e) => e.code === "broker-channel-broken", what); ++ await assert.rejects(host.op({ op: "identity", cap: coder.cap }), (e) => e.code === "broker-channel-broken", what); ++ assert.equal(await host.done, 1, what); ++ assert.ok(logs.includes("broker channel broken (reply for another request); stopping the host"), `${what}: ${logs.join("\n")}`); ++ } ++}); ++ + test("a notifier that refuses stops the broker and the host refuses with exit 3", async (t) => { + const root = tmp(t); + const f = fixture(root); +diff --git a/packages/cli/tests/launcher.test.mjs b/packages/cli/tests/launcher.test.mjs +index b0c0ba09..75ee91b8 100644 +--- a/packages/cli/tests/launcher.test.mjs ++++ b/packages/cli/tests/launcher.test.mjs +@@ -53,7 +53,7 @@ function prepare(t, more = (doc) => doc) { + return { root, f, adapter }; + } + +-async function setup(t, more = (doc) => doc, { root, f, adapter } = prepare(t, more)) { ++async function setup(t, more = (doc) => doc, { root, f, adapter } = prepare(t, more), options = {}) { + const system = loadSystem({ env: f.env }); + const boot = bootConfig({ system, businessId: "acme", env: f.env }); + const logs = []; +@@ -68,6 +68,7 @@ async function setup(t, more = (doc) => doc, { root, f, adapter } = prepare(t, m + sessionDefaults: { pollInterval: 100 }, + versions: { pi: "0.85.1", claude: null }, + log: (l) => logs.push(l), ++ ...options, + }); + await launcher.listen(); + } catch (e) { +@@ -267,6 +268,16 @@ for (const exited of [false, true]) { + // The broker child exits on the lost IPC channel; the session runs on. + await until(() => !existsSync(join(f.dataRoot, "bus", "writer.lock"))); + assert.equal(startTimeOf(left.runnerPid), left.runnerStartTime); ++ if (!exited) { ++ // Stopped, the leftover neither polls the dead broker nor answers ++ // SIGTERM: only the next host's SIGKILL ends it. ++ for (const pid of [left.pid, left.runnerPid]) process.kill(pid, "SIGSTOP"); ++ t.after(() => { ++ for (const [pid, start] of [[left.pid, left.startTime], [left.runnerPid, left.runnerStartTime]]) { ++ if (startTimeOf(pid) === start) process.kill(pid, "SIGKILL"); ++ } ++ }); ++ } + if (exited) { + await until(() => startTimeOf(left.pid) === null); + assert.match(readFileSync(runnerLog, "utf8"), /broker unreachable after 30 tries[\s\S]*exiting 23/); +@@ -309,3 +320,75 @@ test("a malformed sessions.json refuses the host start with exit 3 and stays as + assert.equal(readFileSync(file, "utf8"), "{"); + assert.equal(existsSync(join(given.f.dataRoot, "bus", "writer.lock")), false, "the host closed"); + }); ++ ++const within = (p, ms, what) => Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error(`${what} took over ${ms} ms`)), ms).unref())]); ++ ++test("an over-long launch request is refused at once, not at the 10 s idle timeout", async (t) => { ++ const { f } = await setup(t); ++ const s = connect(launchSocketPath(f.dataRoot)); ++ t.after(() => s.destroy()); ++ let buf = ""; ++ s.on("data", (b) => (buf += b)); ++ s.write("x".repeat(5000)); ++ await within(once(s, "end"), 3000, "the refusal"); ++ assert.deepEqual(JSON.parse(buf), { ok: false, error: "invalid-request" }); ++}); ++ ++test("a launch client that never closes its side doesn't hold the host's close", { timeout: 30000 }, async (t) => { ++ const { f, close } = await setup(t); ++ const path = launchSocketPath(f.dataRoot); ++ // One got its reply and never ends; one never sends anything. ++ const answered = connect({ path, allowHalfOpen: true }); ++ const silent = connect({ path, allowHalfOpen: true }); ++ const connected = once(silent, "connect"); ++ const gone = Promise.all([once(answered, "close"), once(silent, "close")]); ++ const drop = () => { ++ answered.destroy(); ++ silent.destroy(); ++ }; ++ t.after(drop); ++ let buf = ""; ++ answered.on("data", (b) => (buf += b)); ++ answered.write("not json\n"); ++ await once(answered, "end"); ++ assert.deepEqual(JSON.parse(buf), { ok: false, error: "invalid-request" }); ++ await within(connected, 2000, "connect"); ++ // On a failure the clients go, so the host can still close and the test ++ // fails instead of hanging. ++ const code = await within(close(), 5000, "close").catch((e) => { ++ drop(); ++ throw e; ++ }); ++ assert.equal(code, 0); ++ assert.equal(existsSync(path), false); ++ answered.end(); ++ silent.end(); ++ await within(gone, 2000, "the clients' close"); ++}); ++ ++test("a runner that ignores SIGTERM is killed when the host closes", { skip, timeout: 60000 }, async (t) => { ++ const { f, launcher, close } = await setup(t, undefined, undefined, { stopTimeoutMs: 1000 }); ++ const pm = await launcher.launchPm(); ++ const runnerLog = join(f.dataRoot, "launches", "acme", pm.run, "runner.log"); ++ await until(() => existsSync(runnerLog) && readFileSync(runnerLog, "utf8").includes("claimed by run")); ++ const [s] = readSessions(f.dataRoot); ++ // Stopped from outside its namespace, it can't act on SIGTERM. ++ process.kill(s.runnerPid, "SIGSTOP"); ++ const kill = () => { ++ if (startTimeOf(s.runnerPid) === s.runnerStartTime) process.kill(s.runnerPid, "SIGKILL"); ++ }; ++ t.after(kill); ++ const started = Date.now(); ++ const code = await within(close(), 15000, "close").catch((e) => { ++ kill(); ++ throw e; ++ }); ++ assert.equal(code, 0); ++ assert.ok(Date.now() - started >= 1000, "it waited for the stop timeout"); ++ assert.equal(startTimeOf(s.pid), null); ++ assert.equal(startTimeOf(s.runnerPid), null); ++ assert.deepEqual(readSessions(f.dataRoot), []); ++ const ends = log(f).filter((e) => e.event === "end" && e.run === pm.run); ++ assert.equal(ends.length, 1); ++ assert.equal(ends[0].signal, "SIGKILL"); ++}); +diff --git a/packages/harness/README.md b/packages/harness/README.md +index bc2dcc85..79ff829a 100644 +--- a/packages/harness/README.md ++++ b/packages/harness/README.md +@@ -26,10 +26,13 @@ bundle's `manifest.json` names the lines it relies on (`reliesOn`): + | 4. gate hang | the runner's wall clock plus the `agent_end` marker | `timeout -k 2 10 \|\| exit 2`, hook timeout 20 | + | 5. bash | limited only by the tool limit | limited only by the tool limit | + +-Claude Code also runs with `--restricted` (no user, project or local +-settings; file tools confined to the working directory). That is defence +-in depth: none of the S0 lines depend on it, and no test treats it as the +-layer that holds. ++Claude Code also runs with `--restricted`: no user, project or local ++settings, file tools confined to the working directory, and no `CLAUDE.md` ++file (user, parent or workspace) or auto-memory in the prompt. None of the ++S0 lines depend on it, and the gate doesn't rely on it for paths. It is the ++layer that keeps founder and repository memory out of the session's ++prompt, though, so `claude-session.test.mjs` fails if the adapter stops ++passing it, and shows the memory reaching the model without it. + + ## The bundle + +@@ -74,7 +77,10 @@ refusal comes back to the model as `refused: `. + policy's built-in tools and typed tools pass, anything else is blocked, and + every path argument of a file tool (and a `find`/`Glob` pattern) must + resolve inside the workspace after symlinks and Pi's own path +-normalisation. Pi calls it from the extension, Claude Code from ++normalisation. A path that reaches a dangling symlink, at any depth, is ++refused even when the link points inside: a write through it would create ++the target wherever it names, and Pi's `write` makes the missing ++directories first. Pi calls it from the extension, Claude Code from + `claude-gate.mjs`. + + ## The runner +@@ -148,7 +154,14 @@ These are limits, not bugs, and nothing in this package claims otherwise. + - **Resolution** runs without the project layer, there is no skills source + (bundles list none), and the resolved `thinking` level is recorded in the + manifest but not applied to either harness. +-- **`--restricted`** (Claude Code) is an extra layer, not one the S0 lines +- prove. ++- **`--restricted`** (Claude Code) is not one of the S0 lines. It is what ++ keeps `CLAUDE.md` files and auto-memory out of the prompt (above). ++- **The gate checks a path when the call is made.** A link created or ++ changed between the check and the tool's own open (by `bash`, or by ++ another process of the same user) isn't seen. That is the same reach as ++ `bash` itself. ++- **The system prompt is in argv** for both adapters, so the same UID can ++ read it in `/proc//cmdline`; the PID namespace hides it from other ++ sessions. It holds no secret. + - **Pi adapter paths** with spaces break its unquoted `-e` and skill + splitting; the launcher's paths don't contain spaces. +diff --git a/packages/harness/src/gate.mjs b/packages/harness/src/gate.mjs +index 322a84ce..47cc029e 100644 +--- a/packages/harness/src/gate.mjs ++++ b/packages/harness/src/gate.mjs +@@ -8,7 +8,7 @@ + // and anything bash can reach, the session can reach. See the README's + // limits. + +-import { existsSync, realpathSync } from "node:fs"; ++import { lstatSync, realpathSync } from "node:fs"; + import { homedir } from "node:os"; + import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path"; + import { fileURLToPath } from "node:url"; +@@ -41,17 +41,25 @@ function normalise(p) { + return s; + } + +-// Realpath of the nearest existing ancestor, with the missing tail kept. ++// Realpath of the nearest ancestor that exists as a name, with the missing ++// tail kept. lstat, not exists: a dangling symlink exists as a name, and a ++// write through it would create its target wherever that is. So a path that ++// reaches a dangling symlink, at any depth, can't be checked and is refused. + function real(p) { + let head = p; + const tail = []; +- while (!existsSync(head)) { ++ while (!lstatSync(head, { throwIfNoEntry: false })) { + const up = dirname(head); + if (up === head) break; + tail.unshift(basename(head)); + head = up; + } +- return join(realpathSync(head), ...tail); ++ try { ++ return join(realpathSync(head), ...tail); ++ } catch (error) { ++ if (error.code === "ENOENT") throw Object.assign(new Error("dangling symlink"), { code: "dangling-symlink" }); ++ throw error; ++ } + } + + export function insideWorkspace(workspace, p) { +@@ -87,6 +95,7 @@ export function decide(policy, tool, input) { + try { + if (!insideWorkspace(policy.workspace, value)) return no(`${tool} path is outside the workspace: ${value}`); + } catch (error) { ++ if (error.code === "dangling-symlink") return no(`${tool} path goes through a dangling symlink: ${value}`); + return no(`${tool} path can't be checked: ${error.code ?? error.message}`); + } + return { allow: true }; +diff --git a/packages/harness/src/runner.mjs b/packages/harness/src/runner.mjs +index e2e02d4d..ccf6ca43 100644 +--- a/packages/harness/src/runner.mjs ++++ b/packages/harness/src/runner.mjs +@@ -256,16 +256,16 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l + process.on("SIGTERM", stop); + process.on("SIGINT", stop); + +- let session, cap; ++ let session, cap, policy; + try { + session = { ...DEFAULTS, ...JSON.parse(readFileSync(join(runDir, "session.json"), "utf8")), runDir }; + cap = JSON.parse(await readLine(stdin)).cap; + if (typeof cap !== "string" || !/^[0-9a-f]{64}$/.test(cap)) throw new Error("no capability on stdin"); ++ policy = JSON.parse(readFileSync(session.bundle.policy, "utf8")); + } catch (e) { + log(`runner: ${e.message}`); + return EXIT.usage; + } +- const policy = JSON.parse(readFileSync(session.bundle.policy, "utf8")); + if (stopping) { + log("runner: stopped before claim"); + return EXIT.stopped; +diff --git a/packages/harness/tests/claude-session.test.mjs b/packages/harness/tests/claude-session.test.mjs +index 823a31ee..e45c5d69 100644 +--- a/packages/harness/tests/claude-session.test.mjs ++++ b/packages/harness/tests/claude-session.test.mjs +@@ -1,13 +1,14 @@ + // The host's claude CLI through adapters/claude with the bundle's hook and + // MCP server, against the scripted Messages API. Scratch CLAUDE_CONFIG_DIR + // and HOME, a dummy key, nonessential traffic off: nothing reaches a real +-// model or the user's Claude configuration. Skipped when claude isn't on PATH. ++// model or the user's Claude configuration. Skipped when claude isn't on PATH, ++// except the argv check, which uses a stand-in claude. + + import { test } from "node:test"; + import assert from "node:assert/strict"; + import { spawn, spawnSync } from "node:child_process"; +-import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +-import { dirname, join } from "node:path"; ++import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs"; ++import { basename, dirname, join } from "node:path"; + import { buildBundle } from "../src/bundle.mjs"; + import { adapterEnv } from "../src/runner.mjs"; + import { fakeToolSocket, mockAnthropic, REPO, resolvedFor, scratch } from "./helpers.mjs"; +@@ -57,9 +58,9 @@ async function session(t, script, tools = ["read", "bash"]) { + return { dir, workspace, api, sock, s, env, manifest }; + } + +-function turn(env, request, cwd) { ++function turn(env, request, cwd, adapter = ADAPTER) { + return new Promise((resolve) => { +- const child = spawn("/bin/sh", [ADAPTER], { cwd, env: { ...env, MOSAIC_REQUEST: request }, stdio: ["ignore", "pipe", "pipe"], detached: true }); ++ const child = spawn("/bin/sh", [adapter], { cwd, env: { ...env, MOSAIC_REQUEST: request }, stdio: ["ignore", "pipe", "pipe"], detached: true }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (b) => (stdout += b)); +@@ -136,6 +137,67 @@ test("claude: a second turn resumes the first turn's session", { skip }, async ( + assert.equal(readFileSync(join(s.sessionDir, "claude-session-id"), "utf8"), id); + }); + ++// --restricted is what keeps CLAUDE.md files and auto-memory out of the ++// session's prompt (README "Limits"). This one runs without claude: a ++// stand-in records the adapter's argv. ++test("claude adapter: --restricted is always passed", (t) => { ++ const dir = scratch(t); ++ mkdirSync(join(dir, "bin")); ++ writeFileSync(join(dir, "bin", "claude"), '#!/bin/sh\nprintf "%s\\n" "$@" > "$ARGV_OUT"\necho ok\n'); ++ chmodSync(join(dir, "bin", "claude"), 0o755); ++ for (const f of ["prompt.md", "settings.json", "mcp.json"]) writeFileSync(join(dir, f), "{}"); ++ const r = spawnSync("/bin/sh", [ADAPTER], { ++ encoding: "utf8", ++ stdio: ["ignore", "pipe", "pipe"], ++ env: { ++ PATH: `${join(dir, "bin")}:/usr/bin:/bin`, ++ ARGV_OUT: join(dir, "argv"), ++ MOSAIC_SYSTEM_PROMPT_FILE: join(dir, "prompt.md"), ++ MOSAIC_REQUEST: "x", ++ MOSAIC_WORKSPACE: join(dir, "ws"), ++ MOSAIC_SESSION_DIR: join(dir, "session"), ++ MOSAIC_MODEL: "claude-sonnet-5-5", ++ MOSAIC_CLAUDE_SETTINGS: join(dir, "settings.json"), ++ MOSAIC_CLAUDE_MCP_CONFIG: join(dir, "mcp.json"), ++ }, ++ }); ++ assert.equal(r.status, 0, r.stderr); ++ const argv = readFileSync(join(dir, "argv"), "utf8").split("\n"); ++ assert.ok(argv.includes("--restricted"), argv.join(" ")); ++ assert.ok(!argv.includes("--bare"), argv.join(" ")); ++}); ++ ++test("claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do", { skip }, async (t) => { ++ const { dir, workspace, env } = await session(t, []); ++ const slug = realpathSync(workspace).replace(/[/.]/g, "-"); ++ const plant = { ++ "MARKER-USER": [join(env.HOME, ".claude", "CLAUDE.md"), join(env.CLAUDE_CONFIG_DIR, "CLAUDE.md")], ++ "MARKER-PARENT": [join(dir, "CLAUDE.md")], ++ "MARKER-WS": [join(workspace, "CLAUDE.md")], ++ "MARKER-MEMORY": [join(env.HOME, ".claude", "projects", slug, "memory", "MEMORY.md"), join(env.CLAUDE_CONFIG_DIR, "projects", slug, "memory", "MEMORY.md")], ++ }; ++ for (const [marker, files] of Object.entries(plant)) { ++ for (const f of files) { ++ mkdirSync(dirname(f), { recursive: true }); ++ writeFileSync(f, `${marker}\n`); ++ } ++ } ++ const seen = async (adapter) => { ++ const api = await mockAnthropic(t, {}); ++ const r = await turn({ ...env, ANTHROPIC_BASE_URL: api.url, MOSAIC_SESSION_DIR: join(dir, `session-${basename(adapter)}`) }, "x", workspace, adapter); ++ assert.equal(r.code, 0, r.stderr); ++ const all = api.requests.map((x) => x.raw).join("\n"); ++ assert.match(all, /## This session/); ++ return Object.keys(plant).filter((m) => all.includes(m)); ++ }; ++ assert.deepEqual(await seen(ADAPTER), []); ++ // The control: the same adapter without --restricted lets all four in. ++ const loose = join(dir, "adapter-loose.sh"); ++ writeFileSync(loose, readFileSync(ADAPTER, "utf8").replace(" --restricted \\\n", "")); ++ assert.notEqual(readFileSync(loose, "utf8"), readFileSync(ADAPTER, "utf8")); ++ assert.deepEqual(await seen(loose), Object.keys(plant)); ++}); ++ + test("claude: a missing hook or MCP file refuses before claude starts", { skip }, async (t) => { + const { dir, api, workspace, env } = await session(t, []); + for (const k of ["MOSAIC_CLAUDE_SETTINGS", "MOSAIC_CLAUDE_MCP_CONFIG", "MOSAIC_SYSTEM_PROMPT_FILE"]) { +diff --git a/packages/harness/tests/gate.test.mjs b/packages/harness/tests/gate.test.mjs +index 66e00bb3..c32394c2 100644 +--- a/packages/harness/tests/gate.test.mjs ++++ b/packages/harness/tests/gate.test.mjs +@@ -79,6 +79,29 @@ test("a symlink inside the workspace that points out is outside", (t) => { + blocked(decide(policy, "write", { path: "link/new.txt" }), /outside the workspace/); + }); + ++test("a dangling symlink is refused at any depth, in both harnesses", (t) => { ++ for (const [harness, tool, field] of [["pi", "write", "path"], ["claude-code", "Write", "file_path"]]) { ++ const { dir, workspace, policy } = setup(t, harness, ["read", "write"]); ++ mkdirSync(join(dir, "outside")); ++ // notes.md names a file that doesn't exist yet, outside; dangling names a ++ // directory that doesn't; inner points inside but at nothing. ++ symlinkSync(join(dir, "outside", "planted.txt"), join(workspace, "notes.md")); ++ symlinkSync(join(dir, "nowhere"), join(workspace, "dangling")); ++ symlinkSync(join(workspace, "later.txt"), join(workspace, "inner")); ++ for (const rel of ["notes.md", "dangling/x", "dangling/deep/x", "inner"]) { ++ blocked(decide(policy, tool, { [field]: rel }), /goes through a dangling symlink/); ++ blocked(decide(policy, tool, { [field]: join(workspace, rel) }), /goes through a dangling symlink/); ++ } ++ // Once the target exists, the usual realpath rule decides. ++ writeFileSync(join(dir, "outside", "planted.txt"), "p"); ++ blocked(decide(policy, tool, { [field]: "notes.md" }), /outside the workspace/); ++ writeFileSync(join(workspace, "later.txt"), "l"); ++ allowed(decide(policy, tool, { [field]: "inner" })); ++ // A file used as a directory can't be checked. ++ blocked(decide(policy, tool, { [field]: "a.txt/x" }), /can't be checked: ENOTDIR/); ++ } ++}); ++ + test("claude path fields per tool", (t) => { + const { workspace, policy } = setup(t, "claude-code", ["read", "write", "edit", "grep", "find"]); + allowed(decide(policy, "Read", { file_path: join(workspace, "a.txt") })); +diff --git a/packages/harness/tests/helpers.mjs b/packages/harness/tests/helpers.mjs +index 19f13a64..96c1923f 100644 +--- a/packages/harness/tests/helpers.mjs ++++ b/packages/harness/tests/helpers.mjs +@@ -79,7 +79,7 @@ export async function mockAnthropic(t, { script = [], done = (r) => `DONE ${JSON + const results = toolResults(body.messages); + const step = tools.length ? script[results.length] : null; + const r = step ? { kind: "tool", id: `toolu_${results.length + 1}`, name: step.name, input: step.input } : { kind: "text", text: tools.length ? done(results) : "mock" }; +- requests.push({ tools, results, system: body.system, answer: r }); ++ requests.push({ tools, results, system: body.system, answer: r, raw }); + const content = r.kind === "tool" ? { type: "tool_use", id: r.id, name: r.name, input: {} } : { type: "text", text: "" }; + const delta = r.kind === "tool" ? { type: "input_json_delta", partial_json: JSON.stringify(r.input) } : { type: "text_delta", text: r.text }; + const stop = r.kind === "tool" ? "tool_use" : "end_turn"; +diff --git a/packages/harness/tests/pi-session.test.mjs b/packages/harness/tests/pi-session.test.mjs +index 8fc8b4e4..0486857f 100644 +--- a/packages/harness/tests/pi-session.test.mjs ++++ b/packages/harness/tests/pi-session.test.mjs +@@ -4,7 +4,7 @@ + import { test } from "node:test"; + import assert from "node:assert/strict"; + import { spawn } from "node:child_process"; +-import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs"; ++import { existsSync, mkdirSync, readdirSync, symlinkSync, writeFileSync } from "node:fs"; + import { join } from "node:path"; + import { buildBundle } from "../src/bundle.mjs"; + import { adapterEnv } from "../src/runner.mjs"; +@@ -96,6 +96,30 @@ test("pi: typed tools reach the socket, the gate blocks, agent_end writes the ma + assert.ok(readdirSync(s.sessionDir).length > 0); + }); + ++test("pi: a write through a dangling symlink is blocked, and nothing appears outside", async (t) => { ++ const { dir, workspace, s, env } = await session(t, [ ++ { name: "write", input: { path: "notes.md", content: "planted\n" } }, ++ { name: "write", input: { path: "gone/x.md", content: "planted\n" } }, ++ { name: "write", input: { path: "fine.md", content: "inside\n" } }, ++ ]); ++ mkdirSync(join(dir, "outside")); ++ symlinkSync(join(dir, "outside", "planted.txt"), join(workspace, "notes.md")); ++ symlinkSync(join(dir, "outside", "made"), join(workspace, "gone")); ++ const r = await turn(env, "Message 1 from jason, class REQUEST:\n\nwrite your notes", workspace); ++ assert.equal(r.code, 0, r.stderr); ++ const results = JSON.parse(r.stdout.trim().slice("ANSWER ".length)); ++ assert.equal(results.length, 3); ++ assert.equal(results[0][0], true); ++ assert.match(results[0][1], /dangling symlink: notes\.md/); ++ assert.equal(results[1][0], true); ++ assert.match(results[1][1], /dangling symlink: gone\/x\.md/); ++ assert.equal(results[2][0], false); ++ assert.ok(existsSync(join(workspace, "fine.md")), "the write inside landed"); ++ assert.deepEqual(readdirSync(join(dir, "outside")), []); ++ assert.ok(!existsSync(join(dir, "outside", "made"))); ++ assert.ok(existsSync(s.turnMarker)); ++}); ++ + test("pi: a missing extension refuses before any model call", async (t) => { + const { dir, api, s, env } = await session(t, []); + const r = await turn({ ...env, MOSAIC_EXTENSIONS: join(dir, "missing.mjs") }, "x", s.workspace); +diff --git a/packages/harness/tests/runner.test.mjs b/packages/harness/tests/runner.test.mjs +index 77027f36..25b4d8a9 100644 +--- a/packages/harness/tests/runner.test.mjs ++++ b/packages/harness/tests/runner.test.mjs +@@ -110,15 +110,22 @@ async function setup(t, { session = {}, policy = {}, tools } = {}) { + return { dir, runDir, store, broker, server, cap, pm, call, launches, pids: join(dir, "pids") }; + } + +-function startRunner(runDir, cap, env = {}) { ++// A runner that hasn't exited after a minute is killed, so a test that ++// expected an exit fails on the code instead of hanging. ++function startRunner(runDir, cap, env = {}, input = cap === null ? "" : JSON.stringify({ cap }) + "\n") { + const child = spawn(process.execPath, [RUNNER, runDir], { + stdio: ["pipe", "ignore", "pipe"], + env: { PATH: process.env.PATH, ...env }, + }); + let stderr = ""; + child.stderr.on("data", (b) => (stderr += b)); +- child.stdin.end(cap === null ? "" : JSON.stringify({ cap }) + "\n"); +- const exited = once(child, "exit").then(([code, signal]) => ({ code, signal, stderr })); ++ child.stdin.on("error", () => {}); ++ child.stdin.end(input); ++ const clock = setTimeout(() => child.kill("SIGKILL"), 60_000); ++ const exited = once(child, "exit").then(([code, signal]) => { ++ clearTimeout(clock); ++ return { code, signal, stderr }; ++ }); + return { child, exited, stderr: () => stderr }; + } + +@@ -308,5 +315,23 @@ test("no capability, or a malformed one, on stdin exits 2", async (t) => { + assert.equal((await startRunner(ctx.runDir, null).exited).code, EXIT.usage); + assert.equal((await startRunner(ctx.runDir, "not-hex").exited).code, EXIT.usage); + assert.equal((await startRunner(join(ctx.dir, "nope"), ctx.cap).exited).code, EXIT.usage); ++ // A valid capability inside an over-long line: the 4096-byte cap refuses it. ++ const long = await startRunner(ctx.runDir, ctx.cap, {}, JSON.stringify({ cap: ctx.cap, pad: "x".repeat(5000) }) + "\n").exited; ++ assert.equal(long.code, EXIT.usage, long.stderr); ++ assert.match(long.stderr, /stdin too large/); ++ assert.equal(ctx.store.get("SELECT 1 FROM role_claims WHERE role='coder'"), undefined); ++}); ++ ++test("a missing or malformed policy exits 2 before the claim", async (t) => { ++ const ctx = await setup(t); ++ const policy = join(ctx.runDir, "bundle", "policy.json"); ++ writeFileSync(policy, "{"); ++ const bad = await startRunner(ctx.runDir, ctx.cap).exited; ++ assert.equal(bad.code, EXIT.usage, bad.stderr); ++ assert.match(bad.stderr, /^runner: /m); ++ rmSync(policy); ++ const missing = await startRunner(ctx.runDir, ctx.cap).exited; ++ assert.equal(missing.code, EXIT.usage, missing.stderr); ++ assert.match(missing.stderr, /runner: ENOENT/); + assert.equal(ctx.store.get("SELECT 1 FROM role_claims WHERE role='coder'"), undefined); + }); diff --git a/agents/darkwing/work/s6-review/r2/mut/Ma-late-signals-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Ma-late-signals-cli.txt new file mode 100644 index 00000000..55ab8b1b --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Ma-late-signals-cli.txt @@ -0,0 +1,122 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (110.029105ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (127.182151ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (90.074289ms) +✔ decide prints a declining choice as declining (87.917671ms) +✔ an unknown outcome is reported once and never resent (67.972919ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (85.047445ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (84.334304ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (54.163126ms) +✔ every human command refuses inside an agent run before it touches the bus (72.449462ms) +✔ usage errors exit 4; no business and no host is a usage error (63.336747ms) +✔ agents and tasks print through the broker (74.755065ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.122452ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (40.0065ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (31.606125ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (29.688897ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (31.090093ms) +✔ a business without tracker.baseUrl gets no trackers entry (32.140689ms) +✔ an unknown business and a broken system config refuse with exit 3 (51.529886ms) +✔ empty views say so (0.635026ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.775294ms) +✔ tasks print the tracker fields the snapshot carries (0.129113ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (876.266621ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (192.48304ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (102.20185ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1124.590982ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (217.725039ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (159.104717ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (153.220614ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (116.793662ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (149.287775ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (100.247002ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (159.42089ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (24.787551ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.129277ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (206.277085ms) +✔ bus start refuses with exit 3 without a notifier config (85.342418ms) +✔ bus start runs until bus stop; status reports it while it runs (660.515393ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.962627ms) +✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (30252.541812ms) +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (18474.63404ms) +✔ a runner that stops at once ends its launch with the runner's reason (196.187752ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (30642.592854ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (33574.283695ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (107.106866ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (119.211249ms) +✔ a launch client that never closes its side doesn't hold the host's close (120.72773ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1248.394467ms) +✔ zoned uses the IANA zone across DST (14.024497ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (112.493755ms) +✔ two blocking decisions get two DMs with different nonces (115.68539ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.178117ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (101.811353ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (82.118324ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (77.476963ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (90.720043ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (141.339002ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (101.892643ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (104.335536ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (62.128633ms) +✔ an inbox read failure is logged and the next poll retries (0.611742ms) +✔ no Discord id reaches the journal or the log (54.54028ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (10.885491ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (25.147422ms) +✔ the journal: a whole file that is one torn line truncates to empty (9.885294ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.497851ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.543709ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.78457ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.303594ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.453696ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.36621ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.431344ms) +✔ digest content stays within Discord's 2000 characters (0.243198ms) +✔ runLoop never overlaps ticks and stops after the one in flight (109.722623ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (349.549925ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (30.388492ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2142.249461ms) +✔ busExit and refuseInsideAgent (0.39274ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (198.187775ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1119.413433ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (44.366985ms) +✔ launches off and on go to the broker and change the business's launch state (51.829893ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (42.907284ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 80 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 114813.584398 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:106:1 +✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (30252.541812ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + actual - expected + + + 'killed' + - 'stopped' + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:172: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: 'killed', + expected: 'stopped', + operator: 'strictEqual', + diff: 'simple' + } + +test at packages/cli/tests/launcher.test.mjs:176:1 +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (18474.63404ms) + Error: timed out waiting + at until (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:43:9) + at async TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:216:3) + 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/darkwing/work/s6-review/r2/mut/Ma-late-signals-harness.txt b/agents/darkwing/work/s6-review/r2/mut/Ma-late-signals-harness.txt new file mode 100644 index 00000000..6e5eb874 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Ma-late-signals-harness.txt @@ -0,0 +1,121 @@ +✔ sessionModel: agent vars win, then the system's execution settings (9.279787ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (5.502328ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.994908ms) +✔ a bundle is written once: an existing file refuses (3.057356ms) +✔ a path with a single quote can't go into the hook command (2.604266ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (121.145971ms) +✔ a missing or wrong policy, or a bad event, exits 2 (89.481595ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1089.811585ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (775.622561ms) +✔ claude: the hook alone blocks a path outside the workspace (444.026047ms) +✔ claude: a second turn resumes the first turn's session (705.498813ms) +✔ claude adapter: --restricted is always passed (4.999097ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (724.038133ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.432483ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.209431ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.758653ms) +✔ file tool paths must resolve inside the workspace (1.276799ms) +✔ pi's own path normalisation can't be used to step out (1.112402ms) +✔ a symlink inside the workspace that points out is outside (0.79875ms) +✔ a dangling symlink is refused at any depth, in both harnesses (3.283396ms) +✔ claude path fields per tool (0.765194ms) +✔ glob patterns stay inside the workspace (1.053637ms) +✔ a path that can't be checked is blocked (0.61845ms) +✔ initialize, ping and tools/list (43.855051ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (32.971154ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (35.596237ms) +✔ a missing argument is a usage error (32.888006ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (376.367274ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (331.603555ms) +✔ pi: a missing extension refuses before any model call (6.804337ms) +✔ pi: an extension without its configuration fails pi's start (263.892582ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.687495ms) +✔ turnRequest names the sender, class, reply and decision (0.277446ms) +✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (229.484324ms) +✖ a SIGTERM before the claim stops the runner with exit 0 and no claim (113.336966ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (419.718475ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1330.519052ms) +✖ SIGTERM during a turn kills the turn's process group and still exits 0 (137.913224ms) +✔ founder credentials stop before the claim (20) (231.142468ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (213.791143ms) +✔ the launch ending under a running session exits 22 (154.16654ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (245.559046ms) +✔ a broker that is down at the claim exits 23, not 21 (97.240524ms) +✔ no capability, or a malformed one, on stdin exits 2 (186.30622ms) +✔ a missing or malformed policy exits 2 before the claim (133.157005ms) +✔ the PM gets launch, its task verbs and the reads (7.216258ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (1.92585ms) +✔ launch only when the business's launch block names the instance as launcher (2.124721ms) +✔ an action outside the instance's authority has no tool (1.677154ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.825896ms) +ℹ tests 50 +ℹ suites 0 +ℹ pass 47 +ℹ fail 3 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10356.726177 + +✖ failing tests: + +test at packages/harness/tests/runner.test.mjs:148:1 +✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (229.484324ms) + AssertionError [ERR_ASSERTION]: runner: demo/coder claimed by run coder-run + + + null !== 0 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/runner.test.mjs:167: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: false, + code: 'ERR_ASSERTION', + actual: null, + expected: 0, + operator: 'strictEqual', + diff: 'simple' + } + +test at packages/harness/tests/runner.test.mjs:173:1 +✖ a SIGTERM before the claim stops the runner with exit 0 and no claim (113.336966ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + + actual - expected + + [ + - 0, + null, + + 'SIGTERM' + ] + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/runner.test.mjs:192: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: [ null, 'SIGTERM' ], + expected: [ 0, null ], + operator: 'deepStrictEqual', + diff: 'simple' + } + +test at packages/harness/tests/runner.test.mjs:239:1 +✖ SIGTERM during a turn kills the turn's process group and still exits 0 (137.913224ms) + AssertionError [ERR_ASSERTION]: runner: demo/coder claimed by run coder-run + + + null !== 0 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/runner.test.mjs:249: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: false, + code: 'ERR_ASSERTION', + actual: null, + expected: 0, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mb-runs-set-early-bus.txt b/agents/darkwing/work/s6-review/r2/mut/Mb-runs-set-early-bus.txt new file mode 100644 index 00000000..b5e67a3c --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mb-runs-set-early-bus.txt @@ -0,0 +1,102 @@ +✔ launch identity is stamped, payload identity is refused and stale holder cannot send (153.236192ms) +✔ decision classes route from policy; gated resolution is human-only, choice and target must match (233.592917ms) +✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (232.842814ms) +✔ launch events require a human CLI capability; generic emit cannot forge authority events (151.298667ms) +✔ within-role decisions close atomically and invalid options or blocking omissions refuse (140.874719ms) +✔ observer capabilities read human inbox but cannot mutate or forge launch identity (132.246197ms) +✔ task action subjects and linked decision trail are complete and ordered (130.571048ms) +✔ launch binding is durable and reconnecting requires the identical trusted record (82.083997ms) +✔ business isolation includes inherited object names and cross-business message references (148.470847ms) +✔ authority never transfers between action, run, target, unresolved or replaced role holder (200.947154ms) +✔ task projection uses schema current view, skipping earlier and equal-start polls (107.936316ms) +✔ revocation permanently bars the old run from reclaiming first, including after broker restart (155.37918ms) +✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (101.120038ms) +✔ both arbiters require human resolution when their cross-role route is themselves (180.901724ms) +✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.515467ms) +✔ only validated broker references load; returned data and exceptions cannot expose a known token (5.903004ms) +✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.612809ms) +✔ expiry refuses use and env references never become client data (0.859583ms) +✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.833087ms) +✔ opaque tokens shorter than 16 characters refuse before use (0.41244ms) +✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (131.151975ms) +✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (123.410748ms) +✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (139.678678ms) +✔ endLaunch leaves a claim another run took alone (150.585789ms) +✔ refuse records action.refused against the caller with the code only (116.404715ms) +✔ launches off refuses role.launch with launch-revoked until launches on (152.663293ms) +✔ broker process: launch ops authorize role.launch, record refusals and end runs (219.363656ms) +✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.330335ms) +✔ process reader gets own kernel identity without exposing environment values (1.487254ms) +✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.079579ms) +✔ real pid 1 remains inspectable when its environment is protected (0.400235ms) +✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (183.830723ms) +✔ missing and foreign-business citations refuse and roll back message and grant (163.312567ms) +✔ cross-role sends still need a matching resolved decision and consume it once (213.148623ms) +✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (178.767627ms) +✔ startup token refusal returns safe code without value or partial listening broker (50.75275ms) +✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (167.027984ms) +✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (156.059938ms) +✔ trusted host registers later launches; socket clients never have a registration verb (168.365601ms) +✔ runtime excludes declared project roots even when host supplies no repoRoots (39.743749ms) +✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (138.564671ms) +✔ v3b prototype refusals, views and append-only mutations (918.279812ms) +✔ gated approval authorizes once, survives store reopen, and fresh approval works (236.896357ms) +✔ another run cannot consume an approval; a failed check leaves it usable (197.000805ms) +✔ two scheduled callers have exactly one grant and one consumed refusal (152.926706ms) +✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (266.34715ms) +✔ class drift gated to cross-role refuses before consumption (178.53185ms) +✔ class drift cross-role to gated refuses before consumption (174.651506ms) +✔ class drift gated to within-role refuses before consumption (152.725015ms) +✔ class drift cross-role to within-role refuses before consumption (178.756218ms) +✔ class drift within-role to gated refuses before consumption (144.162896ms) +✔ class drift within-role to cross-role refuses before consumption (137.602584ms) +✔ message.send consumes approval and prevents a later send or authorize (165.574578ms) +✔ role.revoke consumes approval and prevents a later revoke or authorize (173.815805ms) +✔ creates private WAL store and excludes a second writer until explicit close (116.705782ms) +✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (163.826818ms) +✔ existing empty database and symlink runtime directory refuse, never initialize over damage (160.951856ms) +✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (161.506551ms) +✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (93.498994ms) +✔ async transactions refuse before invoking their function (79.085984ms) +✔ recordTask keeps sync reads and a role write apart (164.271125ms) +✔ read_at must be one canonical UTC format, so the projection compares strings safely (95.318007ms) +✔ a bad entry refuses the whole record (98.577209ms) +✔ taskView reads the projection for one business (118.66996ms) +✔ requestTask hands only a holder and a task verb to the handler, and records refusals (218.873429ms) +✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (386.321385ms) +✔ without an adapter the server refuses every task verb (159.312839ms) +✔ the runtime refuses an invalid adapter and closes a valid one (169.538564ms) +✔ the process loads the S3 adapter from plain-data trackers (223.02781ms) +✔ socket capability stamps launch identity; shared views use wire, no SQL client (141.76185ms) +✔ two wire claims serialize; a lost reply never automatically retries (160.373022ms) +✔ malformed, oversized and identity-forging envelopes refuse without echoing input (103.410412ms) +✔ client preserves UTF-8 when a response divides a multibyte character (11.977052ms) +✔ committed mutation followed by dropped reply reports unknown and is never retried (123.012441ms) +ℹ tests 74 +ℹ suites 0 +ℹ pass 73 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 2261.712706 + +✖ failing tests: + +test at packages/bus/tests/end-launch.test.mjs:73:1 +✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (139.678678ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/bus/tests/end-launch.test.mjs:80: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 Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /unknown-run/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mb-runs-set-early-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mb-runs-set-early-cli.txt new file mode 100644 index 00000000..1b741617 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mb-runs-set-early-cli.txt @@ -0,0 +1,92 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (122.984118ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (131.094926ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (96.333839ms) +✔ decide prints a declining choice as declining (103.065679ms) +✔ an unknown outcome is reported once and never resent (74.886646ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (90.146061ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (70.70686ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (53.526247ms) +✔ every human command refuses inside an agent run before it touches the bus (67.982352ms) +✔ usage errors exit 4; no business and no host is a usage error (68.086699ms) +✔ agents and tasks print through the broker (79.053379ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.209663ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (46.711574ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (35.139018ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (29.906609ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (37.871648ms) +✔ a business without tracker.baseUrl gets no trackers entry (34.187488ms) +✔ an unknown business and a broken system config refuse with exit 3 (56.611491ms) +✔ empty views say so (0.648387ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.818466ms) +✔ tasks print the tracker fields the snapshot carries (0.134716ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (881.329614ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (199.98338ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (130.986447ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1120.775638ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (264.870447ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (163.79319ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (156.35937ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (96.251171ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (161.579016ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (101.04276ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (180.50377ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (22.230676ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.179248ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (218.502736ms) +✔ bus start refuses with exit 3 without a notifier config (88.405387ms) +✔ bus start runs until bus stop; status reports it while it runs (655.129305ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.173049ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1273.166494ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (582.798535ms) +✔ a runner that stops at once ends its launch with the runner's reason (189.561799ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (640.78783ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3598.083011ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (108.133788ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (117.774053ms) +✔ a launch client that never closes its side doesn't hold the host's close (122.939583ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1249.204108ms) +✔ zoned uses the IANA zone across DST (16.725813ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (117.682316ms) +✔ two blocking decisions get two DMs with different nonces (116.988568ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.171321ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (98.658152ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (96.515541ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (89.289262ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (101.376665ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (127.353916ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (105.85298ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (99.002352ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (76.035595ms) +✔ an inbox read failure is logged and the next poll retries (0.67435ms) +✔ no Discord id reaches the journal or the log (50.4712ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (19.298507ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (15.863839ms) +✔ the journal: a whole file that is one torn line truncates to empty (15.787738ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.733316ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.624512ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.886773ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.332506ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.479975ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.398142ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.448945ms) +✔ digest content stays within Discord's 2000 characters (0.294064ms) +✔ runLoop never overlaps ticks and stops after the one in flight (111.979436ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (368.869723ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (39.457721ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2166.323928ms) +✔ busExit and refuseInsideAgent (0.395387ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (187.472309ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1122.277389ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (79.595789ms) +✔ launches off and on go to the broker and change the business's launch state (76.556003ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (68.706144ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 82 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 7964.388585 diff --git a/agents/darkwing/work/s6-review/r2/mut/Mc-no-run-ended-bus.txt b/agents/darkwing/work/s6-review/r2/mut/Mc-no-run-ended-bus.txt new file mode 100644 index 00000000..2fd0f6fc --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mc-no-run-ended-bus.txt @@ -0,0 +1,102 @@ +✔ launch identity is stamped, payload identity is refused and stale holder cannot send (175.826567ms) +✔ decision classes route from policy; gated resolution is human-only, choice and target must match (268.860961ms) +✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (271.683247ms) +✔ launch events require a human CLI capability; generic emit cannot forge authority events (186.811712ms) +✔ within-role decisions close atomically and invalid options or blocking omissions refuse (242.416615ms) +✔ observer capabilities read human inbox but cannot mutate or forge launch identity (226.884635ms) +✔ task action subjects and linked decision trail are complete and ordered (240.516482ms) +✔ launch binding is durable and reconnecting requires the identical trusted record (112.625817ms) +✔ business isolation includes inherited object names and cross-business message references (214.472286ms) +✔ authority never transfers between action, run, target, unresolved or replaced role holder (315.031447ms) +✔ task projection uses schema current view, skipping earlier and equal-start polls (179.983563ms) +✔ revocation permanently bars the old run from reclaiming first, including after broker restart (200.959345ms) +✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (125.747465ms) +✔ both arbiters require human resolution when their cross-role route is themselves (190.776876ms) +✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.892175ms) +✔ only validated broker references load; returned data and exceptions cannot expose a known token (5.049967ms) +✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.019704ms) +✔ expiry refuses use and env references never become client data (0.588128ms) +✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.311728ms) +✔ opaque tokens shorter than 16 characters refuse before use (0.369558ms) +✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (119.49428ms) +✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (122.730917ms) +✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (153.743346ms) +✔ endLaunch leaves a claim another run took alone (170.522948ms) +✔ refuse records action.refused against the caller with the code only (138.954956ms) +✔ launches off refuses role.launch with launch-revoked until launches on (208.024904ms) +✔ broker process: launch ops authorize role.launch, record refusals and end runs (339.242223ms) +✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.568342ms) +✔ process reader gets own kernel identity without exposing environment values (0.724472ms) +✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.076629ms) +✔ real pid 1 remains inspectable when its environment is protected (0.337982ms) +✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (185.856069ms) +✔ missing and foreign-business citations refuse and roll back message and grant (193.266271ms) +✔ cross-role sends still need a matching resolved decision and consume it once (246.414013ms) +✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (193.313699ms) +✔ startup token refusal returns safe code without value or partial listening broker (34.722496ms) +✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (198.309309ms) +✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (162.012783ms) +✔ trusted host registers later launches; socket clients never have a registration verb (178.600888ms) +✔ runtime excludes declared project roots even when host supplies no repoRoots (37.296474ms) +✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (224.977314ms) +✔ v3b prototype refusals, views and append-only mutations (1141.883601ms) +✔ gated approval authorizes once, survives store reopen, and fresh approval works (251.83052ms) +✔ another run cannot consume an approval; a failed check leaves it usable (229.201842ms) +✔ two scheduled callers have exactly one grant and one consumed refusal (168.502218ms) +✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (368.781137ms) +✔ class drift gated to cross-role refuses before consumption (303.716619ms) +✔ class drift cross-role to gated refuses before consumption (305.8573ms) +✔ class drift gated to within-role refuses before consumption (210.931ms) +✔ class drift cross-role to within-role refuses before consumption (256.176102ms) +✔ class drift within-role to gated refuses before consumption (237.786434ms) +✔ class drift within-role to cross-role refuses before consumption (213.51124ms) +✔ message.send consumes approval and prevents a later send or authorize (205.615916ms) +✔ role.revoke consumes approval and prevents a later revoke or authorize (201.284131ms) +✔ creates private WAL store and excludes a second writer until explicit close (123.676876ms) +✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (173.964344ms) +✔ existing empty database and symlink runtime directory refuse, never initialize over damage (173.329183ms) +✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (180.89419ms) +✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (110.220299ms) +✔ async transactions refuse before invoking their function (90.935183ms) +✔ recordTask keeps sync reads and a role write apart (178.34485ms) +✔ read_at must be one canonical UTC format, so the projection compares strings safely (105.066353ms) +✔ a bad entry refuses the whole record (115.43348ms) +✔ taskView reads the projection for one business (124.036913ms) +✔ requestTask hands only a holder and a task verb to the handler, and records refusals (259.12632ms) +✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (456.306481ms) +✔ without an adapter the server refuses every task verb (274.270368ms) +✔ the runtime refuses an invalid adapter and closes a valid one (247.228167ms) +✔ the process loads the S3 adapter from plain-data trackers (288.195365ms) +✔ socket capability stamps launch identity; shared views use wire, no SQL client (155.955841ms) +✔ two wire claims serialize; a lost reply never automatically retries (177.626273ms) +✔ malformed, oversized and identity-forging envelopes refuse without echoing input (118.252926ms) +✔ client preserves UTF-8 when a response divides a multibyte character (11.92216ms) +✔ committed mutation followed by dropped reply reports unknown and is never retried (151.54177ms) +ℹ tests 74 +ℹ suites 0 +ℹ pass 73 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 3036.102819 + +✖ failing tests: + +test at packages/bus/tests/end-launch.test.mjs:73:1 +✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (153.743346ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/bus/tests/end-launch.test.mjs:79: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 Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /run-ended/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mc-no-run-ended-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mc-no-run-ended-cli.txt new file mode 100644 index 00000000..0ead977a --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mc-no-run-ended-cli.txt @@ -0,0 +1,124 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (126.26257ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (122.587629ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (113.385796ms) +✔ decide prints a declining choice as declining (108.2869ms) +✔ an unknown outcome is reported once and never resent (114.02891ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (105.470563ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (76.845315ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (83.969966ms) +✔ every human command refuses inside an agent run before it touches the bus (100.618886ms) +✔ usage errors exit 4; no business and no host is a usage error (95.987002ms) +✔ agents and tasks print through the broker (92.102341ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.398543ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (43.136097ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (29.771523ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (31.208799ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (30.285756ms) +✔ a business without tracker.baseUrl gets no trackers entry (27.639752ms) +✔ an unknown business and a broken system config refuse with exit 3 (55.442178ms) +✔ empty views say so (0.701293ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.90513ms) +✔ tasks print the tracker fields the snapshot carries (0.149924ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (893.502822ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (236.939433ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (155.87989ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1161.248086ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (272.620752ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (159.700918ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (162.774057ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (101.226481ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (162.527232ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (109.231631ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (161.452378ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (24.382363ms) +✔ bus stop refuses to signal a live pid that is not a bus host (203.125876ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (209.759823ms) +✔ bus start refuses with exit 3 without a notifier config (86.551547ms) +✔ bus start runs until bus stop; status reports it while it runs (658.850114ms) +✔ bus-service.sh renders the unit and installs it into a given directory (27.00563ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1275.854375ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (602.096082ms) +✔ a runner that stops at once ends its launch with the runner's reason (207.426418ms) +✖ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (652.976913ms) +✖ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3585.184654ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (122.253724ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (141.531945ms) +✔ a launch client that never closes its side doesn't hold the host's close (129.094621ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1241.989909ms) +✔ zoned uses the IANA zone across DST (18.203626ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (120.846349ms) +✔ two blocking decisions get two DMs with different nonces (118.305315ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.26266ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (101.750783ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (92.510492ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (95.279304ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (110.580511ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (159.679295ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (149.947664ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (130.129762ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (83.888655ms) +✔ an inbox read failure is logged and the next poll retries (0.649593ms) +✔ no Discord id reaches the journal or the log (86.854487ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (27.125526ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (29.794834ms) +✔ the journal: a whole file that is one torn line truncates to empty (11.779919ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.57699ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.733241ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.817568ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.303436ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.470677ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.352871ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.440279ms) +✔ digest content stays within Discord's 2000 characters (0.253447ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.694729ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (355.095149ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (35.17381ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2138.478585ms) +✔ busExit and refuseInsideAgent (0.401007ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (210.447374ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1143.25547ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (103.818557ms) +✔ launches off and on go to the broker and change the business's launch state (85.560269ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (72.47853ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 80 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 8048.227984 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:253:3 +✖ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (652.976913ms) + AssertionError [ERR_ASSERTION]: run rfd10ac1e8ea4 (pm) from an earlier host ended: host-lost + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:308:12) + 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: false, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: '==', + diff: 'simple' + } + +test at packages/cli/tests/launcher.test.mjs:253:3 +✖ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3585.184654ms) + AssertionError [ERR_ASSERTION]: run r8f9ea986fe09 (pm) from an earlier host ended: host-lost + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:308:12) + 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: false, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: '==', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Md-no-instance-running-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Md-no-instance-running-cli.txt new file mode 100644 index 00000000..19060eb8 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Md-no-instance-running-cli.txt @@ -0,0 +1,137 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (114.924706ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (139.856708ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (164.979267ms) +✔ decide prints a declining choice as declining (104.117279ms) +✔ an unknown outcome is reported once and never resent (84.975621ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (77.461823ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (85.020978ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (72.178605ms) +✔ every human command refuses inside an agent run before it touches the bus (73.770936ms) +✔ usage errors exit 4; no business and no host is a usage error (88.478931ms) +✔ agents and tasks print through the broker (92.537743ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.125131ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (39.654827ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (32.193424ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (30.69513ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (34.294892ms) +✔ a business without tracker.baseUrl gets no trackers entry (30.904946ms) +✔ an unknown business and a broken system config refuse with exit 3 (57.72543ms) +✔ empty views say so (0.930348ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.916593ms) +✔ tasks print the tracker fields the snapshot carries (0.19946ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (912.063406ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (218.563894ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (139.935075ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1122.68701ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (224.906111ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (156.620082ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (144.463327ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (102.863039ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (152.969174ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (108.27205ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (176.554181ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (23.0238ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.545734ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (211.140698ms) +✔ bus start refuses with exit 3 without a notifier config (87.559733ms) +✔ bus start runs until bus stop; status reports it while it runs (657.201834ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.508444ms) +✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (254.585334ms) +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (590.273225ms) +✔ a runner that stops at once ends its launch with the runner's reason (232.427964ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (673.190299ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3568.794721ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (110.916678ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (115.609679ms) +✔ a launch client that never closes its side doesn't hold the host's close (122.872825ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1220.581529ms) +✔ zoned uses the IANA zone across DST (17.027978ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (115.264119ms) +✔ two blocking decisions get two DMs with different nonces (140.488003ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.176949ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (153.766588ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (93.240357ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (96.146403ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (80.318033ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (143.67716ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (117.870439ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (102.613945ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (69.197617ms) +✔ an inbox read failure is logged and the next poll retries (0.602564ms) +✔ no Discord id reaches the journal or the log (64.714634ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (23.527415ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (33.810355ms) +✔ the journal: a whole file that is one torn line truncates to empty (19.310715ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.017871ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.600636ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.703496ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.415299ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.470597ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.374525ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.435053ms) +✔ digest content stays within Discord's 2000 characters (0.251933ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.674121ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (392.840776ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (32.321136ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2145.259182ms) +✔ busExit and refuseInsideAgent (0.431683ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (226.799451ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1186.533855ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (76.542107ms) +✔ launches off and on go to the broker and change the business's launch state (64.933849ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (58.812077ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 80 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 6968.504134 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:106:1 +✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (254.585334ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /PM launch refused: instance-running/. Input: + + 'CliError: PM launch refused: capacity-full (opus sessions: 1 of 1)' + + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:138:3) + 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: CliError: PM launch refused: capacity-full (opus sessions: 1 of 1) + at file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/launcher.mjs:297:15 + at process.processTicksAndRejections (node:internal/process/task_queues:104:5), + expected: /PM launch refused: instance-running/, + operator: 'rejects', + diff: 'simple' + } + +test at packages/cli/tests/launcher.test.mjs:176:1 +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (590.273225ms) + AssertionError [ERR_ASSERTION]: coder + + actual - expected + + { + + error: 'capacity-full', + - error: 'instance-running', + ok: false + } + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:210:12) + 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: false, + code: 'ERR_ASSERTION', + actual: { ok: false, error: 'capacity-full' }, + expected: { ok: false, error: 'instance-running' }, + operator: 'deepStrictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Me-no-authorize-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Me-no-authorize-cli.txt new file mode 100644 index 00000000..cad30020 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Me-no-authorize-cli.txt @@ -0,0 +1,112 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (111.255138ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (125.508938ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (91.896149ms) +✔ decide prints a declining choice as declining (106.175686ms) +✔ an unknown outcome is reported once and never resent (74.094549ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (71.417455ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (74.132084ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (47.139946ms) +✔ every human command refuses inside an agent run before it touches the bus (69.971054ms) +✔ usage errors exit 4; no business and no host is a usage error (62.578864ms) +✔ agents and tasks print through the broker (80.062189ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.098569ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (43.309013ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (30.748981ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (29.483062ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (31.191949ms) +✔ a business without tracker.baseUrl gets no trackers entry (28.935574ms) +✔ an unknown business and a broken system config refuse with exit 3 (52.639812ms) +✔ empty views say so (0.808333ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.909846ms) +✔ tasks print the tracker fields the snapshot carries (0.130227ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (876.127063ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (190.204189ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (108.089833ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1119.713427ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (238.83377ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (157.529766ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (149.067871ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (101.662917ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (150.941681ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (116.969654ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (156.019112ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (21.18589ms) +✔ bus stop refuses to signal a live pid that is not a bus host (203.049794ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (209.851989ms) +✔ bus start refuses with exit 3 without a notifier config (87.657993ms) +✔ bus start runs until bus stop; status reports it while it runs (654.730775ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.75261ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1259.731975ms) +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (589.610248ms) +✔ a runner that stops at once ends its launch with the runner's reason (194.190384ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (640.14369ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3580.02733ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (109.908691ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (120.591714ms) +✔ a launch client that never closes its side doesn't hold the host's close (131.283386ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1233.962544ms) +✔ zoned uses the IANA zone across DST (17.874488ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (97.570286ms) +✔ two blocking decisions get two DMs with different nonces (113.227481ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.26896ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (95.38752ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (102.453301ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (87.404188ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (84.295629ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (118.959061ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (110.50822ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (92.011524ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (52.275838ms) +✔ an inbox read failure is logged and the next poll retries (0.636761ms) +✔ no Discord id reaches the journal or the log (64.498208ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (10.043392ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (14.377898ms) +✔ the journal: a whole file that is one torn line truncates to empty (9.560266ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.717191ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.820422ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.612272ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.436825ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.682133ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.575002ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.637491ms) +✔ digest content stays within Discord's 2000 characters (0.433103ms) +✔ runLoop never overlaps ticks and stops after the one in flight (111.362252ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (353.608227ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (41.611495ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2145.34634ms) +✔ busExit and refuseInsideAgent (0.470134ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (193.65513ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1112.280055ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (69.811538ms) +✔ launches off and on go to the broker and change the business's launch state (73.371508ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (67.252505ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 81 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 7942.958651 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:176:1 +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (589.610248ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:218: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: true, + expected: false, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mf-no-exit2-wrapper-harness.txt b/agents/darkwing/work/s6-review/r2/mut/Mf-no-exit2-wrapper-harness.txt new file mode 100644 index 00000000..ef3c57ec --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mf-no-exit2-wrapper-harness.txt @@ -0,0 +1,83 @@ +✔ sessionModel: agent vars win, then the system's execution settings (13.755567ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (5.041707ms) +✖ a claude-code bundle adds the wrapped gate hook and the MCP config (3.303408ms) +✔ a bundle is written once: an existing file refuses (2.427304ms) +✔ a path with a single quote can't go into the hook command (1.929757ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (124.002277ms) +✔ a missing or wrong policy, or a bad event, exits 2 (90.738011ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1091.680831ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (788.259361ms) +✔ claude: the hook alone blocks a path outside the workspace (451.984188ms) +✔ claude: a second turn resumes the first turn's session (695.642152ms) +✔ claude adapter: --restricted is always passed (5.390974ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (741.569967ms) +✔ claude: a missing hook or MCP file refuses before claude starts (8.422858ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (2.716548ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.775047ms) +✔ file tool paths must resolve inside the workspace (0.898308ms) +✔ pi's own path normalisation can't be used to step out (1.037538ms) +✔ a symlink inside the workspace that points out is outside (0.809608ms) +✔ a dangling symlink is refused at any depth, in both harnesses (3.478412ms) +✔ claude path fields per tool (0.915426ms) +✔ glob patterns stay inside the workspace (0.869385ms) +✔ a path that can't be checked is blocked (0.542139ms) +✔ initialize, ping and tools/list (45.777057ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (32.557805ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (33.320422ms) +✔ a missing argument is a usage error (29.77781ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (398.451117ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (341.426143ms) +✔ pi: a missing extension refuses before any model call (6.625067ms) +✔ pi: an extension without its configuration fails pi's start (254.365203ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.33242ms) +✔ turnRequest names the sender, class, reply and decision (0.273994ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (259.532962ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (126.492501ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (430.083188ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1291.975499ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (161.287704ms) +✔ founder credentials stop before the claim (20) (178.200231ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (193.500108ms) +✔ the launch ending under a running session exits 22 (159.092117ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (245.273136ms) +✔ a broker that is down at the claim exits 23, not 21 (87.433738ms) +✔ no capability, or a malformed one, on stdin exits 2 (193.512304ms) +✔ a missing or malformed policy exits 2 before the claim (132.080472ms) +✔ the PM gets launch, its task verbs and the reads (7.388611ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.737055ms) +✔ launch only when the business's launch block names the instance as launcher (1.442548ms) +✔ an action outside the instance's authority has no tool (2.266333ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.989301ms) +ℹ tests 50 +ℹ suites 0 +ℹ pass 49 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10395.052827 + +✖ failing tests: + +test at packages/harness/tests/bundle.test.mjs:70:1 +✖ a claude-code bundle adds the wrapped gate hook and the MCP config (3.303408ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + actual - expected + + + "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-OBpfCk/bundle/policy.json'" + - "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-OBpfCk/bundle/policy.json' || exit 2" + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/bundle.test.mjs:78: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 Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-OBpfCk/bundle/policy.json'", + expected: "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-OBpfCk/bundle/policy.json' || exit 2", + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mg-no-founder-check-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mg-no-founder-check-cli.txt new file mode 100644 index 00000000..512449c7 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mg-no-founder-check-cli.txt @@ -0,0 +1,92 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (111.845866ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (146.30493ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (162.511453ms) +✔ decide prints a declining choice as declining (134.343106ms) +✔ an unknown outcome is reported once and never resent (155.120921ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (110.249306ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (162.445035ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (154.95565ms) +✔ every human command refuses inside an agent run before it touches the bus (113.663842ms) +✔ usage errors exit 4; no business and no host is a usage error (167.959898ms) +✔ agents and tasks print through the broker (101.072847ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.600865ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (36.975588ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (29.590365ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (28.818021ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (32.286288ms) +✔ a business without tracker.baseUrl gets no trackers entry (27.839286ms) +✔ an unknown business and a broken system config refuse with exit 3 (49.857332ms) +✔ empty views say so (0.696065ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.769945ms) +✔ tasks print the tracker fields the snapshot carries (0.130196ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (926.134033ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (262.732258ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (203.540492ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1151.811701ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (241.844428ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (163.764777ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (166.480486ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (119.825338ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (150.916296ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (102.807974ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (173.317435ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (21.703006ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.903416ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (203.87514ms) +✔ bus start refuses with exit 3 without a notifier config (88.386894ms) +✔ bus start runs until bus stop; status reports it while it runs (664.97357ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.464996ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (271.460122ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (822.114219ms) +✔ a runner that stops at once ends its launch with the runner's reason (264.778461ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (710.712905ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3586.191732ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (109.435485ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (119.93556ms) +✔ a launch client that never closes its side doesn't hold the host's close (117.106423ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1230.796161ms) +✔ zoned uses the IANA zone across DST (13.655927ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (115.040495ms) +✔ two blocking decisions get two DMs with different nonces (144.766928ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.16895ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (154.750768ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (135.255437ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (165.891369ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (123.211822ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (210.647519ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (174.013402ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (199.365824ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (106.522535ms) +✔ an inbox read failure is logged and the next poll retries (0.623885ms) +✔ no Discord id reaches the journal or the log (98.496427ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (37.499023ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (47.087475ms) +✔ the journal: a whole file that is one torn line truncates to empty (21.766055ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.391083ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.832608ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.216815ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.396706ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.491873ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.397125ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.434077ms) +✔ digest content stays within Discord's 2000 characters (0.28782ms) +✔ runLoop never overlaps ticks and stops after the one in flight (111.466363ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (379.099406ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (30.405542ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2139.665054ms) +✔ busExit and refuseInsideAgent (0.575151ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (227.00156ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1194.435801ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (110.717728ms) +✔ launches off and on go to the broker and change the business's launch state (111.76061ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (88.44437ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 82 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 7312.207082 diff --git a/agents/darkwing/work/s6-review/r2/mut/Mg-no-founder-check-harness.txt b/agents/darkwing/work/s6-review/r2/mut/Mg-no-founder-check-harness.txt new file mode 100644 index 00000000..3c9304db --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mg-no-founder-check-harness.txt @@ -0,0 +1,94 @@ +✔ sessionModel: agent vars win, then the system's execution settings (13.222054ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.78026ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.683832ms) +✔ a bundle is written once: an existing file refuses (1.889732ms) +✔ a path with a single quote can't go into the hook command (2.459797ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (116.145984ms) +✔ a missing or wrong policy, or a bad event, exits 2 (81.895431ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1091.425201ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (739.790986ms) +✔ claude: the hook alone blocks a path outside the workspace (435.342932ms) +✔ claude: a second turn resumes the first turn's session (693.268054ms) +✔ claude adapter: --restricted is always passed (5.160173ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (749.610877ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.62828ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.07337ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.709143ms) +✔ file tool paths must resolve inside the workspace (1.022203ms) +✔ pi's own path normalisation can't be used to step out (1.200294ms) +✔ a symlink inside the workspace that points out is outside (1.003455ms) +✔ a dangling symlink is refused at any depth, in both harnesses (2.426274ms) +✔ claude path fields per tool (0.831067ms) +✔ glob patterns stay inside the workspace (0.981382ms) +✔ a path that can't be checked is blocked (0.437563ms) +✔ initialize, ping and tools/list (44.417769ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (35.50608ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (31.848439ms) +✔ a missing argument is a usage error (32.572346ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (355.138802ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (325.286355ms) +✔ pi: a missing extension refuses before any model call (6.428672ms) +✔ pi: an extension without its configuration fails pi's start (252.02911ms) +✖ founderCheck: founder variables, then a needed service without a usable token (1.499708ms) +✔ turnRequest names the sender, class, reply and decision (0.26081ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (246.005721ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (112.478101ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (367.945897ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1303.943908ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (140.961595ms) +✖ founder credentials stop before the claim (20) (60049.519604ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (190.97907ms) +✔ the launch ending under a running session exits 22 (147.854938ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (252.983893ms) +✔ a broker that is down at the claim exits 23, not 21 (92.036605ms) +✔ no capability, or a malformed one, on stdin exits 2 (183.523093ms) +✔ a missing or malformed policy exits 2 before the claim (124.350561ms) +✔ the PM gets launch, its task verbs and the reads (6.555341ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.985532ms) +✔ launch only when the business's launch block names the instance as launcher (1.758363ms) +✔ an action outside the instance's authority has no tool (1.720371ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.05331ms) +ℹ tests 50 +ℹ suites 0 +ℹ pass 48 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 63313.063221 + +✖ failing tests: + +test at packages/harness/tests/runner.test.mjs:34:1 +✖ founderCheck: founder variables, then a needed service without a usable token (1.499708ms) + AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. Received type object (null) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/runner.test.mjs:38:10) + 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: null, + expected: /GITEA_TOKEN, SSH_AUTH_SOCK/, + operator: 'match', + diff: 'simple' + } + +test at packages/harness/tests/runner.test.mjs:261:1 +✖ founder credentials stop before the claim (20) (60049.519604ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + null !== 20 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/runner.test.mjs:264: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: null, + expected: 20, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mh-no-close-sigkill-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mh-no-close-sigkill-cli.txt new file mode 100644 index 00000000..25aa0f61 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mh-no-close-sigkill-cli.txt @@ -0,0 +1,101 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (117.884395ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (142.011649ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (120.565159ms) +✔ decide prints a declining choice as declining (120.18831ms) +✔ an unknown outcome is reported once and never resent (94.348464ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (110.154499ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (136.631424ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (119.43147ms) +✔ every human command refuses inside an agent run before it touches the bus (107.417145ms) +✔ usage errors exit 4; no business and no host is a usage error (101.306696ms) +✔ agents and tasks print through the broker (99.437974ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.687822ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (37.224575ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (32.166755ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (31.699704ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (28.709599ms) +✔ a business without tracker.baseUrl gets no trackers entry (26.272125ms) +✔ an unknown business and a broken system config refuse with exit 3 (55.207227ms) +✔ empty views say so (0.896314ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.95593ms) +✔ tasks print the tracker fields the snapshot carries (0.129415ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (892.156207ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (236.422577ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (168.772559ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1156.932237ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (236.897622ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (166.220305ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (143.076602ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (101.342069ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (148.698243ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (109.610181ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (165.57878ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (22.954287ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.699908ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (207.538009ms) +✔ bus start refuses with exit 3 without a notifier config (86.933925ms) +✔ bus start runs until bus stop; status reports it while it runs (649.393107ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.650687ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (286.741186ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (718.573572ms) +✔ a runner that stops at once ends its launch with the runner's reason (249.070572ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (681.150741ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3554.604244ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (112.953704ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (115.357289ms) +✔ a launch client that never closes its side doesn't hold the host's close (118.67376ms) +✖ a runner that ignores SIGTERM is killed when the host closes (15218.208953ms) +✔ zoned uses the IANA zone across DST (13.770034ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (118.558601ms) +✔ two blocking decisions get two DMs with different nonces (135.781266ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.283942ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (101.862997ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (115.11545ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (113.676503ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (104.222471ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (150.320875ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (151.404666ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (134.742524ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (91.995184ms) +✔ an inbox read failure is logged and the next poll retries (0.639087ms) +✔ no Discord id reaches the journal or the log (101.837031ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (10.873146ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (16.645806ms) +✔ the journal: a whole file that is one torn line truncates to empty (31.302415ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.42336ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.6205ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.740171ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.367992ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.447342ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.340741ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.402334ms) +✔ digest content stays within Discord's 2000 characters (0.245244ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.380418ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (372.420053ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (31.887808ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2129.199091ms) +✔ busExit and refuseInsideAgent (0.410951ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (218.257861ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1129.14848ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (112.196687ms) +✔ launches off and on go to the broker and change the business's launch state (74.865699ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (45.96292ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 81 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 21130.146212 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:369:1 +✖ a runner that ignores SIGTERM is killed when the host closes (15218.208953ms) + Error: close took over 15000 ms + at Timeout._onTimeout (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:324:101) + at listOnTimeout (node:internal/timers:685:17) + at process.processTimers (node:internal/timers:618:7) diff --git a/agents/darkwing/work/s6-review/r2/mut/Mi-recover-no-kill-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mi-recover-no-kill-cli.txt new file mode 100644 index 00000000..8eeb65c7 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mi-recover-no-kill-cli.txt @@ -0,0 +1,114 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (109.913293ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (118.003851ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (112.137885ms) +✔ decide prints a declining choice as declining (89.265726ms) +✔ an unknown outcome is reported once and never resent (89.912343ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (85.265998ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (52.238147ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (68.233399ms) +✔ every human command refuses inside an agent run before it touches the bus (67.127441ms) +✔ usage errors exit 4; no business and no host is a usage error (71.682669ms) +✔ agents and tasks print through the broker (79.521375ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.228315ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (38.366322ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (31.004033ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (32.779757ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (35.905233ms) +✔ a business without tracker.baseUrl gets no trackers entry (26.900284ms) +✔ an unknown business and a broken system config refuse with exit 3 (50.20401ms) +✔ empty views say so (0.627154ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.788734ms) +✔ tasks print the tracker fields the snapshot carries (0.125733ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (880.818489ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (212.656955ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (107.839478ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1114.602676ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (237.868839ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (165.633158ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (196.857419ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (106.479174ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (154.774028ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (112.580721ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (164.633695ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (22.667092ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.90696ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (220.56323ms) +✔ bus start refuses with exit 3 without a notifier config (91.521228ms) +✔ bus start runs until bus stop; status reports it while it runs (654.540544ms) +✔ bus-service.sh renders the unit and installs it into a given directory (28.054852ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (255.366834ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (633.879827ms) +✔ a runner that stops at once ends its launch with the runner's reason (238.334932ms) +✖ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (5389.23578ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3591.216651ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (109.661685ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (109.2473ms) +✔ a launch client that never closes its side doesn't hold the host's close (117.02329ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1223.335505ms) +✔ zoned uses the IANA zone across DST (24.090291ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (107.844441ms) +✔ two blocking decisions get two DMs with different nonces (108.944801ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.180553ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (114.135107ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (92.814309ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (89.814961ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (91.252846ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (107.781293ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (111.991959ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (112.477383ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (90.654855ms) +✔ an inbox read failure is logged and the next poll retries (0.62123ms) +✔ no Discord id reaches the journal or the log (57.000052ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (18.664385ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (20.990527ms) +✔ the journal: a whole file that is one torn line truncates to empty (10.493841ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.584681ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.551121ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.801933ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.307542ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.475672ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.351519ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.426221ms) +✔ digest content stays within Discord's 2000 characters (0.257949ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.73641ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (346.30492ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (34.874451ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2130.964328ms) +✔ busExit and refuseInsideAgent (0.412311ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (195.456423ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1130.733553ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (47.180538ms) +✔ launches off and on go to the broker and change the business's launch state (56.131748ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (54.65592ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 81 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 11744.146829 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:253:3 +✖ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (5389.23578ms) + AssertionError [ERR_ASSERTION]: the old session process is gone + + actual - expected + + + '293295372' + - null + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:288:12) + 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: false, + code: 'ERR_ASSERTION', + actual: '293295372', + expected: null, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mj-no-stdin-cap-harness.txt b/agents/darkwing/work/s6-review/r2/mut/Mj-no-stdin-cap-harness.txt new file mode 100644 index 00000000..a49a95ec --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mj-no-stdin-cap-harness.txt @@ -0,0 +1,79 @@ +✔ sessionModel: agent vars win, then the system's execution settings (9.560686ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.533445ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.559218ms) +✔ a bundle is written once: an existing file refuses (1.805363ms) +✔ a path with a single quote can't go into the hook command (1.619328ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (114.276019ms) +✔ a missing or wrong policy, or a bad event, exits 2 (79.671029ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1090.201585ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (733.488775ms) +✔ claude: the hook alone blocks a path outside the workspace (434.280065ms) +✔ claude: a second turn resumes the first turn's session (693.516361ms) +✔ claude adapter: --restricted is always passed (4.81615ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (718.692902ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.611874ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (2.51607ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.642592ms) +✔ file tool paths must resolve inside the workspace (0.832544ms) +✔ pi's own path normalisation can't be used to step out (0.888885ms) +✔ a symlink inside the workspace that points out is outside (0.752018ms) +✔ a dangling symlink is refused at any depth, in both harnesses (2.350641ms) +✔ claude path fields per tool (0.682112ms) +✔ glob patterns stay inside the workspace (0.928635ms) +✔ a path that can't be checked is blocked (0.448888ms) +✔ initialize, ping and tools/list (44.092584ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (32.070127ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (34.879671ms) +✔ a missing argument is a usage error (27.189043ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (349.671657ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (326.468976ms) +✔ pi: a missing extension refuses before any model call (6.621016ms) +✔ pi: an extension without its configuration fails pi's start (254.717079ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.048867ms) +✔ turnRequest names the sender, class, reply and decision (0.218323ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (242.781802ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (100.03186ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (390.07147ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1296.269843ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (148.02031ms) +✔ founder credentials stop before the claim (20) (163.958466ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (189.269553ms) +✔ the launch ending under a running session exits 22 (136.865684ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (239.430024ms) +✔ a broker that is down at the claim exits 23, not 21 (90.747737ms) +✖ no capability, or a malformed one, on stdin exits 2 (60155.411728ms) +✔ a missing or malformed policy exits 2 before the claim (177.561752ms) +✔ the PM gets launch, its task verbs and the reads (6.482302ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (1.9674ms) +✔ launch only when the business's launch block names the instance as launcher (1.697755ms) +✔ an action outside the instance's authority has no tool (1.405452ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.133694ms) +ℹ tests 50 +ℹ suites 0 +ℹ pass 49 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 63428.846975 + +✖ failing tests: + +test at packages/harness/tests/runner.test.mjs:313:1 +✖ no capability, or a malformed one, on stdin exits 2 (60155.411728ms) + AssertionError [ERR_ASSERTION]: runner: demo/coder claimed by run coder-run + + + null !== 2 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/runner.test.mjs:320: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: false, + code: 'ERR_ASSERTION', + actual: null, + expected: 2, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mk-launch-line-max-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mk-launch-line-max-cli.txt new file mode 100644 index 00000000..0e7be9b5 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mk-launch-line-max-cli.txt @@ -0,0 +1,101 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (162.500315ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (177.934119ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (127.764402ms) +✔ decide prints a declining choice as declining (160.868866ms) +✔ an unknown outcome is reported once and never resent (150.658446ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (128.152304ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (141.319517ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (94.198963ms) +✔ every human command refuses inside an agent run before it touches the bus (99.586936ms) +✔ usage errors exit 4; no business and no host is a usage error (94.825105ms) +✔ agents and tasks print through the broker (102.257686ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.073338ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (46.435449ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (42.161475ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (32.692764ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (30.95545ms) +✔ a business without tracker.baseUrl gets no trackers entry (32.995143ms) +✔ an unknown business and a broken system config refuse with exit 3 (53.96509ms) +✔ empty views say so (0.726597ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.882059ms) +✔ tasks print the tracker fields the snapshot carries (0.14236ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (952.372413ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (223.228615ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (154.759164ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1166.978507ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (329.162296ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (205.653188ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (202.208886ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (154.339398ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (191.76797ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (153.078047ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (215.285555ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (24.175919ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.931688ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (208.721094ms) +✔ bus start refuses with exit 3 without a notifier config (96.433156ms) +✔ bus start runs until bus stop; status reports it while it runs (660.990525ms) +✔ bus-service.sh renders the unit and installs it into a given directory (27.951547ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1307.802994ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (664.453124ms) +✔ a runner that stops at once ends its launch with the runner's reason (255.681961ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (774.826194ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3654.939053ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (144.004039ms) +✖ an over-long launch request is refused at once, not at the 10 s idle timeout (3412.359464ms) +✔ a launch client that never closes its side doesn't hold the host's close (179.960494ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1248.097834ms) +✔ zoned uses the IANA zone across DST (19.34815ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (162.974921ms) +✔ two blocking decisions get two DMs with different nonces (177.914722ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.170738ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (128.140766ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (152.973701ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (148.988982ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (128.584675ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (197.312731ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (147.562084ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (114.916739ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (102.016613ms) +✔ an inbox read failure is logged and the next poll retries (0.618221ms) +✔ no Discord id reaches the journal or the log (119.100513ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (25.171504ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (44.075897ms) +✔ the journal: a whole file that is one torn line truncates to empty (27.713509ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.376026ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.639962ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.802ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.340471ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.45465ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.362259ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.42642ms) +✔ digest content stays within Discord's 2000 characters (0.255232ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.560954ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (403.836981ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (44.720137ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2133.889595ms) +✔ busExit and refuseInsideAgent (0.513786ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (295.836242ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1145.102558ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (118.588127ms) +✔ launches off and on go to the broker and change the business's launch state (114.265524ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (100.172121ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 81 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 11734.232731 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:326:1 +✖ an over-long launch request is refused at once, not at the 10 s idle timeout (3412.359464ms) + Error: the refusal took over 3000 ms + at Timeout._onTimeout (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:324:101) + at listOnTimeout (node:internal/timers:685:17) + at process.processTimers (node:internal/timers:618:7) diff --git a/agents/darkwing/work/s6-review/r2/mut/Ml-gate-pattern-harness.txt b/agents/darkwing/work/s6-review/r2/mut/Ml-gate-pattern-harness.txt new file mode 100644 index 00000000..60557cb6 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Ml-gate-pattern-harness.txt @@ -0,0 +1,82 @@ +✔ sessionModel: agent vars win, then the system's execution settings (11.194724ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.286547ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.403036ms) +✔ a bundle is written once: an existing file refuses (2.1512ms) +✔ a path with a single quote can't go into the hook command (1.731022ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (122.217615ms) +✔ a missing or wrong policy, or a bad event, exits 2 (96.735555ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1104.500034ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (946.946735ms) +✔ claude: the hook alone blocks a path outside the workspace (545.123592ms) +✔ claude: a second turn resumes the first turn's session (885.302087ms) +✔ claude adapter: --restricted is always passed (5.281479ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (956.377577ms) +✔ claude: a missing hook or MCP file refuses before claude starts (8.885071ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (2.661395ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.67723ms) +✔ file tool paths must resolve inside the workspace (0.957626ms) +✔ pi's own path normalisation can't be used to step out (0.993466ms) +✔ a symlink inside the workspace that points out is outside (0.791344ms) +✔ a dangling symlink is refused at any depth, in both harnesses (2.51196ms) +✔ claude path fields per tool (0.729902ms) +✖ glob patterns stay inside the workspace (1.92067ms) +✔ a path that can't be checked is blocked (0.824426ms) +✔ initialize, ping and tools/list (40.904169ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (33.839576ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (38.184751ms) +✔ a missing argument is a usage error (32.677945ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (392.965267ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (470.375412ms) +✔ pi: a missing extension refuses before any model call (7.163298ms) +✔ pi: an extension without its configuration fails pi's start (276.898461ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.16047ms) +✔ turnRequest names the sender, class, reply and decision (0.27114ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (235.398042ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (110.823755ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (561.541245ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1449.259176ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (218.54095ms) +✔ founder credentials stop before the claim (20) (242.729037ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (244.054228ms) +✔ the launch ending under a running session exits 22 (127.55566ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (297.466543ms) +✔ a broker that is down at the claim exits 23, not 21 (131.291616ms) +✔ no capability, or a malformed one, on stdin exits 2 (255.582554ms) +✔ a missing or malformed policy exits 2 before the claim (149.982176ms) +✔ the PM gets launch, its task verbs and the reads (6.991981ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.515836ms) +✔ launch only when the business's launch block names the instance as launcher (1.864572ms) +✔ an action outside the instance's authority has no tool (1.539859ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.903688ms) +ℹ tests 50 +ℹ suites 0 +ℹ pass 49 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10684.506761 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:115:1 +✖ glob patterns stay inside the workspace (1.92067ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/gate.test.mjs:121:5) + 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: true, + expected: false, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mm-no-revoke-bus.txt b/agents/darkwing/work/s6-review/r2/mut/Mm-no-revoke-bus.txt new file mode 100644 index 00000000..190e7154 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mm-no-revoke-bus.txt @@ -0,0 +1,102 @@ +✔ launch identity is stamped, payload identity is refused and stale holder cannot send (177.229803ms) +✔ decision classes route from policy; gated resolution is human-only, choice and target must match (275.752672ms) +✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (278.809307ms) +✔ launch events require a human CLI capability; generic emit cannot forge authority events (179.057449ms) +✔ within-role decisions close atomically and invalid options or blocking omissions refuse (166.815435ms) +✔ observer capabilities read human inbox but cannot mutate or forge launch identity (167.264193ms) +✔ task action subjects and linked decision trail are complete and ordered (180.406752ms) +✔ launch binding is durable and reconnecting requires the identical trusted record (109.64095ms) +✔ business isolation includes inherited object names and cross-business message references (252.119335ms) +✔ authority never transfers between action, run, target, unresolved or replaced role holder (424.591029ms) +✔ task projection uses schema current view, skipping earlier and equal-start polls (139.347503ms) +✔ revocation permanently bars the old run from reclaiming first, including after broker restart (246.764458ms) +✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (157.256723ms) +✔ both arbiters require human resolution when their cross-role route is themselves (256.737356ms) +✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.815371ms) +✔ only validated broker references load; returned data and exceptions cannot expose a known token (5.505282ms) +✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.675771ms) +✔ expiry refuses use and env references never become client data (0.928186ms) +✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.806798ms) +✔ opaque tokens shorter than 16 characters refuse before use (0.423882ms) +✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (134.40541ms) +✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (126.056749ms) +✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (173.037613ms) +✔ endLaunch leaves a claim another run took alone (186.025336ms) +✔ refuse records action.refused against the caller with the code only (132.051699ms) +✖ launches off refuses role.launch with launch-revoked until launches on (167.479733ms) +✔ broker process: launch ops authorize role.launch, record refusals and end runs (251.260579ms) +✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.799433ms) +✔ process reader gets own kernel identity without exposing environment values (0.636082ms) +✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.788808ms) +✔ real pid 1 remains inspectable when its environment is protected (0.341787ms) +✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (211.319193ms) +✔ missing and foreign-business citations refuse and roll back message and grant (195.215344ms) +✔ cross-role sends still need a matching resolved decision and consume it once (266.86907ms) +✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (186.52044ms) +✔ startup token refusal returns safe code without value or partial listening broker (37.446938ms) +✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (173.835214ms) +✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (188.388532ms) +✔ trusted host registers later launches; socket clients never have a registration verb (190.013239ms) +✔ runtime excludes declared project roots even when host supplies no repoRoots (37.739358ms) +✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (163.945389ms) +✔ v3b prototype refusals, views and append-only mutations (1083.019917ms) +✔ gated approval authorizes once, survives store reopen, and fresh approval works (263.722973ms) +✔ another run cannot consume an approval; a failed check leaves it usable (243.701826ms) +✔ two scheduled callers have exactly one grant and one consumed refusal (168.502436ms) +✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (325.314036ms) +✔ class drift gated to cross-role refuses before consumption (208.156247ms) +✔ class drift cross-role to gated refuses before consumption (216.120898ms) +✔ class drift gated to within-role refuses before consumption (235.048484ms) +✔ class drift cross-role to within-role refuses before consumption (328.948633ms) +✔ class drift within-role to gated refuses before consumption (281.360844ms) +✔ class drift within-role to cross-role refuses before consumption (206.289663ms) +✔ message.send consumes approval and prevents a later send or authorize (240.808783ms) +✔ role.revoke consumes approval and prevents a later revoke or authorize (280.998053ms) +✔ creates private WAL store and excludes a second writer until explicit close (138.150282ms) +✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (182.019336ms) +✔ existing empty database and symlink runtime directory refuse, never initialize over damage (211.271904ms) +✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (168.894848ms) +✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (119.46873ms) +✔ async transactions refuse before invoking their function (96.187261ms) +✔ recordTask keeps sync reads and a role write apart (185.827602ms) +✔ read_at must be one canonical UTC format, so the projection compares strings safely (108.208596ms) +✔ a bad entry refuses the whole record (126.728342ms) +✔ taskView reads the projection for one business (141.375996ms) +✔ requestTask hands only a holder and a task verb to the handler, and records refusals (253.38447ms) +✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (409.317736ms) +✔ without an adapter the server refuses every task verb (201.178983ms) +✔ the runtime refuses an invalid adapter and closes a valid one (250.764772ms) +✔ the process loads the S3 adapter from plain-data trackers (322.736733ms) +✔ socket capability stamps launch identity; shared views use wire, no SQL client (160.441912ms) +✔ two wire claims serialize; a lost reply never automatically retries (176.70387ms) +✔ malformed, oversized and identity-forging envelopes refuse without echoing input (133.427523ms) +✔ client preserves UTF-8 when a response divides a multibyte character (11.974517ms) +✔ committed mutation followed by dropped reply reports unknown and is never retried (136.481387ms) +ℹ tests 74 +ℹ suites 0 +ℹ pass 73 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 3088.587313 + +✖ failing tests: + +test at packages/bus/tests/end-launch.test.mjs:121:1 +✖ launches off refuses role.launch with launch-revoked until launches on (167.479733ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/bus/tests/end-launch.test.mjs:128: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 Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: { code: 'launch-revoked' }, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mm-no-revoke-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mm-no-revoke-cli.txt new file mode 100644 index 00000000..3f3ea7a5 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mm-no-revoke-cli.txt @@ -0,0 +1,92 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (148.613606ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (145.321931ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (110.010972ms) +✔ decide prints a declining choice as declining (128.53301ms) +✔ an unknown outcome is reported once and never resent (110.119445ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (108.850385ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (109.345423ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (58.036577ms) +✔ every human command refuses inside an agent run before it touches the bus (109.877031ms) +✔ usage errors exit 4; no business and no host is a usage error (110.264523ms) +✔ agents and tasks print through the broker (108.43468ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.011908ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (51.555735ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (32.021543ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (34.442353ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (32.395198ms) +✔ a business without tracker.baseUrl gets no trackers entry (27.413436ms) +✔ an unknown business and a broken system config refuse with exit 3 (67.657477ms) +✔ empty views say so (1.09354ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.292231ms) +✔ tasks print the tracker fields the snapshot carries (0.223616ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (920.104044ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (232.203266ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (194.33404ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1236.474176ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (491.628366ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (191.72114ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (202.467254ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (338.898401ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (173.462864ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (134.641506ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (198.871316ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (22.522396ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.309444ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (210.829679ms) +✔ bus start refuses with exit 3 without a notifier config (86.248558ms) +✔ bus start runs until bus stop; status reports it while it runs (654.76759ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.581017ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1314.913345ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (748.260563ms) +✔ a runner that stops at once ends its launch with the runner's reason (262.075043ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (852.154376ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3664.261675ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (110.17587ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (117.558237ms) +✔ a launch client that never closes its side doesn't hold the host's close (121.739664ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1231.82696ms) +✔ zoned uses the IANA zone across DST (17.450684ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (153.728134ms) +✔ two blocking decisions get two DMs with different nonces (130.565097ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.213946ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (112.875167ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (105.336718ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (115.425186ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (108.88511ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (170.201461ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (145.665616ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (139.969675ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (151.983372ms) +✔ an inbox read failure is logged and the next poll retries (0.603488ms) +✔ no Discord id reaches the journal or the log (155.240832ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (48.414629ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (85.619058ms) +✔ the journal: a whole file that is one torn line truncates to empty (32.586082ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.330476ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.560581ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (3.954422ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.306099ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.413956ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.35932ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.46591ms) +✔ digest content stays within Discord's 2000 characters (0.236425ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.673108ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (390.682228ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (44.959117ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2149.177237ms) +✔ busExit and refuseInsideAgent (0.407839ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (248.520404ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1140.169503ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (173.388107ms) +✔ launches off and on go to the broker and change the business's launch state (163.252289ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (87.164337ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 82 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 8513.932829 diff --git a/agents/darkwing/work/s6-review/r2/mut/Mn-recover-no-end-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mn-recover-no-end-cli.txt new file mode 100644 index 00000000..754c001d --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mn-recover-no-end-cli.txt @@ -0,0 +1,130 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (170.153283ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (205.099579ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (143.381162ms) +✔ decide prints a declining choice as declining (113.377221ms) +✔ an unknown outcome is reported once and never resent (106.830715ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (120.492756ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (135.211353ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (140.606626ms) +✔ every human command refuses inside an agent run before it touches the bus (149.46296ms) +✔ usage errors exit 4; no business and no host is a usage error (151.038488ms) +✔ agents and tasks print through the broker (305.111452ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.415264ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (38.26399ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (31.251151ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (29.181767ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (30.554803ms) +✔ a business without tracker.baseUrl gets no trackers entry (29.180603ms) +✔ an unknown business and a broken system config refuse with exit 3 (56.241292ms) +✔ empty views say so (0.630648ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.065816ms) +✔ tasks print the tracker fields the snapshot carries (0.192748ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (952.038923ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (298.115946ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (189.918809ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1368.931825ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (228.256463ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (161.103544ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (168.844007ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (97.165146ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (150.408035ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (115.415106ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (164.795368ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (23.584241ms) +✔ bus stop refuses to signal a live pid that is not a bus host (203.000157ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (215.496046ms) +✔ bus start refuses with exit 3 without a notifier config (90.18942ms) +✔ bus start runs until bus stop; status reports it while it runs (668.720827ms) +✔ bus-service.sh renders the unit and installs it into a given directory (27.238879ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (309.549055ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (753.366044ms) +✔ a runner that stops at once ends its launch with the runner's reason (354.246578ms) +✖ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (658.963011ms) +✖ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3389.078968ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (140.258185ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (155.002531ms) +✔ a launch client that never closes its side doesn't hold the host's close (151.488343ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1265.877319ms) +✔ zoned uses the IANA zone across DST (18.332147ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (169.368237ms) +✔ two blocking decisions get two DMs with different nonces (172.821188ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.281242ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (180.272496ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (113.603914ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (116.05131ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (110.290546ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (183.295434ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (224.239563ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (171.948181ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (304.595918ms) +✔ an inbox read failure is logged and the next poll retries (0.614526ms) +✔ no Discord id reaches the journal or the log (89.273535ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (19.528131ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (25.43311ms) +✔ the journal: a whole file that is one torn line truncates to empty (20.934135ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.158187ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.614145ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.770104ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.326489ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.448782ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.362714ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.394327ms) +✔ digest content stays within Discord's 2000 characters (0.24259ms) +✔ runLoop never overlaps ticks and stops after the one in flight (111.023465ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (411.146569ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (38.602183ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2139.679108ms) +✔ busExit and refuseInsideAgent (0.405233ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (289.638597ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1199.364246ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (299.910632ms) +✔ launches off and on go to the broker and change the business's launch state (91.086127ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (56.458221ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 80 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 7246.785077 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:253:3 +✖ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (658.963011ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + 0 !== 1 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:292:12) + 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: 0, + expected: 1, + operator: 'strictEqual', + diff: 'simple' + } + +test at packages/cli/tests/launcher.test.mjs:253:3 +✖ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3389.078968ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + 0 !== 1 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:292:12) + 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: 0, + expected: 1, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mo-policy-outside-try-harness.txt b/agents/darkwing/work/s6-review/r2/mut/Mo-policy-outside-try-harness.txt new file mode 100644 index 00000000..03582172 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mo-policy-outside-try-harness.txt @@ -0,0 +1,88 @@ +✔ sessionModel: agent vars win, then the system's execution settings (10.072066ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.464194ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.041137ms) +✔ a bundle is written once: an existing file refuses (1.693357ms) +✔ a path with a single quote can't go into the hook command (1.641576ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (121.237419ms) +✔ a missing or wrong policy, or a bad event, exits 2 (87.516879ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1110.448001ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (793.508268ms) +✔ claude: the hook alone blocks a path outside the workspace (585.02418ms) +✔ claude: a second turn resumes the first turn's session (909.649643ms) +✔ claude adapter: --restricted is always passed (8.689451ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (876.923862ms) +✔ claude: a missing hook or MCP file refuses before claude starts (10.398933ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (2.649943ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.662426ms) +✔ file tool paths must resolve inside the workspace (0.900944ms) +✔ pi's own path normalisation can't be used to step out (0.928954ms) +✔ a symlink inside the workspace that points out is outside (0.793623ms) +✔ a dangling symlink is refused at any depth, in both harnesses (2.377096ms) +✔ claude path fields per tool (0.761812ms) +✔ glob patterns stay inside the workspace (0.941867ms) +✔ a path that can't be checked is blocked (0.485438ms) +✔ initialize, ping and tools/list (48.18657ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (35.517959ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (31.251428ms) +✔ a missing argument is a usage error (43.83764ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (398.761733ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (340.73565ms) +✔ pi: a missing extension refuses before any model call (6.705612ms) +✔ pi: an extension without its configuration fails pi's start (282.254531ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.589327ms) +✔ turnRequest names the sender, class, reply and decision (0.235049ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (304.492209ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (100.17469ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (384.345215ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1297.7852ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (230.321537ms) +✔ founder credentials stop before the claim (20) (301.916569ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (323.890546ms) +✔ the launch ending under a running session exits 22 (166.444389ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (305.527659ms) +✔ a broker that is down at the claim exits 23, not 21 (141.535353ms) +✔ no capability, or a malformed one, on stdin exits 2 (266.319545ms) +✖ a missing or malformed policy exits 2 before the claim (152.302277ms) +✔ the PM gets launch, its task verbs and the reads (8.291622ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (1.846874ms) +✔ launch only when the business's launch block names the instance as launcher (1.468387ms) +✔ an action outside the instance's authority has no tool (1.482613ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (7.935313ms) +ℹ tests 50 +ℹ suites 0 +ℹ pass 49 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10428.347094 + +✖ failing tests: + +test at packages/harness/tests/runner.test.mjs:325:1 +✖ a missing or malformed policy exits 2 before the claim (152.302277ms) + AssertionError [ERR_ASSERTION]: :1 + { + + SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2) + at JSON.parse () + at main (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/src/runner.mjs:268:17) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/src/runner.mjs:369:22 + + Node.js v26.8.1 + + + 1 !== 2 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/runner.test.mjs:330: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: false, + code: 'ERR_ASSERTION', + actual: 1, + expected: 2, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mp-any-reply-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mp-any-reply-cli.txt new file mode 100644 index 00000000..1030f2ca --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mp-any-reply-cli.txt @@ -0,0 +1,110 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (148.697373ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (149.700256ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (128.806462ms) +✔ decide prints a declining choice as declining (120.583876ms) +✔ an unknown outcome is reported once and never resent (137.338274ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (127.075516ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (128.145326ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (121.555421ms) +✔ every human command refuses inside an agent run before it touches the bus (121.159027ms) +✔ usage errors exit 4; no business and no host is a usage error (114.317995ms) +✔ agents and tasks print through the broker (130.258556ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.201353ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (51.334717ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (37.112733ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (35.596109ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (33.392489ms) +✔ a business without tracker.baseUrl gets no trackers entry (31.250826ms) +✔ an unknown business and a broken system config refuse with exit 3 (51.127857ms) +✔ empty views say so (0.939438ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.310236ms) +✔ tasks print the tracker fields the snapshot carries (0.218466ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (938.914142ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (246.786456ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (190.983502ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1191.036938ms) +✖ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (160.601147ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (230.076697ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (190.261213ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (131.603939ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (195.26257ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (726.01043ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (1625.636559ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (208.572521ms) +✔ bus stop refuses to signal a live pid that is not a bus host (226.062986ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (433.248251ms) +✔ bus start refuses with exit 3 without a notifier config (192.266722ms) +✔ bus start runs until bus stop; status reports it while it runs (893.270829ms) +✔ bus-service.sh renders the unit and installs it into a given directory (29.435844ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (300.480969ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (684.354163ms) +✔ a runner that stops at once ends its launch with the runner's reason (273.925464ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (802.080225ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (4630.122168ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (254.910333ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (210.969484ms) +✔ a launch client that never closes its side doesn't hold the host's close (148.695994ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1266.625366ms) +✔ zoned uses the IANA zone across DST (21.67197ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (144.729882ms) +✔ two blocking decisions get two DMs with different nonces (155.268269ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.297991ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (126.071063ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (122.754724ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (131.978289ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (117.924008ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (176.265305ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (156.975988ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (145.563662ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (122.564352ms) +✔ an inbox read failure is logged and the next poll retries (0.603809ms) +✔ no Discord id reaches the journal or the log (122.743751ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (34.661862ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (54.673675ms) +✔ the journal: a whole file that is one torn line truncates to empty (29.981829ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.234882ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.621975ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.834696ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.466476ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.510543ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.383721ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.427926ms) +✔ digest content stays within Discord's 2000 characters (0.255025ms) +✔ runLoop never overlaps ticks and stops after the one in flight (111.486218ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (428.349483ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (46.508343ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2169.509112ms) +✔ busExit and refuseInsideAgent (0.4227ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (244.465517ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1151.118912ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (112.967802ms) +✔ launches off and on go to the broker and change the business's launch state (140.237907ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (140.61441ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 81 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 8661.544704 + +✖ failing tests: + +test at packages/cli/tests/host.test.mjs:240:1 +✖ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (160.601147ms) + AssertionError [ERR_ASSERTION]: another id + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/host.test.mjs:258: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: CliError: identity refused: unauthenticated + at file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:205:29 + at process.processTicksAndRejections (node:internal/process/task_queues:104:5), + operator: 'rejects', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mq-no-timer-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mq-no-timer-cli.txt new file mode 100644 index 00000000..99a8aaca --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mq-no-timer-cli.txt @@ -0,0 +1,98 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (161.282919ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (216.406574ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (101.711895ms) +✔ decide prints a declining choice as declining (118.770223ms) +✔ an unknown outcome is reported once and never resent (88.180161ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (133.631087ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (89.273035ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (64.242211ms) +✔ every human command refuses inside an agent run before it touches the bus (81.769885ms) +✔ usage errors exit 4; no business and no host is a usage error (76.046858ms) +✔ agents and tasks print through the broker (98.61662ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.639647ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (74.144883ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (45.990257ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (37.454954ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (33.945313ms) +✔ a business without tracker.baseUrl gets no trackers entry (31.546787ms) +✔ an unknown business and a broken system config refuse with exit 3 (59.210979ms) +✔ empty views say so (1.234024ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.528788ms) +✔ tasks print the tracker fields the snapshot carries (0.260352ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1110.739563ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (212.314627ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (117.462697ms) +✖ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (30011.627316ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (361.14127ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (238.779627ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (209.344431ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (142.395402ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (201.707236ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (140.62722ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (211.583262ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (23.170158ms) +✔ bus stop refuses to signal a live pid that is not a bus host (201.927537ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (212.818094ms) +✔ bus start refuses with exit 3 without a notifier config (97.691899ms) +✔ bus start runs until bus stop; status reports it while it runs (712.239824ms) +✔ bus-service.sh renders the unit and installs it into a given directory (33.025617ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1408.528387ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (678.289224ms) +✔ a runner that stops at once ends its launch with the runner's reason (232.87958ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (636.773128ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3685.682808ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (107.723084ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (147.617186ms) +✔ a launch client that never closes its side doesn't hold the host's close (165.141582ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1256.881502ms) +✔ zoned uses the IANA zone across DST (24.975618ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (118.650939ms) +✔ two blocking decisions get two DMs with different nonces (236.384189ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.254913ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (108.217406ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (102.65163ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (104.429391ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (126.330216ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (152.955109ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (114.784564ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (111.484479ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (96.945293ms) +✔ an inbox read failure is logged and the next poll retries (0.635993ms) +✔ no Discord id reaches the journal or the log (59.412086ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (21.056217ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (26.922879ms) +✔ the journal: a whole file that is one torn line truncates to empty (10.716354ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.524492ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.548308ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (5.00263ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.321553ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.472191ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.36437ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.44078ms) +✔ digest content stays within Discord's 2000 characters (0.281593ms) +✔ runLoop never overlaps ticks and stops after the one in flight (111.647453ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (533.872157ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (56.300634ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2175.145866ms) +✔ busExit and refuseInsideAgent (0.405042ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (321.44385ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1112.326063ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (86.567989ms) +✔ launches off and on go to the broker and change the business's launch state (119.61121ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (119.535284ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 81 +ℹ fail 0 +ℹ cancelled 1 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 34369.220268 + +✖ failing tests: + +test at packages/cli/tests/host.test.mjs:206:1 +✖ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (30011.627316ms) + 'test timed out after 30000ms' diff --git a/agents/darkwing/work/s6-review/r2/mut/Mr-ignore-unsolicited-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mr-ignore-unsolicited-cli.txt new file mode 100644 index 00000000..ff4adbcb --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mr-ignore-unsolicited-cli.txt @@ -0,0 +1,92 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (138.638924ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (132.941019ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (116.731662ms) +✔ decide prints a declining choice as declining (109.132344ms) +✔ an unknown outcome is reported once and never resent (117.897707ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (122.014581ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (100.722744ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (77.9869ms) +✔ every human command refuses inside an agent run before it touches the bus (83.470336ms) +✔ usage errors exit 4; no business and no host is a usage error (89.721984ms) +✔ agents and tasks print through the broker (86.401911ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.10934ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (55.168327ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (39.926919ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (33.523335ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (30.393071ms) +✔ a business without tracker.baseUrl gets no trackers entry (27.532027ms) +✔ an unknown business and a broken system config refuse with exit 3 (57.895953ms) +✔ empty views say so (0.929157ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.990349ms) +✔ tasks print the tracker fields the snapshot carries (0.155053ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (908.592046ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (215.303058ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (145.876256ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1165.049844ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (389.403888ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (238.871198ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (215.473048ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (164.522219ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (243.423454ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (136.171525ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (198.742801ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (26.600601ms) +✔ bus stop refuses to signal a live pid that is not a bus host (203.603644ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (227.895553ms) +✔ bus start refuses with exit 3 without a notifier config (91.022479ms) +✔ bus start runs until bus stop; status reports it while it runs (761.049696ms) +✔ bus-service.sh renders the unit and installs it into a given directory (35.781901ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1298.198148ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (655.756425ms) +✔ a runner that stops at once ends its launch with the runner's reason (253.622608ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (810.912019ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3789.366254ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (150.305011ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (156.429058ms) +✔ a launch client that never closes its side doesn't hold the host's close (182.414216ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1284.428961ms) +✔ zoned uses the IANA zone across DST (26.607121ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (124.92784ms) +✔ two blocking decisions get two DMs with different nonces (127.300122ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.186263ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (114.696626ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (105.643281ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (127.026089ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (104.520561ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (176.552447ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (124.599483ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (106.56747ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (96.82751ms) +✔ an inbox read failure is logged and the next poll retries (0.616406ms) +✔ no Discord id reaches the journal or the log (110.898763ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (22.412109ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (38.367565ms) +✔ the journal: a whole file that is one torn line truncates to empty (31.163172ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (3.4225ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.619509ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.778816ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.459667ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.464156ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.361591ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.425791ms) +✔ digest content stays within Discord's 2000 characters (0.259698ms) +✔ runLoop never overlaps ticks and stops after the one in flight (112.028169ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (380.372573ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (48.608719ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2154.943017ms) +✔ busExit and refuseInsideAgent (0.460744ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (214.990431ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1155.659087ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (102.063205ms) +✔ launches off and on go to the broker and change the business's launch state (117.097829ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (95.414427ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 82 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 8670.126908 diff --git a/agents/darkwing/work/s6-review/r2/mut/Ms-follow-walk-harness.txt b/agents/darkwing/work/s6-review/r2/mut/Ms-follow-walk-harness.txt new file mode 100644 index 00000000..48a8b6b3 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Ms-follow-walk-harness.txt @@ -0,0 +1,100 @@ +✔ sessionModel: agent vars win, then the system's execution settings (14.267841ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (5.331756ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.063225ms) +✔ a bundle is written once: an existing file refuses (2.3933ms) +✔ a path with a single quote can't go into the hook command (2.229339ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (149.790714ms) +✔ a missing or wrong policy, or a bad event, exits 2 (114.816308ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1113.267018ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (1052.590796ms) +✔ claude: the hook alone blocks a path outside the workspace (566.552275ms) +✔ claude: a second turn resumes the first turn's session (735.772562ms) +✔ claude adapter: --restricted is always passed (5.210642ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (752.794353ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.481333ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (4.137095ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (1.087122ms) +✔ file tool paths must resolve inside the workspace (1.830471ms) +✔ pi's own path normalisation can't be used to step out (1.461666ms) +✔ a symlink inside the workspace that points out is outside (1.387513ms) +✖ a dangling symlink is refused at any depth, in both harnesses (2.238046ms) +✔ claude path fields per tool (1.183856ms) +✔ glob patterns stay inside the workspace (1.290554ms) +✔ a path that can't be checked is blocked (0.757566ms) +✔ initialize, ping and tools/list (55.169248ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (42.192702ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (40.690799ms) +✔ a missing argument is a usage error (45.831681ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (447.976546ms) +✖ pi: a write through a dangling symlink is blocked, and nothing appears outside (460.290575ms) +✔ pi: a missing extension refuses before any model call (8.272109ms) +✔ pi: an extension without its configuration fails pi's start (317.932286ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.482243ms) +✔ turnRequest names the sender, class, reply and decision (0.191539ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (335.253633ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (218.308665ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (578.967725ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1419.210919ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (200.913598ms) +✔ founder credentials stop before the claim (20) (278.03445ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (255.703953ms) +✔ the launch ending under a running session exits 22 (170.51835ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (285.056323ms) +✔ a broker that is down at the claim exits 23, not 21 (141.580326ms) +✔ no capability, or a malformed one, on stdin exits 2 (220.456576ms) +✔ a missing or malformed policy exits 2 before the claim (170.29542ms) +✔ the PM gets launch, its task verbs and the reads (10.850788ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.930345ms) +✔ launch only when the business's launch block names the instance as launcher (2.233962ms) +✔ an action outside the instance's authority has no tool (2.476419ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (13.327592ms) +ℹ tests 50 +ℹ suites 0 +ℹ pass 48 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10898.485035 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:82:1 +✖ a dangling symlink is refused at any depth, in both harnesses (2.238046ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/gate.test.mjs:92:7) + 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: true, + expected: false, + operator: 'strictEqual', + diff: 'simple' + } + +test at packages/harness/tests/pi-session.test.mjs:99:1 +✖ pi: a write through a dangling symlink is blocked, and nothing appears outside (460.290575ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + false !== true + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/pi-session.test.mjs:112:10) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mt-no-socket-destroy-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mt-no-socket-destroy-cli.txt new file mode 100644 index 00000000..68a887a2 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mt-no-socket-destroy-cli.txt @@ -0,0 +1,101 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (131.528352ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (142.345308ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (129.209329ms) +✔ decide prints a declining choice as declining (126.056946ms) +✔ an unknown outcome is reported once and never resent (264.240021ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (183.909761ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (209.444872ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (113.38368ms) +✔ every human command refuses inside an agent run before it touches the bus (107.245204ms) +✔ usage errors exit 4; no business and no host is a usage error (133.581676ms) +✔ agents and tasks print through the broker (130.930415ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.69181ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (59.595479ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (42.730476ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (37.233092ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (34.758544ms) +✔ a business without tracker.baseUrl gets no trackers entry (32.376378ms) +✔ an unknown business and a broken system config refuse with exit 3 (59.265798ms) +✔ empty views say so (1.36445ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.627688ms) +✔ tasks print the tracker fields the snapshot carries (0.27856ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (946.036696ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (325.488492ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (185.16169ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1218.491359ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (335.746778ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (216.683428ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (242.774968ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (155.236033ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (191.091803ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (169.814045ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (240.918562ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (23.585391ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.733928ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (246.621308ms) +✔ bus start refuses with exit 3 without a notifier config (108.533333ms) +✔ bus start runs until bus stop; status reports it while it runs (718.328651ms) +✔ bus-service.sh renders the unit and installs it into a given directory (29.399566ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1322.484728ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (708.944623ms) +✔ a runner that stops at once ends its launch with the runner's reason (238.787251ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (796.066461ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3644.773768ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (131.691436ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (134.617147ms) +✖ a launch client that never closes its side doesn't hold the host's close (5141.26814ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1245.744729ms) +✔ zoned uses the IANA zone across DST (21.727344ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (135.654962ms) +✔ two blocking decisions get two DMs with different nonces (134.029654ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.219983ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (127.900132ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (118.574638ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (253.517992ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (187.1812ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (245.139275ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (150.086073ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (154.204532ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (126.531257ms) +✔ an inbox read failure is logged and the next poll retries (0.638941ms) +✔ no Discord id reaches the journal or the log (138.385811ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (25.990241ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (16.992018ms) +✔ the journal: a whole file that is one torn line truncates to empty (24.49927ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.181954ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.610406ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.787671ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.294652ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.444251ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.362729ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.494578ms) +✔ digest content stays within Discord's 2000 characters (0.253667ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.515683ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (403.373726ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (45.853ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2149.662427ms) +✔ busExit and refuseInsideAgent (0.413335ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (237.6072ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1139.313934ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (128.151994ms) +✔ launches off and on go to the broker and change the business's launch state (152.199859ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (121.178327ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 81 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 13457.09326 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:337:1 +✖ a launch client that never closes its side doesn't hold the host's close (5141.26814ms) + Error: close took over 5000 ms + at Timeout._onTimeout (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/tests/launcher.test.mjs:324:101) + at listOnTimeout (node:internal/timers:685:17) + at process.processTimers (node:internal/timers:618:7) diff --git a/agents/darkwing/work/s6-review/r2/mut/Mu-no-restricted-harness.txt b/agents/darkwing/work/s6-review/r2/mut/Mu-no-restricted-harness.txt new file mode 100644 index 00000000..85db62a6 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mu-no-restricted-harness.txt @@ -0,0 +1,102 @@ +✔ sessionModel: agent vars win, then the system's execution settings (10.94696ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.717387ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.654246ms) +✔ a bundle is written once: an existing file refuses (2.265185ms) +✔ a path with a single quote can't go into the hook command (2.444314ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (125.8015ms) +✔ a missing or wrong policy, or a bad event, exits 2 (95.187298ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1095.106113ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (819.040254ms) +✔ claude: the hook alone blocks a path outside the workspace (460.007196ms) +✔ claude: a second turn resumes the first turn's session (742.523714ms) +✖ claude adapter: --restricted is always passed (6.097611ms) +✖ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (455.591086ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.562261ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (2.64706ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.790916ms) +✔ file tool paths must resolve inside the workspace (1.195488ms) +✔ pi's own path normalisation can't be used to step out (1.046526ms) +✔ a symlink inside the workspace that points out is outside (0.779909ms) +✔ a dangling symlink is refused at any depth, in both harnesses (2.947901ms) +✔ claude path fields per tool (0.952429ms) +✔ glob patterns stay inside the workspace (1.047138ms) +✔ a path that can't be checked is blocked (0.452905ms) +✔ initialize, ping and tools/list (46.485032ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (35.386331ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (32.536619ms) +✔ a missing argument is a usage error (32.23212ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (382.741035ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (328.302617ms) +✔ pi: a missing extension refuses before any model call (7.445419ms) +✔ pi: an extension without its configuration fails pi's start (262.696262ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.634907ms) +✔ turnRequest names the sender, class, reply and decision (0.272949ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (277.257539ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (114.949ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (437.01996ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1287.409236ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (194.407345ms) +✔ founder credentials stop before the claim (20) (203.734693ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (216.376213ms) +✔ the launch ending under a running session exits 22 (170.260876ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (282.678026ms) +✔ a broker that is down at the claim exits 23, not 21 (110.208765ms) +✔ no capability, or a malformed one, on stdin exits 2 (220.855985ms) +✔ a missing or malformed policy exits 2 before the claim (152.929041ms) +✔ the PM gets launch, its task verbs and the reads (7.712731ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.59188ms) +✔ launch only when the business's launch block names the instance as launcher (1.494155ms) +✔ an action outside the instance's authority has no tool (1.530795ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (9.705728ms) +ℹ tests 50 +ℹ suites 0 +ℹ pass 48 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10435.898259 + +✖ failing tests: + +test at packages/harness/tests/claude-session.test.mjs:143:1 +✖ claude adapter: --restricted is always passed (6.097611ms) + AssertionError [ERR_ASSERTION]: -p x --output-format text --system-prompt {} --model claude-sonnet-5-5 --tools --allowedTools mcp__mosaic --permission-mode dontAsk --settings /home/jwoltje/darkwing-scratch/tmp/mosaic-harness-4l0w4H/settings.json --strict-mcp-config --mcp-config /home/jwoltje/darkwing-scratch/tmp/mosaic-harness-4l0w4H/mcp.json --disable-slash-commands --session-id a3f9e9f9-910b-4c70-83f6-95e95f157e7b + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/claude-session.test.mjs:166: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 Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: '==', + diff: 'simple' + } + +test at packages/harness/tests/claude-session.test.mjs:170:1 +✖ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (455.591086ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + + actual - expected + + + [ + + 'MARKER-USER', + + 'MARKER-PARENT', + + 'MARKER-WS', + + 'MARKER-MEMORY' + + ] + - [] + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/tests/claude-session.test.mjs:193:10) + 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: [ 'MARKER-USER', 'MARKER-PARENT', 'MARKER-WS', 'MARKER-MEMORY' ], + expected: [], + operator: 'deepStrictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mv-no-tag-bus.txt b/agents/darkwing/work/s6-review/r2/mut/Mv-no-tag-bus.txt new file mode 100644 index 00000000..827af436 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mv-no-tag-bus.txt @@ -0,0 +1,102 @@ +✔ launch identity is stamped, payload identity is refused and stale holder cannot send (156.688073ms) +✔ decision classes route from policy; gated resolution is human-only, choice and target must match (254.990034ms) +✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (263.4071ms) +✔ launch events require a human CLI capability; generic emit cannot forge authority events (161.541146ms) +✔ within-role decisions close atomically and invalid options or blocking omissions refuse (168.597858ms) +✔ observer capabilities read human inbox but cannot mutate or forge launch identity (163.557403ms) +✔ task action subjects and linked decision trail are complete and ordered (196.811971ms) +✔ launch binding is durable and reconnecting requires the identical trusted record (98.107431ms) +✔ business isolation includes inherited object names and cross-business message references (184.203812ms) +✔ authority never transfers between action, run, target, unresolved or replaced role holder (264.871253ms) +✔ task projection uses schema current view, skipping earlier and equal-start polls (151.601254ms) +✔ revocation permanently bars the old run from reclaiming first, including after broker restart (208.945298ms) +✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (132.772431ms) +✔ both arbiters require human resolution when their cross-role route is themselves (206.679273ms) +✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.721977ms) +✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.468229ms) +✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.381998ms) +✔ expiry refuses use and env references never become client data (0.725347ms) +✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.585195ms) +✔ opaque tokens shorter than 16 characters refuse before use (0.367562ms) +✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (115.594666ms) +✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (125.580874ms) +✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (149.334492ms) +✔ endLaunch leaves a claim another run took alone (179.262151ms) +✔ refuse records action.refused against the caller with the code only (123.396171ms) +✔ launches off refuses role.launch with launch-revoked until launches on (182.423177ms) +✖ broker process: launch ops authorize role.launch, record refusals and end runs (255.509266ms) +✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.798434ms) +✔ process reader gets own kernel identity without exposing environment values (0.564421ms) +✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.86072ms) +✔ real pid 1 remains inspectable when its environment is protected (0.307777ms) +✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (165.580215ms) +✔ missing and foreign-business citations refuse and roll back message and grant (179.045078ms) +✔ cross-role sends still need a matching resolved decision and consume it once (248.113233ms) +✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (178.726728ms) +✔ startup token refusal returns safe code without value or partial listening broker (36.961343ms) +✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (179.862873ms) +✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (201.608434ms) +✔ trusted host registers later launches; socket clients never have a registration verb (188.507925ms) +✔ runtime excludes declared project roots even when host supplies no repoRoots (50.946893ms) +✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (202.870546ms) +✔ v3b prototype refusals, views and append-only mutations (996.668482ms) +✔ gated approval authorizes once, survives store reopen, and fresh approval works (230.014396ms) +✔ another run cannot consume an approval; a failed check leaves it usable (219.424366ms) +✔ two scheduled callers have exactly one grant and one consumed refusal (163.386829ms) +✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (313.530515ms) +✔ class drift gated to cross-role refuses before consumption (203.402278ms) +✔ class drift cross-role to gated refuses before consumption (231.111744ms) +✔ class drift gated to within-role refuses before consumption (205.601926ms) +✔ class drift cross-role to within-role refuses before consumption (207.540289ms) +✔ class drift within-role to gated refuses before consumption (227.612879ms) +✔ class drift within-role to cross-role refuses before consumption (182.67593ms) +✔ message.send consumes approval and prevents a later send or authorize (205.696724ms) +✔ role.revoke consumes approval and prevents a later revoke or authorize (223.52629ms) +✔ creates private WAL store and excludes a second writer until explicit close (106.384558ms) +✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (172.585017ms) +✔ existing empty database and symlink runtime directory refuse, never initialize over damage (194.975755ms) +✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (191.459506ms) +✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (110.751067ms) +✔ async transactions refuse before invoking their function (97.662881ms) +✔ recordTask keeps sync reads and a role write apart (161.995121ms) +✔ read_at must be one canonical UTC format, so the projection compares strings safely (99.394876ms) +✔ a bad entry refuses the whole record (105.139574ms) +✔ taskView reads the projection for one business (144.68784ms) +✔ requestTask hands only a holder and a task verb to the handler, and records refusals (234.262551ms) +✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (429.558263ms) +✔ without an adapter the server refuses every task verb (209.208603ms) +✔ the runtime refuses an invalid adapter and closes a valid one (211.154568ms) +✔ the process loads the S3 adapter from plain-data trackers (304.261499ms) +✔ socket capability stamps launch identity; shared views use wire, no SQL client (130.132782ms) +✔ two wire claims serialize; a lost reply never automatically retries (167.754097ms) +✔ malformed, oversized and identity-forging envelopes refuse without echoing input (119.612349ms) +✔ client preserves UTF-8 when a response divides a multibyte character (13.392997ms) +✔ committed mutation followed by dropped reply reports unknown and is never retried (137.570475ms) +ℹ tests 74 +ℹ suites 0 +ℹ pass 73 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 2705.19541 + +✖ failing tests: + +test at packages/bus/tests/end-launch.test.mjs:135:1 +✖ broker process: launch ops authorize role.launch, record refusals and end runs (255.509266ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + undefined !== 7 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/bus/tests/end-launch.test.mjs:179: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: undefined, + expected: 7, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/Mv-no-tag-cli.txt b/agents/darkwing/work/s6-review/r2/mut/Mv-no-tag-cli.txt new file mode 100644 index 00000000..b4b239c0 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/Mv-no-tag-cli.txt @@ -0,0 +1,208 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (136.584442ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (141.940734ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (132.039183ms) +✔ decide prints a declining choice as declining (143.587352ms) +✔ an unknown outcome is reported once and never resent (124.761265ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (121.794483ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (135.996842ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (101.111215ms) +✔ every human command refuses inside an agent run before it touches the bus (109.364301ms) +✔ usage errors exit 4; no business and no host is a usage error (96.177626ms) +✔ agents and tasks print through the broker (112.915537ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.21964ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (49.164257ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (38.064735ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (32.805116ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (33.123203ms) +✔ a business without tracker.baseUrl gets no trackers entry (32.302573ms) +✔ an unknown business and a broken system config refuse with exit 3 (60.310168ms) +✔ empty views say so (1.146721ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.493659ms) +✔ tasks print the tracker fields the snapshot carries (0.226234ms) +mosaic-notify: notify: poll failed: ENOENT: no such file or directory, open '/home/jwoltje/darkwing-scratch/tmp/mosaic-cli-5w2KeE/data/notify/acme/sent.jsonl' +✖ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (273.165302ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (284.22541ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (183.834661ms) +✖ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (171.069067ms) +✖ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (164.966958ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (217.716459ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (198.431849ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (144.888946ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (199.525503ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (146.507475ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (213.318879ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (26.316119ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.981804ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (226.439466ms) +✔ bus start refuses with exit 3 without a notifier config (91.492051ms) +✔ bus start runs until bus stop; status reports it while it runs (746.921036ms) +✔ bus-service.sh renders the unit and installs it into a given directory (29.344451ms) + +Interrupted while running: + +⚠ packages/cli/tests/launcher.test.mjs (packages/cli/tests/launcher.test.mjs:1:1) +⚠ packages/cli/tests/notifier.test.mjs (packages/cli/tests/notifier.test.mjs:1:1) +⚠ packages/cli/tests/trackers-boot.test.mjs (packages/cli/tests/trackers-boot.test.mjs:1:1) +⚠ packages/cli/tests/transport.test.mjs (packages/cli/tests/transport.test.mjs:1:1) +⚠ packages/cli/tests/verbs.test.mjs (packages/cli/tests/verbs.test.mjs:1:1) +✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (217.752184ms) +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (187.022254ms) +✖ a runner that stops at once ends its launch with the runner's reason (196.677194ms) +file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:166 + const fail = (code, text) => Object.assign(new CliError(text, 1), { code }); + ^ + +CliError: broker channel broken: reply for another request + at fail (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:166:46) + at breakChannel (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:172:22) + at ChildProcess. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:185:12) + at ChildProcess.emit (node:events:514:20) + at emit (node:internal/child_process:973:14) + at process.processTicksAndRejections (node:internal/process/task_queues:91:21) { + exitCode: 1, + code: 'broker-channel-broken' +} + +Node.js v26.8.1 +✖ packages/cli/tests/launcher.test.mjs (899959.906082ms) +✔ zoned uses the IANA zone across DST (23.264734ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (138.651891ms) +✔ two blocking decisions get two DMs with different nonces (135.324908ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.186002ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (130.279935ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (118.259498ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (131.903789ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (130.558414ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (194.25207ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (143.608044ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (117.165811ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (110.833235ms) +✔ an inbox read failure is logged and the next poll retries (0.698135ms) +✔ no Discord id reaches the journal or the log (102.537652ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (25.475607ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (41.724142ms) +✔ the journal: a whole file that is one torn line truncates to empty (20.888812ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (5.607382ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.76875ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.591218ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.385496ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.611992ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.588504ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.554506ms) +✔ digest content stays within Discord's 2000 characters (0.297253ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.70324ms) +tasks acme startup tracker-unavailable +✖ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (250.626352ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (37.535565ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2165.50339ms) +✔ busExit and refuseInsideAgent (0.453975ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (235.634935ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1137.945416ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (97.50083ms) +✔ launches off and on go to the broker and change the business's launch state (102.966001ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (98.360172ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 69 +ℹ fail 7 +ℹ cancelled 1 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 899978.394176 + +✖ failing tests: + +test at packages/cli/tests/host.test.mjs:124:1 +✖ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (273.165302ms) + Error [CliError]: broker channel broken: reply for another request + at fail (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:166:46) + at breakChannel (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:172:22) + at ChildProcess. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:185:12) + at ChildProcess.emit (node:events:514:20) + at emit (node:internal/child_process:973:14) + at process.processTicksAndRejections (node:internal/process/task_queues:91:21) { + exitCode: 1, + code: 'broker-channel-broken' + } + +test at packages/cli/tests/host.test.mjs:206:1 +✖ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (171.069067ms) + Error [CliError]: broker channel broken: reply for another request + at fail (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:166:46) + at breakChannel (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:172:22) + at ChildProcess. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:185:12) + at ChildProcess.emit (node:events:514:20) + at emit (node:internal/child_process:973:14) + at process.processTicksAndRejections (node:internal/process/task_queues:91:21) { + exitCode: 1, + code: 'broker-channel-broken' + } + +test at packages/cli/tests/host.test.mjs:240:1 +✖ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (164.966958ms) + Error [CliError]: broker channel broken: reply for another request + at fail (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:166:46) + at breakChannel (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:172:22) + at ChildProcess. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:185:12) + at ChildProcess.emit (node:events:514:20) + at emit (node:internal/child_process:973:14) + at process.processTicksAndRejections (node:internal/process/task_queues:91:21) { + exitCode: 1, + code: 'broker-channel-broken' + } + +test at packages/cli/tests/launcher.test.mjs:106:1 +✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (217.752184ms) + Error [CliError]: broker channel broken: reply for another request + at fail (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:166:46) + at breakChannel (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:172:22) + at ChildProcess. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:185:12) + at ChildProcess.emit (node:events:514:20) + at emit (node:internal/child_process:973:14) + at process.processTicksAndRejections (node:internal/process/task_queues:91:21) { + exitCode: 1, + code: 'broker-channel-broken' + } + +test at packages/cli/tests/launcher.test.mjs:176:1 +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (187.022254ms) + Error [CliError]: broker channel broken: reply for another request + at fail (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:166:46) + at breakChannel (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:172:22) + at ChildProcess. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:185:12) + at ChildProcess.emit (node:events:514:20) + at emit (node:internal/child_process:973:14) + at process.processTicksAndRejections (node:internal/process/task_queues:91:21) { + exitCode: 1, + code: 'broker-channel-broken' + } + +test at packages/cli/tests/launcher.test.mjs:237:1 +✖ a runner that stops at once ends its launch with the runner's reason (196.677194ms) + Error [CliError]: broker channel broken: reply for another request + at fail (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:166:46) + at breakChannel (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:172:22) + at ChildProcess. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:185:12) + at ChildProcess.emit (node:events:514:20) + at emit (node:internal/child_process:973:14) + at process.processTicksAndRejections (node:internal/process/task_queues:91:21) { + exitCode: 1, + code: 'broker-channel-broken' + } + +test at packages/cli/tests/launcher.test.mjs:1:1 +✖ packages/cli/tests/launcher.test.mjs (899959.906082ms) + 'Promise resolution is still pending but the event loop has already resolved' + +test at packages/cli/tests/trackers-boot.test.mjs:15:1 +✖ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (250.626352ms) + Error [CliError]: broker channel broken: reply for another request + at fail (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:166:46) + at breakChannel (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:172:22) + at ChildProcess. (file:///home/jwoltje/darkwing-scratch/r41b/wt/packages/cli/src/host.mjs:185:12) + at ChildProcess.emit (node:events:514:20) + at emit (node:internal/child_process:973:14) + at process.processTicksAndRejections (node:internal/process/task_queues:91:21) { + exitCode: 1, + code: 'broker-channel-broken' + } diff --git a/agents/darkwing/work/s6-review/r2/mut/all.log b/agents/darkwing/work/s6-review/r2/mut/all.log new file mode 100644 index 00000000..753c0168 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/all.log @@ -0,0 +1,22 @@ +Ma-late-signals (packages/harness/src/runner.mjs): harness rc 1 pass 47 fail 3; cli rc 1 pass 80 fail 2; +Mb-runs-set-early (packages/bus/src/broker.mjs): bus rc 1 pass 73 fail 1; cli rc 0 pass 82 fail 0; +Mc-no-run-ended (packages/bus/src/broker.mjs): bus rc 1 pass 73 fail 1; cli rc 1 pass 80 fail 2; +Md-no-instance-running (packages/cli/src/launcher.mjs): cli rc 1 pass 80 fail 2; +Me-no-authorize (packages/cli/src/launcher.mjs): cli rc 1 pass 81 fail 1; +Mf-no-exit2-wrapper (packages/harness/src/bundle.mjs): harness rc 1 pass 49 fail 1; +Mg-no-founder-check (packages/harness/src/runner.mjs): harness rc 1 pass 48 fail 2; cli rc 0 pass 82 fail 0; +Mh-no-close-sigkill (packages/cli/src/launcher.mjs): cli rc 1 pass 81 fail 1; +Mi-recover-no-kill (packages/cli/src/launcher.mjs): cli rc 1 pass 81 fail 1; +Mj-no-stdin-cap (packages/harness/src/runner.mjs): harness rc 1 pass 49 fail 1; +Mk-launch-line-max (packages/cli/src/launcher.mjs): cli rc 1 pass 81 fail 1; +Ml-gate-pattern (packages/harness/src/gate.mjs): harness rc 1 pass 49 fail 1; +Mm-no-revoke (packages/bus/src/broker.mjs): bus rc 1 pass 73 fail 1; cli rc 0 pass 82 fail 0; +Mn-recover-no-end (packages/cli/src/launcher.mjs): cli rc 1 pass 80 fail 2; +Mo-policy-outside-try (packages/harness/src/runner.mjs): harness rc 1 pass 49 fail 1; +Mp-any-reply (packages/cli/src/host.mjs): cli rc 1 pass 81 fail 1; +Mq-no-timer (packages/cli/src/host.mjs): cli rc 1 pass 81 fail 0; +Mr-ignore-unsolicited (packages/cli/src/host.mjs): cli rc 0 pass 82 fail 0; +Ms-follow-walk (packages/harness/src/gate.mjs): harness rc 1 pass 48 fail 2; +Mt-no-socket-destroy (packages/cli/src/launcher.mjs): cli rc 1 pass 81 fail 1; +Mu-no-restricted (adapters/claude/adapter.sh): harness rc 1 pass 48 fail 2; +Mv-no-tag (packages/bus/src/process.mjs): bus rc 1 pass 73 fail 1; cli rc 124 pass 69 fail 7; diff --git a/agents/darkwing/work/s6-review/r2/mut/all.sh b/agents/darkwing/work/s6-review/r2/mut/all.sh new file mode 100755 index 00000000..89ebdf0f --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/all.sh @@ -0,0 +1,26 @@ +#!/bin/bash +R=~/darkwing-scratch/r41b/mut +rm -f $R/summary.txt $R/M*-*.txt +$R/run.sh Ma-late-signals harness cli +$R/run.sh Mb-runs-set-early bus cli +$R/run.sh Mc-no-run-ended bus cli +$R/run.sh Md-no-instance-running cli +$R/run.sh Me-no-authorize cli +$R/run.sh Mf-no-exit2-wrapper harness +$R/run.sh Mg-no-founder-check harness cli +$R/run.sh Mh-no-close-sigkill cli +$R/run.sh Mi-recover-no-kill cli +$R/run.sh Mj-no-stdin-cap harness +$R/run.sh Mk-launch-line-max cli +$R/run.sh Ml-gate-pattern harness +$R/run.sh Mm-no-revoke bus cli +$R/run.sh Mn-recover-no-end cli +$R/run.sh Mo-policy-outside-try harness +$R/run.sh Mp-any-reply cli +$R/run.sh Mq-no-timer cli +$R/run.sh Mr-ignore-unsolicited cli +$R/run.sh Ms-follow-walk harness +$R/run.sh Mt-no-socket-destroy cli +$R/run.sh Mu-no-restricted harness +$R/run.sh Mv-no-tag bus cli +echo DONE >> $R/summary.txt diff --git a/agents/darkwing/work/s6-review/r2/mut/manifest-after.txt b/agents/darkwing/work/s6-review/r2/mut/manifest-after.txt new file mode 100644 index 00000000..e0d5cd2a --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/manifest-after.txt @@ -0,0 +1,42 @@ +adapters/README.md: OK +adapters/claude/adapter.sh: OK +adapters/pi/adapter.sh: OK +docs/TOOLS.md: OK +packages/bus/README.md: OK +packages/bus/src/broker.mjs: OK +packages/bus/src/process.mjs: OK +packages/bus/src/runtime.mjs: OK +packages/bus/tests/end-launch.test.mjs: OK +packages/cli/README.md: OK +packages/cli/src/cli.mjs: OK +packages/cli/src/host.mjs: OK +packages/cli/src/launcher.mjs: OK +packages/cli/tests/fixtures/launch-host.mjs: OK +packages/cli/tests/host.test.mjs: OK +packages/cli/tests/launcher.test.mjs: OK +packages/cli/tests/verbs.test.mjs: OK +packages/harness/README.md: OK +packages/harness/package.json: OK +packages/harness/src/bundle.mjs: OK +packages/harness/src/claude-gate.mjs: OK +packages/harness/src/gate.mjs: OK +packages/harness/src/mcp-server.mjs: OK +packages/harness/src/pi-extension.mjs: OK +packages/harness/src/runner.mjs: OK +packages/harness/src/tools.mjs: OK +packages/harness/tests/bundle.test.mjs: OK +packages/harness/tests/claude-gate.test.mjs: OK +packages/harness/tests/claude-session.test.mjs: OK +packages/harness/tests/fixtures/fake-adapter.mjs: OK +packages/harness/tests/gate.test.mjs: OK +packages/harness/tests/helpers.mjs: OK +packages/harness/tests/mcp-server.test.mjs: OK +packages/harness/tests/pi-session.test.mjs: OK +packages/harness/tests/runner.test.mjs: OK +packages/harness/tests/tools.test.mjs: OK +packages/seat/README.md: OK +packages/seat/src/proc.mjs: OK +packages/seat/src/session.mjs: OK +packages/seat/tests/session.test.mjs: OK +scripts/agent-host-dev.sh: OK +scripts/mosaic: OK diff --git a/agents/darkwing/work/s6-review/r2/mut/mutate.py b/agents/darkwing/work/s6-review/r2/mut/mutate.py new file mode 100644 index 00000000..d15fdece --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/mutate.py @@ -0,0 +1,62 @@ +# mutate.py : apply one named mutant (exact text, must match once). +import sys +M = { + "Ma-late-signals": ("packages/harness/src/runner.mjs", + ' process.on("SIGTERM", stop);\n process.on("SIGINT", stop);\n\n let session, cap, policy;', + ' let session, cap, policy;', ), + "Mb-runs-set-early": ("packages/bus/src/broker.mjs", + " const session = { ...record, address: record.address ?? null, human: false };\n", + " const session = { ...record, address: record.address ?? null, human: false };\n this.#runs.set(key, session);\n"), + "Mc-no-run-ended": ("packages/bus/src/broker.mjs", + " fail('run-ended');\n } else", " void 0;\n } else"), + "Md-no-instance-running": ("packages/cli/src/launcher.mjs", + ' if (registry.running(b.id, instance)) throw new Refusal("instance-running");\n', ""), + "Me-no-authorize": ("packages/cli/src/launcher.mjs", + ' await host.op({ op: "authorizeLaunch", cap, instance });\n', ""), + "Mf-no-exit2-wrapper": ("packages/harness/src/bundle.mjs", + "${quote(files.policy)} || exit 2`", "${quote(files.policy)}`"), + "Mg-no-founder-check": ("packages/harness/src/runner.mjs", + " const found = FOUNDER_ENV.filter((k) => env[k] !== undefined);", " const found = [];"), + "Mh-no-close-sigkill": ("packages/cli/src/launcher.mjs", + ' if (startTimeOf(s.pid) === s.startTime) process.kill(s.pid, "SIGKILL");', " void 0;"), + "Mi-recover-no-kill": ("packages/cli/src/launcher.mjs", + ' process.kill(s.pid, "SIGKILL");', " void 0;"), + "Mj-no-stdin-cap": ("packages/harness/src/runner.mjs", + " if (input.length > 4096) reject", " if (false) reject"), + "Mk-launch-line-max": ("packages/cli/src/launcher.mjs", + " if (buf.length > LINE_MAX) return reply", " if (false) return reply"), + "Ml-gate-pattern": ("packages/harness/src/gate.mjs", + "/(^|[/\\\\])\\.\\.([/\\\\]|$)/.test(pattern)", "false"), + "Mm-no-revoke": ("packages/bus/src/broker.mjs", + " fail('launch-revoked');", " void 0;"), + "Mn-recover-no-end": ("packages/cli/src/launcher.mjs", + ' await endRun(s.run, "host-lost", null);\n', ""), + "Mo-policy-outside-try": ("packages/harness/src/runner.mjs", + ' policy = JSON.parse(readFileSync(session.bundle.policy, "utf8"));\n } catch (e) {\n log(`runner: ${e.message}`);\n return EXIT.usage;\n }\n', + ' } catch (e) {\n log(`runner: ${e.message}`);\n return EXIT.usage;\n }\n policy = JSON.parse(readFileSync(session.bundle.policy, "utf8"));\n'), + "Mp-any-reply": ("packages/cli/src/host.mjs", + " if (pending && m?.id === pending.id) {", " if (pending) {"), + "Mq-no-timer": ("packages/cli/src/host.mjs", + " const timer = setTimeout(() => breakChannel(`no reply within ${Math.round(requestTimeoutMs / 1000)} s`), requestTimeoutMs);", + " const timer = null;"), + "Mr-ignore-unsolicited": ("packages/cli/src/host.mjs", + ' } else breakChannel(pending ? "reply for another request" : "reply with no request waiting");', + ' } else if (pending) breakChannel("reply for another request");'), + "Ms-follow-walk": ("packages/harness/src/gate.mjs", + " while (!lstatSync(head, { throwIfNoEntry: false })) {", + " while (!(() => { try { return realpathSync(head); } catch { return null; } })()) {"), + "Mt-no-socket-destroy": ("packages/cli/src/launcher.mjs", + " for (const socket of sockets) socket.destroy();", " void sockets;"), + "Mu-no-restricted": ("adapters/claude/adapter.sh", + " --restricted \\\n", ""), + "Mv-no-tag": ("packages/bus/src/process.mjs", + "const tag = (message, reply) => (Number.isSafeInteger(message?.id) ? { ...reply, id: message.id } : reply);", + "const tag = (message, reply) => reply;"), +} +f, old, new = M[sys.argv[1]] +if "--file-only" in sys.argv: print(f); sys.exit(0) +s = open(f).read() +n = s.count(old) +if n != 1: sys.exit(f"{sys.argv[1]}: {n} matches in {f}") +open(f, "w").write(s.replace(old, new)) +print(f) diff --git a/agents/darkwing/work/s6-review/r2/mut/parse.sh b/agents/darkwing/work/s6-review/r2/mut/parse.sh new file mode 100755 index 00000000..944b9ad7 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/parse.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# parse.sh: pass/fail and failing test names per mutant output (spec reporter). +cd ~/darkwing-scratch/r41b/mut +for f in M*-*.txt; do + p=$(grep -E '^ℹ pass' "$f" | awk '{print $3}'); x=$(grep -E '^ℹ fail' "$f" | awk '{print $3}') + echo "$f: pass $p fail $x" + [ "$x" != 0 ] && awk '/^✖ failing tests:/{on=1;next} on && /^✖ /{sub(/^✖ /," "); sub(/ \([0-9.]+ms\)$/,""); print}' "$f" | sort -u +done diff --git a/agents/darkwing/work/s6-review/r2/mut/run.sh b/agents/darkwing/work/s6-review/r2/mut/run.sh new file mode 100755 index 00000000..6116df2d --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# run.sh ...: mutate, run each package's node suite, restore from a backup copy. +set -u +cd ~/darkwing-scratch/r41b/wt +export TMPDIR=~/darkwing-scratch/tmp DOCKER_HOST=unix:///nonexistent.sock +m=$1; shift +f=$(python3 ../mut/mutate.py "$m" --file-only) || { echo "$m: no file" | tee -a ../mut/summary.txt; exit 1; } +cp -p "$f" ../mut/backup.tmp +python3 ../mut/mutate.py "$m" > /dev/null || { echo "$m: no match" | tee -a ../mut/summary.txt; exit 1; } +line="$m ($f):" +for p in "$@"; do + out=../mut/$m-$p.txt + timeout 900 node --test "packages/$p/tests/*.test.mjs" > "$out" 2>&1 + rc=$? + pass=$(grep -E '^ℹ pass' "$out" | awk '{print $3}'); fail=$(grep -E '^ℹ fail' "$out" | awk '{print $3}') + line="$line $p rc $rc pass ${pass:-?} fail ${fail:-?};" +done +cp -p ../mut/backup.tmp "$f" && rm ../mut/backup.tmp +echo "$line" | tee -a ../mut/summary.txt diff --git a/agents/darkwing/work/s6-review/r2/mut/summary.txt b/agents/darkwing/work/s6-review/r2/mut/summary.txt new file mode 100644 index 00000000..0d66cd12 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/mut/summary.txt @@ -0,0 +1,23 @@ +Ma-late-signals (packages/harness/src/runner.mjs): harness rc 1 pass 47 fail 3; cli rc 1 pass 80 fail 2; +Mb-runs-set-early (packages/bus/src/broker.mjs): bus rc 1 pass 73 fail 1; cli rc 0 pass 82 fail 0; +Mc-no-run-ended (packages/bus/src/broker.mjs): bus rc 1 pass 73 fail 1; cli rc 1 pass 80 fail 2; +Md-no-instance-running (packages/cli/src/launcher.mjs): cli rc 1 pass 80 fail 2; +Me-no-authorize (packages/cli/src/launcher.mjs): cli rc 1 pass 81 fail 1; +Mf-no-exit2-wrapper (packages/harness/src/bundle.mjs): harness rc 1 pass 49 fail 1; +Mg-no-founder-check (packages/harness/src/runner.mjs): harness rc 1 pass 48 fail 2; cli rc 0 pass 82 fail 0; +Mh-no-close-sigkill (packages/cli/src/launcher.mjs): cli rc 1 pass 81 fail 1; +Mi-recover-no-kill (packages/cli/src/launcher.mjs): cli rc 1 pass 81 fail 1; +Mj-no-stdin-cap (packages/harness/src/runner.mjs): harness rc 1 pass 49 fail 1; +Mk-launch-line-max (packages/cli/src/launcher.mjs): cli rc 1 pass 81 fail 1; +Ml-gate-pattern (packages/harness/src/gate.mjs): harness rc 1 pass 49 fail 1; +Mm-no-revoke (packages/bus/src/broker.mjs): bus rc 1 pass 73 fail 1; cli rc 0 pass 82 fail 0; +Mn-recover-no-end (packages/cli/src/launcher.mjs): cli rc 1 pass 80 fail 2; +Mo-policy-outside-try (packages/harness/src/runner.mjs): harness rc 1 pass 49 fail 1; +Mp-any-reply (packages/cli/src/host.mjs): cli rc 1 pass 81 fail 1; +Mq-no-timer (packages/cli/src/host.mjs): cli rc 1 pass 81 fail 0; +Mr-ignore-unsolicited (packages/cli/src/host.mjs): cli rc 0 pass 82 fail 0; +Ms-follow-walk (packages/harness/src/gate.mjs): harness rc 1 pass 48 fail 2; +Mt-no-socket-destroy (packages/cli/src/launcher.mjs): cli rc 1 pass 81 fail 1; +Mu-no-restricted (adapters/claude/adapter.sh): harness rc 1 pass 48 fail 2; +Mv-no-tag (packages/bus/src/process.mjs): bus rc 1 pass 73 fail 1; cli rc 124 pass 69 fail 7; +DONE diff --git a/agents/darkwing/work/s6-review/r2/out/gate.log b/agents/darkwing/work/s6-review/r2/out/gate.log new file mode 100644 index 00000000..e69de29b diff --git a/agents/darkwing/work/s6-review/r2/out/node-bus.txt b/agents/darkwing/work/s6-review/r2/out/node-bus.txt new file mode 100644 index 00000000..f7aaa56c --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/node-bus.txt @@ -0,0 +1,82 @@ +✔ launch identity is stamped, payload identity is refused and stale holder cannot send (153.57822ms) +✔ decision classes route from policy; gated resolution is human-only, choice and target must match (233.251054ms) +✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (232.549664ms) +✔ launch events require a human CLI capability; generic emit cannot forge authority events (142.766856ms) +✔ within-role decisions close atomically and invalid options or blocking omissions refuse (147.003548ms) +✔ observer capabilities read human inbox but cannot mutate or forge launch identity (132.873809ms) +✔ task action subjects and linked decision trail are complete and ordered (160.969646ms) +✔ launch binding is durable and reconnecting requires the identical trusted record (93.358656ms) +✔ business isolation includes inherited object names and cross-business message references (140.491985ms) +✔ authority never transfers between action, run, target, unresolved or replaced role holder (205.648152ms) +✔ task projection uses schema current view, skipping earlier and equal-start polls (107.864049ms) +✔ revocation permanently bars the old run from reclaiming first, including after broker restart (171.325451ms) +✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (103.424423ms) +✔ both arbiters require human resolution when their cross-role route is themselves (169.523241ms) +✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.083743ms) +✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.333241ms) +✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.462563ms) +✔ expiry refuses use and env references never become client data (0.737079ms) +✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.529768ms) +✔ opaque tokens shorter than 16 characters refuse before use (0.295748ms) +✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (111.484486ms) +✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (116.229163ms) +✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (148.98598ms) +✔ endLaunch leaves a claim another run took alone (148.061365ms) +✔ refuse records action.refused against the caller with the code only (113.320123ms) +✔ launches off refuses role.launch with launch-revoked until launches on (153.185093ms) +✔ broker process: launch ops authorize role.launch, record refusals and end runs (217.699001ms) +✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (1.831586ms) +✔ process reader gets own kernel identity without exposing environment values (1.438508ms) +✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.699815ms) +✔ real pid 1 remains inspectable when its environment is protected (0.281586ms) +✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (179.22726ms) +✔ missing and foreign-business citations refuse and roll back message and grant (170.311287ms) +✔ cross-role sends still need a matching resolved decision and consume it once (216.397859ms) +✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (168.892638ms) +✔ startup token refusal returns safe code without value or partial listening broker (38.749597ms) +✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (158.844199ms) +✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (160.791573ms) +✔ trusted host registers later launches; socket clients never have a registration verb (155.929935ms) +✔ runtime excludes declared project roots even when host supplies no repoRoots (36.514497ms) +✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (148.478418ms) +✔ v3b prototype refusals, views and append-only mutations (913.068827ms) +✔ gated approval authorizes once, survives store reopen, and fresh approval works (213.052692ms) +✔ another run cannot consume an approval; a failed check leaves it usable (203.386428ms) +✔ two scheduled callers have exactly one grant and one consumed refusal (142.717505ms) +✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (270.900196ms) +✔ class drift gated to cross-role refuses before consumption (175.746188ms) +✔ class drift cross-role to gated refuses before consumption (176.865975ms) +✔ class drift gated to within-role refuses before consumption (171.251501ms) +✔ class drift cross-role to within-role refuses before consumption (165.453616ms) +✔ class drift within-role to gated refuses before consumption (143.916835ms) +✔ class drift within-role to cross-role refuses before consumption (138.282316ms) +✔ message.send consumes approval and prevents a later send or authorize (167.342176ms) +✔ role.revoke consumes approval and prevents a later revoke or authorize (200.544381ms) +✔ creates private WAL store and excludes a second writer until explicit close (105.170279ms) +✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (154.965708ms) +✔ existing empty database and symlink runtime directory refuse, never initialize over damage (175.037347ms) +✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (139.630438ms) +✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (87.286236ms) +✔ async transactions refuse before invoking their function (67.714362ms) +✔ recordTask keeps sync reads and a role write apart (141.833366ms) +✔ read_at must be one canonical UTC format, so the projection compares strings safely (93.241958ms) +✔ a bad entry refuses the whole record (90.849219ms) +✔ taskView reads the projection for one business (126.126217ms) +✔ requestTask hands only a holder and a task verb to the handler, and records refusals (207.739779ms) +✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (379.996046ms) +✔ without an adapter the server refuses every task verb (169.786113ms) +✔ the runtime refuses an invalid adapter and closes a valid one (160.121287ms) +✔ the process loads the S3 adapter from plain-data trackers (220.255592ms) +✔ socket capability stamps launch identity; shared views use wire, no SQL client (121.194023ms) +✔ two wire claims serialize; a lost reply never automatically retries (145.399406ms) +✔ malformed, oversized and identity-forging envelopes refuse without echoing input (115.902591ms) +✔ client preserves UTF-8 when a response divides a multibyte character (12.084874ms) +✔ committed mutation followed by dropped reply reports unknown and is never retried (119.289433ms) +ℹ tests 74 +ℹ suites 0 +ℹ pass 74 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 2272.713014 diff --git a/agents/darkwing/work/s6-review/r2/out/node-business.txt b/agents/darkwing/work/s6-review/r2/out/node-business.txt new file mode 100644 index 00000000..1fc091e4 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/node-business.txt @@ -0,0 +1,68 @@ +✔ config directory and file path follow MOSAIC_CONFIG (1.288784ms) +✔ the fixture business validates and comes back frozen (3.843478ms) +✔ two instances may share a definition (1.311579ms) +✔ top-level refusals (3.921793ms) +✔ arbiters and projects (5.392636ms) +✔ role instances (2.746848ms) +✔ Vikunja bots (5.802152ms) +✔ a role without Vikunja takes no tracker block (2.084588ms) +✔ credential references match the definition's services (2.490105ms) +✔ launch (7.39285ms) +✔ loadBusiness: file checks (1.823287ms) +✔ loadBusiness: not a regular file (40.089512ms) +✔ loading writes nothing (1.597522ms) +✔ names that are Object.prototype properties don't count as declared (2.680279ms) +✔ the shipped example refuses as written and validates once filled in (0.565546ms) +✔ usage errors exit 4 (276.884025ms) +✔ validate: a good business exits 0 and prints instance digests (66.628426ms) +✔ validate: project files (319.917868ms) +✔ validate: missing files and a broken system config (239.090243ms) +✔ validate: credential reference problems exit 2 and name each one (68.819484ms) +✔ validate: a token file inside the repository is refused (67.972803ms) +✔ validate: role definitions come from MOSAIC_ROLES_DIR (201.938073ms) +✔ resolve: prints one instance's record (207.019929ms) +✔ resolve: refusals (393.297475ms) +✔ parse: exactly one of file or env, plus the service's date (2.376694ms) +✔ check: a good file has no problems (0.805413ms) +✔ check never opens the file: a write-only token passes (0.386944ms) +✔ check: file problems (0.820838ms) +✔ check: token files can't live in the repository or dataRoot, even through a linked directory (0.798258ms) +✔ check: dates and environment references (0.360025ms) +✔ path and load (2.210416ms) +✔ refusals (1.397312ms) +✔ systemVars flattens the validated config (2.390461ms) +✔ precedence: system, business, project, project role, agent (5.838965ms) +✔ limits narrow the definition and never widen it (2.129322ms) +✔ role.launch stays within-role only for the instance the launch block names (4.008966ms) +✔ limits.authority without role.launch leaves the launcher with no launch block (1.712887ms) +✔ limits.authority narrows cross-role actions too (0.965188ms) +✔ classify (1.013227ms) +✔ the record carries what the broker and launcher need (0.923596ms) +✔ digest: key order doesn't matter, any value change does (5.724959ms) +✔ refusals (2.138413ms) +✔ the four shipped version 2 roles load (2.771848ms) +✔ shipped role scopes match addendum B section 2 and the SR runbook (0.850429ms) +✔ shipped authority follows the note's table (0.431978ms) +✔ version 1 files keep loading with no authority (0.936277ms) +✔ the conductor policy isn't a role (0.206819ms) +✔ a missing role file is exit 4, a symbolic link too (0.347551ms) +✔ version 2 refusals (1.221265ms) +✔ authority: closed vocabulary, no gated-only action, no overlap (1.937817ms) +✔ credentials: Gitea scopes (0.799515ms) +✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.032066ms) +✔ credentials: services (0.543365ms) +✔ contract: a non-empty regular Markdown file beside the role file (0.745177ms) +✔ every key names known layers and a merge rule (0.779561ms) +✔ unknown keys and wrong layers refuse (0.649575ms) +✔ types (1.745899ms) +✔ merge: defaults, then the most specific layer wins (0.24119ms) +✔ merge: limits only narrow, and provenance lists each source (0.333722ms) +✔ merge doesn't change its inputs (0.12342ms) +ℹ tests 60 +ℹ suites 0 +ℹ pass 60 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 1906.398934 diff --git a/agents/darkwing/work/s6-review/r2/out/node-cli.txt b/agents/darkwing/work/s6-review/r2/out/node-cli.txt new file mode 100644 index 00000000..3d10f1c5 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/node-cli.txt @@ -0,0 +1,92 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (343.61827ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (133.792832ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (148.06927ms) +✔ decide prints a declining choice as declining (127.504848ms) +✔ an unknown outcome is reported once and never resent (106.107949ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (93.315085ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (93.042323ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (68.945295ms) +✔ every human command refuses inside an agent run before it touches the bus (244.089211ms) +✔ usage errors exit 4; no business and no host is a usage error (262.099651ms) +✔ agents and tasks print through the broker (131.131098ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.869193ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (52.024623ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (35.071569ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (30.399739ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (31.51699ms) +✔ a business without tracker.baseUrl gets no trackers entry (31.518112ms) +✔ an unknown business and a broken system config refuse with exit 3 (51.49228ms) +✔ empty views say so (1.159883ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.43033ms) +✔ tasks print the tracker fields the snapshot carries (0.239584ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1039.616011ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (512.790126ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (201.119259ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1179.825432ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (262.567619ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (200.710136ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (156.479626ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (103.006338ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (155.902856ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (103.353313ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (179.278505ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (21.943377ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.925198ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (216.969284ms) +✔ bus start refuses with exit 3 without a notifier config (92.624695ms) +✔ bus start runs until bus stop; status reports it while it runs (657.984685ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.925921ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1605.063976ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (669.314746ms) +✔ a runner that stops at once ends its launch with the runner's reason (196.090978ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (681.636067ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3598.000873ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (106.218367ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (125.190289ms) +✔ a launch client that never closes its side doesn't hold the host's close (134.040076ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1235.782492ms) +✔ zoned uses the IANA zone across DST (25.296472ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (352.795182ms) +✔ two blocking decisions get two DMs with different nonces (143.002964ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.170322ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (139.42675ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (104.567349ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (102.080946ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (94.413777ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (151.405656ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (274.241216ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (301.07668ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (135.172634ms) +✔ an inbox read failure is logged and the next poll retries (0.629219ms) +✔ no Discord id reaches the journal or the log (135.178436ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (19.527863ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (24.655186ms) +✔ the journal: a whole file that is one torn line truncates to empty (10.737649ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.647645ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.840409ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.865056ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.299845ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.467725ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.353944ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.425559ms) +✔ digest content stays within Discord's 2000 characters (0.273199ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.506996ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (514.094892ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (43.225726ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2218.736453ms) +✔ busExit and refuseInsideAgent (0.402628ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (423.616401ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1146.422026ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (133.213809ms) +✔ launches off and on go to the broker and change the business's launch state (161.595821ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (87.827055ms) +ℹ tests 82 +ℹ suites 0 +ℹ pass 82 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 8437.60923 diff --git a/agents/darkwing/work/s6-review/r2/out/node-harness.txt b/agents/darkwing/work/s6-review/r2/out/node-harness.txt new file mode 100644 index 00000000..28509089 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/node-harness.txt @@ -0,0 +1,58 @@ +✔ sessionModel: agent vars win, then the system's execution settings (16.351458ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (8.486143ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (4.924205ms) +✔ a bundle is written once: an existing file refuses (7.714847ms) +✔ a path with a single quote can't go into the hook command (2.914831ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (181.225521ms) +✔ a missing or wrong policy, or a bad event, exits 2 (98.543767ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1103.411534ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (1129.58691ms) +✔ claude: the hook alone blocks a path outside the workspace (591.639549ms) +✔ claude: a second turn resumes the first turn's session (766.438591ms) +✔ claude adapter: --restricted is always passed (5.009647ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (835.863042ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.995462ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (5.256191ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (1.416363ms) +✔ file tool paths must resolve inside the workspace (1.80134ms) +✔ pi's own path normalisation can't be used to step out (1.464984ms) +✔ a symlink inside the workspace that points out is outside (1.213263ms) +✔ a dangling symlink is refused at any depth, in both harnesses (4.554847ms) +✔ claude path fields per tool (1.932074ms) +✔ glob patterns stay inside the workspace (2.028059ms) +✔ a path that can't be checked is blocked (1.256848ms) +✔ initialize, ping and tools/list (66.673477ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (42.240996ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (46.54748ms) +✔ a missing argument is a usage error (51.277827ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (504.50749ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (494.672447ms) +✔ pi: a missing extension refuses before any model call (17.138359ms) +✔ pi: an extension without its configuration fails pi's start (370.307009ms) +✔ founderCheck: founder variables, then a needed service without a usable token (2.439718ms) +✔ turnRequest names the sender, class, reply and decision (0.393876ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (356.294943ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (179.594114ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (599.210351ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1476.753805ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (213.92013ms) +✔ founder credentials stop before the claim (20) (209.305788ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (250.403664ms) +✔ the launch ending under a running session exits 22 (167.505496ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (271.078035ms) +✔ a broker that is down at the claim exits 23, not 21 (101.373681ms) +✔ no capability, or a malformed one, on stdin exits 2 (216.397926ms) +✔ a missing or malformed policy exits 2 before the claim (166.370584ms) +✔ the PM gets launch, its task verbs and the reads (11.256865ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (6.134091ms) +✔ launch only when the business's launch block names the instance as launcher (4.965631ms) +✔ an action outside the instance's authority has no tool (2.448784ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (12.464112ms) +ℹ tests 50 +ℹ suites 0 +ℹ pass 50 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10983.011443 diff --git a/agents/darkwing/work/s6-review/r2/out/node-seat.txt b/agents/darkwing/work/s6-review/r2/out/node-seat.txt new file mode 100644 index 00000000..5e366e46 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/node-seat.txt @@ -0,0 +1,35 @@ +✔ resolveSeat: by name under --repo resolves the repo layout (1.258144ms) +✔ resolveSeat: by path resolves the fleet layout (0.352115ms) +✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.727948ms) +✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.671869ms) +✔ makeRegistration produces a record that validates; each shape violation throws SeatError (0.804549ms) +✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (0.84661ms) +✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (1.138897ms) +✔ CLI launch: registers, execs the fake launch script, and passes args through (28.298559ms) +✔ CLI launch: --harness lands in the record (30.253119ms) +✔ CLI launch: the launch script's own exit code passes through (30.837052ms) +✔ CLI launch: relaunching a seat rewrites the one registration record (56.802231ms) +✔ CLI launch: omitting --task records an empty string, not null (30.890374ms) +✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (81.947471ms) +✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (131.5649ms) +✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.398639ms) +✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (0.569271ms) +✔ validateRegistration/readRegistration: taskSetBy is optional; a record without it (written before #1511) still loads unchanged; an invalid one is refused; the version does not change (0.731113ms) +✔ updateTask: records setBy, preserves startedAt and every unrelated field, upgrades an old record in place only when the task is set, and replaces a previous attribution (0.84775ms) +✔ CLI seat task: --by beats MOSAIC_AGENT_NAME, the environment beats nothing, empty environment is unknown, invalid --by or environment refuses with exit 4 and leaves the record byte for byte (283.675969ms) +✔ family: exactly one launch.max key in the model name, else null (0.644531ms) +✔ sessionEnv passes only the allowlist, the repo's bin on PATH, and the run id (0.651633ms) +✔ newRun: short ids, 0700 directories, and a refusal when the socket path won't fit (1.478567ms) +✔ session file and launch log: 0600, the session file written once (0.962995ms) +✔ endReason maps the runner's exit codes; a signal is killed (0.111409ms) +✔ Registry mirrors to sessions.json; readSessions marks live entries; bad files refuse (1.123149ms) +✔ stopSession refuses an unknown run, reports a stale one, and won't signal a pid that isn't a runner (1.037453ms) +✔ a session runs under unshare as pid 1 of its namespace, claims, answers, and stops on mosaic stop (256.863792ms) +ℹ tests 27 +ℹ suites 0 +ℹ pass 27 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 736.276249 diff --git a/agents/darkwing/work/s6-review/r2/out/probe-claude-dangling.txt b/agents/darkwing/work/s6-review/r2/out/probe-claude-dangling.txt new file mode 100644 index 00000000..b8a72f06 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-claude-dangling.txt @@ -0,0 +1,4 @@ +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":true,"content":"PreToolUse:Write hook error: [timeout -k 2 10 /usr/bin/node /home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/src/claude-gate.mjs /home/jwoltje/darkwing-scratch/tmp/r41-cdangle-Sq7M/bundle/policy.json || exit 2]: mosaic gate: Write path goes through a dangling symlink: /home/jwoltje/darkwing-scratch/tmp/r41-cdangle-Sq7M/ws/notes.md\n"}] +outside file: absent diff --git a/agents/darkwing/work/s6-review/r2/out/probe-claude-memory.txt b/agents/darkwing/work/s6-review/r2/out/probe-claude-memory.txt new file mode 100644 index 00000000..64f9ba98 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-claude-memory.txt @@ -0,0 +1,14 @@ +variant candidate: exit 0; stdout: mock answer; stderr: +requests: 1; POST /v1/messages: 1 + MARKER-USER-CLAUDE-MD 0 request(s) + MARKER-PARENT-CLAUDE-MD 0 request(s) + MARKER-WS-CLAUDE-MD 0 request(s) + MARKER-AUTOMEMORY 0 request(s) + the generated prompt 1 request(s) +variant no-restricted: exit 0; stdout: mock answer; stderr: +requests: 1; POST /v1/messages: 1 + MARKER-USER-CLAUDE-MD 1 request(s) + MARKER-PARENT-CLAUDE-MD 1 request(s) + MARKER-WS-CLAUDE-MD 1 request(s) + MARKER-AUTOMEMORY 1 request(s) + the generated prompt 1 request(s) diff --git a/agents/darkwing/work/s6-review/r2/out/probe-claude-memory.txt.dirs b/agents/darkwing/work/s6-review/r2/out/probe-claude-memory.txt.dirs new file mode 100644 index 00000000..d842b185 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-claude-memory.txt.dirs @@ -0,0 +1,2 @@ +/home/jwoltje/darkwing-scratch/tmp/r41-claude-5vAv +/home/jwoltje/darkwing-scratch/tmp/r41-claude-0Nos diff --git a/agents/darkwing/work/s6-review/r2/out/probe-claude-variant.txt b/agents/darkwing/work/s6-review/r2/out/probe-claude-variant.txt new file mode 100644 index 00000000..f28356ef --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-claude-variant.txt @@ -0,0 +1,16 @@ +== MODE=plain +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":true,"content":"PreToolUse:Read hook error: [timeout -k 2 10 /usr/bin/node /home/jwoltje/darkwing-scratch/r41b/wt/packages/harness/src/claude-gate.mjs /home/jwoltje/darkwing-scratch/tmp/r41-cvariant-M3Xf/bundle/policy.json || exit 2]: mosaic gate: Read path is outside the workspace: /home/jwoltje/darkwing-scratch/tmp/r41-cvariant-M3Xf/ws/plain.txt\n"}] +== MODE=quote +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":true,"content":"File does not exist. Note: your current working directory is /home/jwoltje/darkwing-scratch/tmp/r41-cvariant-t09n/ws."}] +== MODE=ampm +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":true,"content":"Refusing to read /home/jwoltje/darkwing-scratch/tmp/r41-cvariant-KAsy/ws/shot 9.41 AM.png: its symlink resolution changed after permission was checked (it now resolves to a path the check did not see). If a link in the working directory is being rewritten concurrently, stop that and retry."}] +== MODE=nfd +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":true,"content":"File does not exist. Note: your current working directory is /home/jwoltje/darkwing-scratch/tmp/r41-cvariant-nerL/ws."}] diff --git a/agents/darkwing/work/s6-review/r2/out/probe-close-hang-line.txt b/agents/darkwing/work/s6-review/r2/out/probe-close-hang-line.txt new file mode 100644 index 00000000..c18c0506 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-close-hang-line.txt @@ -0,0 +1,6 @@ +client got {"ok":false,"error":"unauthenticated"} at 2 ms +client got FIN at 2 ms (not ending its side) +host.close(0) at 200 ms +closed 0 at 210 ms +client destroyed; closed 0 at 210 ms +exit 0 diff --git a/agents/darkwing/work/s6-review/r2/out/probe-close-hang-silent.txt b/agents/darkwing/work/s6-review/r2/out/probe-close-hang-silent.txt new file mode 100644 index 00000000..d849892b --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-close-hang-silent.txt @@ -0,0 +1,5 @@ +host.close(0) at 200 ms +client got FIN at 201 ms (not ending its side) +closed 0 at 225 ms +client destroyed; closed 0 at 225 ms +exit 0 diff --git a/agents/darkwing/work/s6-review/r2/out/probe-close-kill.txt b/agents/darkwing/work/s6-review/r2/out/probe-close-kill.txt new file mode 100644 index 00000000..42e1ed4c --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-close-kill.txt @@ -0,0 +1,8 @@ +pm run ra2fcb11efc74: session pid 1180584, runner pid 1180585, claimed +runner SIGSTOPped +host.close -> 0 after 30032 ms +session pid alive: false; runner pid alive: false +launch-log end lines: [{"run":"ra2fcb11efc74","reason":"killed","exitCode":null,"signal":"SIGKILL"}] +sessions.json after: [] +launcher: run ra2fcb11efc74 (pm) ended: killed +exit 0 diff --git a/agents/darkwing/work/s6-review/r2/out/probe-dangling-write.txt b/agents/darkwing/work/s6-review/r2/out/probe-dangling-write.txt new file mode 100644 index 00000000..27442179 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-dangling-write.txt @@ -0,0 +1,5 @@ +pi write {path: "notes.md"}: {"allow":false,"reason":"mosaic gate: write path goes through a dangling symlink: notes.md"} +claude-code Write {file_path: "notes.md"}: {"allow":false,"reason":"mosaic gate: Write path goes through a dangling symlink: /home/jwoltje/darkwing-scratch/tmp/r41-dangle-YCWI0j/ws/notes.md"} +outside file exists before: false +outside file after pi's write calls: "written through the gate\n" +exit 0 diff --git a/agents/darkwing/work/s6-review/r2/out/probe-desync.txt b/agents/darkwing/work/s6-review/r2/out/probe-desync.txt new file mode 100644 index 00000000..c9bdf8b1 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-desync.txt @@ -0,0 +1,26 @@ +baseline, no stall: + authorizeLaunch(pm, coder): {"ok":{"class":"within-role"}} + authorizeLaunch(cto, coder): {"err":"decision-required"} + identity(bogus): {"err":"unauthenticated"} +SIGSTOP broker 1172438 +host: broker channel broken (no reply within 10 s); stopping the host +p1 authorizeLaunch(pm) after 10025 ms: {"err":"broker-channel-broken"} +SIGCONT +p2 authorizeLaunch(cto), should refuse: {"err":"broker-channel-broken"} +p3 identity(bogus), should refuse: {"err":"broker-channel-broken"} +p4 identity(cto), should be cto: {"err":"broker-channel-broken"} +--- bind case: two binds queued during a stall +node:internal/process/per_thread:277 + throw new ErrnoException(err, 'kill'); + ^ + +Error: kill ESRCH + at process.kill (node:internal/process/per_thread:277:13) + at file:///home/jwoltje/darkwing-scratch/r41b/probe/desync.mjs:53:9 { + errno: -3, + code: 'ESRCH', + syscall: 'kill' +} + +Node.js v26.8.1 +exit 1 diff --git a/agents/darkwing/work/s6-review/r2/out/probe-gate-edges.txt b/agents/darkwing/work/s6-review/r2/out/probe-gate-edges.txt new file mode 100644 index 00000000..69dfc9e4 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-gate-edges.txt @@ -0,0 +1,27 @@ +pi read {"path":"sub/../../x"} deny: read path is outside the workspace: sub/../../x +pi read {"path":"@/etc/passwd"} deny: read path is outside the workspace: @/etc/passwd +pi read {"path":"~/x"} deny: read path is outside the workspace: ~/x +pi read {"path":"file:///etc/passwd"} deny: read path is outside the workspace: file:///etc/passwd +pi read {"path":" /etc/passwd"} ALLOW +pi read {"path":"etc-link/passwd"} deny: read path is outside the workspace: etc-link/passwd +pi write {"path":"dangling/x"} deny: write path goes through a dangling symlink: dangling/x +pi read {"path":"sub/./x"} ALLOW +pi grep {"path":"..","pattern":"x"} deny: grep path is outside the workspace: .. +pi find {"pattern":"{..,x}/*"} ALLOW +pi find {"pattern":"*","path":"/"} deny: find path is outside the workspace: / +pi ls {} ALLOW +pi bash {"command":"cat /etc/passwd"} deny: bash isn't in this session's policy +pi message.send {} ALLOW +claude-code Read {"file_path":"/etc/passwd"} deny: Read path is outside the workspace: /etc/passwd +claude-code Read {"file_path":"/home/jwoltje/darkwing-scratch/tmp/r41-gate-eayfBf/ws/sub/x"} ALLOW +claude-code Glob {"pattern":"/etc/*"} deny: Glob pattern must stay inside the workspace: /etc/* +claude-code Glob {"pattern":"**/x","path":"/"} deny: Glob path is outside the workspace: / +claude-code Grep {"pattern":"x","path":"/etc"} deny: Grep path is outside the workspace: /etc +claude-code Grep {"pattern":"x","glob":"../*"} ALLOW +claude-code Bash {"command":"id"} deny: Bash isn't in this session's policy +claude-code WebFetch {"url":"http://x"} deny: WebFetch isn't in this session's policy +claude-code mcp__mosaic__message.send {} ALLOW +claude-code mcp__other__message.send {} deny: mcp__other__message.send isn't in this session's policy +claude-code message.send {} deny: message.send isn't in this session's policy +claude-code Read {"file_path":7} deny: Read.file_path isn't a string +exit 0 diff --git a/agents/darkwing/work/s6-review/r2/out/probe-gate-edges2.txt b/agents/darkwing/work/s6-review/r2/out/probe-gate-edges2.txt new file mode 100644 index 00000000..ab1e7272 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-gate-edges2.txt @@ -0,0 +1,16 @@ +pi read {"path":"loop-a"} deny: read path can't be checked: ELOOP +pi write {"path":"loop-a/x"} deny: write path can't be checked: ELOOP +pi write {"path":"chain-1"} deny: write path goes through a dangling symlink: chain-1 +pi write {"path":"sub/rel-dangling"} deny: write path goes through a dangling symlink: sub/rel-dangling +pi read {"path":"up/x"} deny: read path is outside the workspace: up/x +pi write {"path":"up/ws/x"} ALLOW +pi read {"path":"link/../x"} ALLOW +pi write {"path":"deep-dangling"} deny: write path goes through a dangling symlink: deep-dangling +pi edit {"path":"chain-1"} deny: edit path goes through a dangling symlink: chain-1 +pi read {"path":"chain-1"} deny: read path goes through a dangling symlink: chain-1 +claude-code Edit {"file_path":"/chain-1"} deny: Edit path goes through a dangling symlink: /chain-1 +claude-code Read {"file_path":"/loop-b"} deny: Read path can't be checked: ELOOP +pi read {"path":"notes's.txt"} ALLOW +pi read {"path":"résumé.txt"} ALLOW +pi read {"path":"shot 9.41 AM.png"} ALLOW +exit 0 diff --git a/agents/darkwing/work/s6-review/r2/out/probe-pi-dangling.txt b/agents/darkwing/work/s6-review/r2/out/probe-pi-dangling.txt new file mode 100644 index 00000000..ae8c70ea --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-pi-dangling.txt @@ -0,0 +1,12 @@ +== MODE=dangling +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":true,"content":"mosaic gate: write path goes through a dangling symlink: notes.md"}] +turn marker: written +outside file: absent +== MODE=existing +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":true,"content":"mosaic gate: write path is outside the workspace: notes.md"}] +turn marker: written +outside file: was here before diff --git a/agents/darkwing/work/s6-review/r2/out/probe-pi-variant.txt b/agents/darkwing/work/s6-review/r2/out/probe-pi-variant.txt new file mode 100644 index 00000000..9e6b34b8 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/probe-pi-variant.txt @@ -0,0 +1,16 @@ +== MODE=plain asked p l a i n . t x t +gate decide: {"allow":false,"reason":"mosaic gate: read path is outside the workspace: plain.txt"} +exit 0; stderr: +tool_result sent back: [{"is_error":true,"content":"mosaic gate: read path is outside the workspace: plain.txt"}] +== MODE=quote asked n o t e s ' s . t x t +gate decide: {"allow":true} +exit 0; stderr: +tool_result sent back: [{"is_error":false,"content":"SECRET-OUTSIDE-THE-WORKSPACE\n"}] +== MODE=ampm asked s h o t 9 . 4 1 A M . p n g +gate decide: {"allow":true} +exit 0; stderr: +tool_result sent back: [{"is_error":false,"content":"SECRET-OUTSIDE-THE-WORKSPACE\n"}] +== MODE=nfd asked r 303 251 s u m 303 251 . t x t +gate decide: {"allow":true} +exit 0; stderr: +tool_result sent back: [{"is_error":false,"content":"SECRET-OUTSIDE-THE-WORKSPACE\n"}] diff --git a/agents/darkwing/work/s6-review/r2/out/summary.txt b/agents/darkwing/work/s6-review/r2/out/summary.txt new file mode 100644 index 00000000..9a32ed8f --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/summary.txt @@ -0,0 +1,15 @@ +node-harness exit=0 ℹ pass 50 ℹ fail 0 +node-seat exit=0 ℹ pass 27 ℹ fail 0 +node-cli exit=0 ℹ pass 82 ℹ fail 0 +node-bus exit=0 ℹ pass 74 ℹ fail 0 +node-business exit=0 ℹ pass 60 ℹ fail 0 +test-auth exit=0 selftest: 15 passed, 0 failed +test-config exit=0 selftest: 24 passed, 0 failed +test-conductor exit=0 selftest: 17 passed, 0 failed +test-queue exit=0 queue suite: 27 passed, 0 failed +test-foundation exit=0 selftest: 44 passed, 0 failed +test-extension-package exit=0 extension package selftest: 18 passed, 0 failed +test-release exit=0 selftest: 4 passed, 0 failed +test-discord exit=0 discord suite: 66 passed, 0 failed +test-task exit=1 selftest: 26 passed, 2 failed +GATE-DONE diff --git a/agents/darkwing/work/s6-review/r2/out/test-auth.txt b/agents/darkwing/work/s6-review/r2/out/test-auth.txt new file mode 100644 index 00000000..ac228760 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/test-auth.txt @@ -0,0 +1,17 @@ +OK status with missing harness credential exits 3 and still lists accounts +OK status reports harness credential (read-only) + mosaic accounts +OK api key material never reaches output +OK oauth token material never reaches output +OK unparseable credential file exits 2 +OK symlinked credential file exits 4 +OK env-side credential names reported +OK env var values never reach output +OK accounts without an accounts dir reports none and creates nothing +OK accounts lists files and marks the active one +OK loose account perms flagged in listing +OK agent --auth with missing account file refuses (exit 4) +OK agent --auth with non-0600 account file refuses +OK agent --auth with invalid account name refuses +OK auth.sh without valid config refuses + +selftest: 15 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/r2/out/test-conductor.txt b/agents/darkwing/work/s6-review/r2/out/test-conductor.txt new file mode 100644 index 00000000..1ff4f3c0 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/test-conductor.txt @@ -0,0 +1,55 @@ +Note: switching to '8dd5ff004b0ce6157446ef78523a1036ed86f42c'. + +You are in 'detached HEAD' state. You can look around, make experimental +changes and commit them, and you can discard any commits you make in this +state without impacting any branches by switching back to a branch. + +If you want to create a new branch to retain commits you create, you may +do so (now or later) by using -c with the switch command. Example: + + git switch -c + +Or undo this operation with: + + git switch - + +Turn off this advice by setting config variable advice.detachedHead to false + +Not currently on any branch. +nothing to commit, working tree clean +Note: switching to '8dd5ff004b0ce6157446ef78523a1036ed86f42c'. + +You are in 'detached HEAD' state. You can look around, make experimental +changes and commit them, and you can discard any commits you make in this +state without impacting any branches by switching back to a branch. + +If you want to create a new branch to retain commits you create, you may +do so (now or later) by using -c with the switch command. Example: + + git switch -c + +Or undo this operation with: + + git switch - + +Turn off this advice by setting config variable advice.detachedHead to false + +OK dry-run: allowed change, exit 0, nothing committed (exit 0) +OK dry-run committed nothing +OK apply: allowed change exits 0 (exit 0) +OK apply: attribution in commit subject +OK apply: target tree clean after commit +OK disallowed path refused (exit 1) +OK disallowed path: target untouched +OK syntax gate refused broken .mjs (exit 1) +OK syntax gate: target untouched +OK suite failure refused (exit 1) +OK suite failure: target reverted to clean +OK disabled policy refused (exit 2) +OK disabled policy: target untouched +OK failed run refused (exit 1) +OK failed run: target untouched +OK missing run exits 4 (exit 4) +OK invalid policy exits 2 (exit 2) + +selftest: 17 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/r2/out/test-config.txt b/agents/darkwing/work/s6-review/r2/out/test-config.txt new file mode 100644 index 00000000..76c05ef7 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/test-config.txt @@ -0,0 +1,26 @@ +OK absent adapter defaults to pi +OK adapter mock validates (exit 0) +OK unsupported adapter exits 2 (exit 2) +OK env exports adapter +OK bootstrap creates default when absent (exit 0) +OK bootstrap wrote config file +OK bootstrap is idempotent on existing config (exit 0) +OK bootstrap did not rewrite existing config +OK validate missing config exits 3 (exit 3) +OK malformed JSON exits 2 (exit 2) +OK unsupported configVersion exits 2 (exit 2) +OK unknown top-level key exits 2 (exit 2) +OK unknown execution key exits 2 (exit 2) +OK unsupported backend exits 2 (exit 2) +OK unsupported environment exits 2 (exit 2) +OK relative dataRoot exits 2 (exit 2) +OK non-canonical dataRoot exits 2 (exit 2) +OK filesystem root dataRoot exits 2 (exit 2) +OK home directory dataRoot exits 2 (exit 2) +OK dataRoot containing config dir exits 2 (exit 2) +OK control character in provider exits 2 (exit 2) +OK symlinked config file exits 2 (exit 2) +OK env exports resolve correctly +OK failed validation modified nothing + +selftest: 24 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/r2/out/test-discord.txt b/agents/darkwing/work/s6-review/r2/out/test-discord.txt new file mode 100644 index 00000000..b013f9ff --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/test-discord.txt @@ -0,0 +1,70 @@ +toolchain: node v26.8.1 + +OK syntax: packages/discord/src/approvals.mjs +OK syntax: packages/discord/src/authorize.mjs +OK syntax: packages/discord/src/binding.mjs +OK syntax: packages/discord/src/cli.mjs +OK syntax: packages/discord/src/connector.mjs +OK syntax: packages/discord/src/context.mjs +OK syntax: packages/discord/src/engine-pi.mjs +OK syntax: packages/discord/src/errors.mjs +OK syntax: packages/discord/src/gateway.mjs +OK syntax: packages/discord/src/git.mjs +OK syntax: packages/discord/src/journal.mjs +OK syntax: packages/discord/src/notify.mjs +OK syntax: packages/discord/src/rest.mjs +OK syntax: packages/discord/src/setspark.mjs +OK syntax: packages/discord/src/tools.mjs +OK syntax: packages/discord/src/web.mjs +OK syntax: packages/discord/bin/git-credential.mjs +OK syntax: packages/discord/extension/tools.mjs +OK syntax: packages/discord/tests/approvals.test.mjs +OK syntax: packages/discord/tests/authorize.test.mjs +OK syntax: packages/discord/tests/binding.test.mjs +OK syntax: packages/discord/tests/connector.test.mjs +OK syntax: packages/discord/tests/context.test.mjs +OK syntax: packages/discord/tests/engine.test.mjs +OK syntax: packages/discord/tests/fake-pi.mjs +OK syntax: packages/discord/tests/gateway.test.mjs +OK syntax: packages/discord/tests/git.test.mjs +OK syntax: packages/discord/tests/helpers.mjs +OK syntax: packages/discord/tests/journal.test.mjs +OK syntax: packages/discord/tests/notify.test.mjs +OK syntax: packages/discord/tests/recover.test.mjs +OK syntax: packages/discord/tests/rest.test.mjs +OK syntax: packages/discord/tests/setspark.test.mjs +OK syntax: packages/discord/tests/tools.test.mjs +OK syntax: packages/discord/tests/web.test.mjs +OK syntax: packages/discord/fixtures/claim-worker.mjs +OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs +OK syntax: scripts/discord.sh +OK syntax: scripts/discord-service.sh +OK packages/discord declares no dependencies +OK no bot-token-shaped string in packages/discord +OK fixture binding uses placeholder ids only +OK fixture binding validates +OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool +OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start +OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root +OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token +OK real pi with protocol vault adds reserve_id to the git verbs +OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key +OK real pi refuses a git key on a read-only root (fail closed) +OK real pi with the pilot flags (--no-tools) exposes no tool at all +OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed) +OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT +OK node --test packages/discord/tests/ (ℹ pass 178) +OK scripts/discord.sh --help exits 0 +OK scripts/discord.sh check without a binding exits 4 +OK scripts/discord.sh recover without a binding exits 4 +OK scripts/discord.sh reload without a binding exits 4 +OK scripts/discord-service.sh without a command exits 4 +OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP +OK service install writes the rendered unit (0644) and leaves no temp file +OK service install a second time reports unchanged +OK systemd-analyze verify accepts the rendered unit +OK service uninstall removes the unit file +OK service install with an unknown flag exits 4 +OK service install with USER unset finishes and names the account for lingering + +discord suite: 66 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/r2/out/test-extension-package.txt b/agents/darkwing/work/s6-review/r2/out/test-extension-package.txt new file mode 100644 index 00000000..58067949 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/test-extension-package.txt @@ -0,0 +1,21 @@ +OK initial ordinary-file install +OK installed tree matches canonical source +OK installed tree has no symlinks +OK check detects installation drift +OK sync refuses to overwrite installation drift +OK check detects an extra destination file +OK check detects an extra destination directory +OK check rejects a destination symlink +OK sync accepts a canonical source update +OK updated installation matches canonical source +scripts/test-extension-package.sh: line 14: 1092507 Killed "$@" > /dev/null 2>&1 +OK forced interruption kills the replacing process +OK next invocation recovers old consistent installation +OK interrupted replacement rolled back +OK sync succeeds after interruption recovery +OK unlocked stale lock file does not block +OK active lock refuses a concurrent sync +OK source symlink fails closed +OK nested second entrypoint fails closed + +extension package selftest: 18 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/r2/out/test-foundation.txt b/agents/darkwing/work/s6-review/r2/out/test-foundation.txt new file mode 100644 index 00000000..6338e1e6 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/test-foundation.txt @@ -0,0 +1,53 @@ +toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0 + +OK syntax: scripts/foundation-inspect.mjs +OK syntax: scripts/foundation/strict-json.mjs +OK syntax: scripts/foundation/canonical.mjs +OK syntax: scripts/foundation/resolve.mjs +OK syntax: scripts/foundation/validate-record.mjs +OK syntax: scripts/foundation/fixtures/build-fixtures.mjs +OK syntax: scripts/foundation/canonical.test.mjs +OK syntax: scripts/foundation/cli.test.mjs +OK syntax: scripts/foundation/fixtures.test.mjs +OK syntax: scripts/foundation/resolve.test.mjs +OK syntax: scripts/foundation/strict-json.test.mjs +OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written) +OK fixture generator runs +OK checked-in fixtures/bundles equal a fresh generation +OK checked-in fixtures/raw equal a fresh generation +OK checked-in fixtures/index.json equal a fresh generation +OK checked-in demo bundles equal a fresh generation +OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT +OK node --test scripts/foundation/ (ℹ pass 80) +OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof) + platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999) + node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases) + schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10 + profile column (schema-valid records only): profile-valid 510, profile-invalid 30 + profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns +OK oracle: zero schema-column disagreements with the pinned checker +OK oracle: strict-only profile refusals are counted and asserted +OK demo: permitted read preview exits 0 (exit 0) +OK demo: permitted file.change preview exits 0 (exit 0) +OK demo: assignment.change proposal is unresolved (exit 3) (exit 3) +OK demo: revoked registration is refused (exit 3) (exit 3) +OK demo: message is not authority (exit 3) (exit 3) +OK usage: no arguments exits 2 (exit 2) +OK io: missing file exits 4 (exit 4) +OK io: directory exits 4 (exit 4) +OK io: symlink exits 4 (O_NOFOLLOW) (exit 4) +OK bound: oversize fixture exits 2 (exit 2) +OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2) +OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2) +OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0) +OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed +OK text output starts with the disclaimer +OK json output is valid JSON with result allowed and exactly the charter §7 fields +OK json golden matches byte-for-byte +OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal +OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs +OK canary never printed (bundle run and credential-file run) +OK a non-bundle JSON file is refused at the shape gate, not read into output +OK no field of the non-bundle file is echoed + +selftest: 44 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/r2/out/test-queue.txt b/agents/darkwing/work/s6-review/r2/out/test-queue.txt new file mode 100644 index 00000000..c23326bb --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/test-queue.txt @@ -0,0 +1,35 @@ +toolchain: node v26.8.1, git version 2.55.0 + +OK syntax: packages/queue/src/cli.mjs +OK syntax: packages/queue/src/errors.mjs +OK syntax: packages/queue/src/io.mjs +OK syntax: packages/queue/src/lock.mjs +OK syntax: packages/queue/src/queue.mjs +OK syntax: packages/queue/src/review.mjs +OK syntax: packages/queue/src/store.mjs +OK syntax: packages/queue/tests/commit.test.mjs +OK syntax: packages/queue/tests/data.test.mjs +OK syntax: packages/queue/tests/dispatch.test.mjs +OK syntax: packages/queue/tests/helpers.mjs +OK syntax: packages/queue/tests/lock.test.mjs +OK syntax: packages/queue/tests/migration.test.mjs +OK syntax: packages/queue/tests/review.test.mjs +OK syntax: packages/queue/tests/store.test.mjs +OK syntax: packages/queue/tests/write.test.mjs +OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs +OK syntax: packages/queue/tests/fixtures/kill-at.mjs +OK syntax: packages/queue/tests/fixtures/lock-child.mjs +OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh +OK syntax: scripts/queue-commit.sh +OK syntax: scripts/git-hooks/pre-commit +OK syntax: scripts/mosaic +OK queue-commit.sh, the guard and scripts/mosaic are executable +OK packages/queue declares no dependencies +ℹ tests 148 +ℹ pass 148 +ℹ fail 0 +OK node --test packages/queue/tests/ +OK scripts/mosaic queue help +skip queue verify and render --check: this checkout (/home/jwoltje/darkwing-scratch/r41b/wt) is not the queue's canonical root (/mnt/storage/src/mosaic-stack) + +queue suite: 27 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/r2/out/test-release-rerun.txt b/agents/darkwing/work/s6-review/r2/out/test-release-rerun.txt new file mode 100644 index 00000000..7202b85d --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/test-release-rerun.txt @@ -0,0 +1,16 @@ +OK valid RELEASE resolves (exit 0) +OK invalid RELEASE exits 1 (exit 1) +OK missing RELEASE exits 1 (exit 1) +OK valid RELEASE leaves image tag consistent with version +OK status safe on empty state (exit 0) +OK status created no pointer +OK fault-injected activation refuses (exit 1) +OK refused activation wrote no pointer +OK refusal logged exactly once with valid fields +OK healthy activation succeeds (exit 0) +OK pointer written with valid fields +OK repeat activation succeeds (log grows) (exit 0) +OK log is append-only across activations +OK rollback without previous refuses (exit 1) + +selftest: 14 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/r2/out/test-release.txt b/agents/darkwing/work/s6-review/r2/out/test-release.txt new file mode 100644 index 00000000..1d6ed050 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/test-release.txt @@ -0,0 +1,7 @@ +OK valid RELEASE resolves (exit 0) +OK invalid RELEASE exits 1 (exit 1) +OK missing RELEASE exits 1 (exit 1) +OK valid RELEASE leaves image tag consistent with version +skip state-machine cases (docker daemon unavailable) + +selftest: 4 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/r2/out/test-task.txt b/agents/darkwing/work/s6-review/r2/out/test-task.txt new file mode 100644 index 00000000..85aa46f0 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/out/test-task.txt @@ -0,0 +1,33 @@ +OK valid task validates (exit 0) +OK unknown task key exits 2 (exit 2) +OK unsupported taskVersion exits 2 (exit 2) +OK invalid task id exits 2 (exit 2) +OK empty prompt exits 2 (exit 2) +OK NUL in expectExact exits 2 (exit 2) +OK out-of-range timeout exits 2 (exit 2) +OK missing mission file exits 4 (exit 4) +OK task with valid mission validates (exit 0) +OK invalid mission exits 2 (exit 2) +OK validate missing task exits 4 (exit 4) +OK validation does not modify the task file +OK prune dry-run exits 0 (exit 0) +OK dry-run deleted nothing +OK prune --keep=2 --yes removes oldest (exit 0) +OK kept exactly 2 newest runs +OK newest run kept, oldest pruned +OK append-only receipt written (3 entries) +OK sessions/workspaces untouched by prune +OK prune with invalid keep exits 4 (exit 4) +skip adapter seam cases (docker daemon unavailable) +skip workspace/capability cases (docker daemon unavailable) +skip live task cases (docker unavailable) +OK onboard without name exits 4 (non-interactive) (exit 4) +OK onboard --name renders profile (exit 0) +OK profile written +OK canon structure: required filled, optional placeholdered +OK canon sections present +FAIL user recall run succeeds (exit 1) +FAIL recalled user name (response: ) +OK no agent identity on headless run + +selftest: 26 passed, 2 failed diff --git a/agents/darkwing/work/s6-review/r2/probe/claude-dangling.sh b/agents/darkwing/work/s6-review/r2/probe/claude-dangling.sh new file mode 100755 index 00000000..444f6919 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/claude-dangling.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# The dangling-symlink write through a real Claude Code session: the +# candidate adapter, a settings.json in the bundle's form running the real +# claude-gate.mjs, --tools Write, and a mock API whose first answer calls +# Write on the link. Dummy key, 127.0.0.1 only. +set -eu +WT=$1; OUT=$2 +R=$(mktemp -d "$TMPDIR/r41-cdangle-XXXX") +mkdir -p "$R/home" "$R/ws" "$R/sess" "$R/bundle" "$R/outside" +ln -s "$R/outside/planted.txt" "$R/ws/notes.md" +echo "the generated prompt" > "$R/bundle/prompt.md" +printf '{"harness":"claude-code","workspace":"%s","tools":["write"],"typed":[]}\n' "$R/ws" > "$R/bundle/policy.json" +printf '{"hooks":{"PreToolUse":[{"matcher":"*","hooks":[{"type":"command","command":"timeout -k 2 10 %s %s %s || exit 2","timeout":20}]}]}}\n' \ + "$(command -v node)" "$WT/packages/harness/src/claude-gate.mjs" "$R/bundle/policy.json" > "$R/bundle/settings.json" +echo '{"mcpServers":{}}' > "$R/bundle/mcp.json" +TOOL_USE=$(printf '{"name":"Write","input":{"file_path":"%s","content":"written through the gate\\n"}}' "$R/ws/notes.md") \ + node "$(dirname "$0")/mock-api.mjs" "$R/api.log" > "$R/port" & MOCK=$! +while [ ! -s "$R/port" ]; do sleep 0.1; done +set +e +env -i PATH="$PATH" HOME="$R/home" USER="$USER" LANG=C.UTF-8 \ + ANTHROPIC_BASE_URL="http://127.0.0.1:$(cat "$R/port")" ANTHROPIC_API_KEY=sk-ant-dummy-not-a-key \ + DISABLE_TELEMETRY=1 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 MOSAIC_TOOLS=Write \ + MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: write notes" \ + MOSAIC_WORKSPACE="$R/ws" MOSAIC_SESSION_DIR="$R/sess" MOSAIC_MODEL=claude-sonnet-5-5 \ + MOSAIC_CLAUDE_SETTINGS="$R/bundle/settings.json" MOSAIC_CLAUDE_MCP_CONFIG="$R/bundle/mcp.json" \ + timeout 90 /bin/sh "$WT/adapters/claude/adapter.sh" < /dev/null > "$R/stdout" 2> "$R/stderr" +echo "exit $?" > "$R/exit" +kill $MOCK +{ + echo "$(cat "$R/exit"); stdout: $(head -c 200 "$R/stdout"); stderr: $(head -c 300 "$R/stderr")" + echo "POST /v1/messages: $(grep -c '"url":"/v1/messages' "$R/api.log")" + echo "tool_result sent back: $(node -e ' + const ls=require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n").map(l=>JSON.parse(l)).filter(l=>l.url.startsWith("/v1/messages")); + const b=JSON.parse(ls.at(-1).body); const r=b.messages.flatMap(m=>Array.isArray(m.content)?m.content:[]).filter(c=>c.type==="tool_result"); + console.log(JSON.stringify(r.map(c=>({is_error:c.is_error??false,content:c.content}))));' "$R/api.log")" + echo "outside file: $( [ -e "$R/outside/planted.txt" ] && cat "$R/outside/planted.txt" || echo absent)" +} | tee -a "$OUT" diff --git a/agents/darkwing/work/s6-review/r2/probe/claude-memory.sh b/agents/darkwing/work/s6-review/r2/probe/claude-memory.sh new file mode 100755 index 00000000..bb3d2743 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/claude-memory.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Does a Claude Code session started by adapters/claude/adapter.sh load +# CLAUDE.md files (fake HOME, parent and workspace) and auto-memory? +# Runs the host's claude against the mock API with a dummy key. +set -eu +WT=$1; OUT=$2; VARIANT=$3 # VARIANT: candidate | no-restricted +R=$(mktemp -d "$TMPDIR/r41-claude-XXXX") +mkdir -p "$R/home/.claude" "$R/work/ws" "$R/sess" "$R/bundle" +echo "MARKER-USER-CLAUDE-MD" > "$R/home/.claude/CLAUDE.md" +echo "MARKER-PARENT-CLAUDE-MD" > "$R/work/CLAUDE.md" +echo "MARKER-WS-CLAUDE-MD" > "$R/work/ws/CLAUDE.md" +SLUG=$(printf '%s' "$R/work/ws" | sed 's|[/.]|-|g') +mkdir -p "$R/home/.claude/projects/$SLUG/memory" +echo "MARKER-AUTOMEMORY" > "$R/home/.claude/projects/$SLUG/memory/MEMORY.md" +echo "the generated prompt" > "$R/bundle/prompt.md" +echo '{}' > "$R/bundle/settings.json" +echo '{"mcpServers":{}}' > "$R/bundle/mcp.json" +cp "$WT/adapters/claude/adapter.sh" "$R/adapter.sh" +[ "$VARIANT" = candidate ] || sed -i '/--restricted \\/d' "$R/adapter.sh" +node "$(dirname "$0")/mock-api.mjs" "$R/api.log" > "$R/port" & MOCK=$! +while [ ! -s "$R/port" ]; do sleep 0.1; done +set +e +env -i PATH="$PATH" HOME="$R/home" USER="$USER" LANG=C.UTF-8 \ + ANTHROPIC_BASE_URL="http://127.0.0.1:$(cat "$R/port")" ANTHROPIC_API_KEY=sk-ant-dummy-not-a-key \ + DISABLE_TELEMETRY=1 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ + MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: hi" \ + MOSAIC_WORKSPACE="$R/work/ws" MOSAIC_SESSION_DIR="$R/sess" MOSAIC_MODEL=claude-sonnet-5-5 \ + MOSAIC_CLAUDE_SETTINGS="$R/bundle/settings.json" MOSAIC_CLAUDE_MCP_CONFIG="$R/bundle/mcp.json" \ + timeout 60 /bin/sh "$R/adapter.sh" > "$R/stdout" 2> "$R/stderr" +echo "exit $?" > "$R/exit" +kill $MOCK +{ + echo "variant $VARIANT: $(cat "$R/exit"); stdout: $(head -c 200 "$R/stdout"); stderr: $(head -c 300 "$R/stderr")" + echo "requests: $(wc -l < "$R/api.log"); POST /v1/messages: $(grep -c '"url":"/v1/messages' "$R/api.log")" + for m in MARKER-USER-CLAUDE-MD MARKER-PARENT-CLAUDE-MD MARKER-WS-CLAUDE-MD MARKER-AUTOMEMORY "the generated prompt"; do + printf ' %-26s %s\n' "$m" "$(grep -c "$m" "$R/api.log" || true) request(s)" + done +} | tee -a "$OUT" +echo "$R" >> "$OUT.dirs" diff --git a/agents/darkwing/work/s6-review/r2/probe/claude-variant.sh b/agents/darkwing/work/s6-review/r2/probe/claude-variant.sh new file mode 100644 index 00000000..be1f7293 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/claude-variant.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# Pi's read-variant probe (pi-variant.sh) through a real Claude Code session: +# candidate adapter, a settings.json in the bundle's form running the real +# claude-gate.mjs, --tools Read, and a mock API whose first answer calls +# Read on the name Pi would not find as given. Dummy key, 127.0.0.1 only. +set -eu +WT=$1; OUT=$2 +R=$(mktemp -d "$TMPDIR/r41-cvariant-XXXX") +mkdir -p "$R/home" "$R/ws" "$R/sess" "$R/bundle" "$R/outside" +echo "SECRET-OUTSIDE-THE-WORKSPACE" > "$R/outside/secret.txt" +ASKED=$(node -e 'console.log({quote:"notes\x27s.txt",ampm:"shot 9.41 AM.png",nfd:"r\u00e9sum\u00e9.txt",plain:"plain.txt"}[process.argv[1]])' "$MODE") +DISK=$(node -e 'console.log({quote:"notes\u2019s.txt",ampm:"shot 9.41\u202fAM.png",nfd:"r\u00e9sum\u00e9.txt".normalize("NFD"),plain:"plain.txt"}[process.argv[1]])' "$MODE") +ln -s "$R/outside/secret.txt" "$R/ws/$DISK" +echo "== MODE=$MODE" | tee -a "$OUT" +echo "the generated prompt" > "$R/bundle/prompt.md" +printf '{"harness":"claude-code","workspace":"%s","tools":["read"],"typed":[]}\n' "$R/ws" > "$R/bundle/policy.json" +printf '{"hooks":{"PreToolUse":[{"matcher":"*","hooks":[{"type":"command","command":"timeout -k 2 10 %s %s %s || exit 2","timeout":20}]}]}}\n' \ + "$(command -v node)" "$WT/packages/harness/src/claude-gate.mjs" "$R/bundle/policy.json" > "$R/bundle/settings.json" +echo '{"mcpServers":{}}' > "$R/bundle/mcp.json" +TOOL_USE=$(node -e 'console.log(JSON.stringify({name:"Read",input:{file_path:process.argv[1]}}))' "$R/ws/$ASKED") \ + node "$(dirname "$0")/mock-api.mjs" "$R/api.log" > "$R/port" & MOCK=$! +while [ ! -s "$R/port" ]; do sleep 0.1; done +set +e +env -i PATH="$PATH" HOME="$R/home" USER="$USER" LANG=C.UTF-8 \ + ANTHROPIC_BASE_URL="http://127.0.0.1:$(cat "$R/port")" ANTHROPIC_API_KEY=sk-ant-dummy-not-a-key \ + DISABLE_TELEMETRY=1 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 MOSAIC_TOOLS=Read \ + MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: read it" \ + MOSAIC_WORKSPACE="$R/ws" MOSAIC_SESSION_DIR="$R/sess" MOSAIC_MODEL=claude-sonnet-5-5 \ + MOSAIC_CLAUDE_SETTINGS="$R/bundle/settings.json" MOSAIC_CLAUDE_MCP_CONFIG="$R/bundle/mcp.json" \ + timeout 90 /bin/sh "$WT/adapters/claude/adapter.sh" < /dev/null > "$R/stdout" 2> "$R/stderr" +echo "exit $?" > "$R/exit" +kill $MOCK +{ + echo "$(cat "$R/exit"); stdout: $(head -c 200 "$R/stdout"); stderr: $(head -c 300 "$R/stderr")" + echo "POST /v1/messages: $(grep -c '"url":"/v1/messages' "$R/api.log")" + echo "tool_result sent back: $(node -e ' + const ls=require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n").map(l=>JSON.parse(l)).filter(l=>l.url.startsWith("/v1/messages")); + const b=JSON.parse(ls.at(-1).body); const r=b.messages.flatMap(m=>Array.isArray(m.content)?m.content:[]).filter(c=>c.type==="tool_result"); + console.log(JSON.stringify(r.map(c=>({is_error:c.is_error??false,content:c.content}))));' "$R/api.log")" +} | tee -a "$OUT" +rm -rf "$R" diff --git a/agents/darkwing/work/s6-review/r2/probe/close-hang.mjs b/agents/darkwing/work/s6-review/r2/probe/close-hang.mjs new file mode 100644 index 00000000..2da4a8f5 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/close-hang.mjs @@ -0,0 +1,37 @@ +// Does launcher.close() (a beforeClose hook) wait on a launch.sock client +// that never closes its side? MODE=silent sends nothing; MODE=line sends a +// request line and keeps the socket open after the reply. +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { connect } from "node:net"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +const WT = process.env.WT, MODE = process.env.MODE ?? "silent"; +const { bootConfig, loadSystem } = await import(`${WT}/packages/cli/src/config.mjs`); +const { startHost } = await import(`${WT}/packages/cli/src/host.mjs`); +const { createLauncher } = await import(`${WT}/packages/cli/src/launcher.mjs`); +const { launchSocketPath } = await import(`${WT}/packages/seat/src/session.mjs`); +const { fixture } = await import(`${WT}/packages/cli/tests/helpers.mjs`); +const root = mkdtempSync(join(tmpdir(), "r41-close-")); +const f = fixture(root, "acme", (doc) => ((doc.launch = { by: "pm", instances: ["coder"], max: { opus: 1, sonnet: 1 } }), doc)); +mkdirSync(f.dataRoot, { mode: 0o700 }); +const adapter = join(root, "adapter.sh"); +writeFileSync(adapter, `exec '${process.execPath}' '${WT}/packages/harness/tests/fixtures/fake-adapter.mjs'\n`); +const system = loadSystem({ env: f.env }); +const host = await startHost({ boot: bootConfig({ system, businessId: "acme", env: f.env }), business: "acme", log: (l) => console.log("host:", l) }); +const launcher = createLauncher({ host, system, env: f.env, adapters: { pi: adapter, "claude-code": adapter }, sessionDefaults: { pollInterval: 100 }, versions: { pi: "0.85.1", claude: null }, log: (l) => console.log("launcher:", l) }); +await launcher.listen(); +const s = connect(launchSocketPath(f.dataRoot)); +s.allowHalfOpen = true; +await new Promise((r) => s.on("connect", r)); +s.on("data", (b) => console.log(`client got ${b.toString().trim()} at ${Date.now() - t0} ms`)); +s.on("end", () => console.log(`client got FIN at ${Date.now() - t0} ms (not ending its side)`)); +const t0 = Date.now(); +if (MODE === "line") s.write(`{"cap":"${"0".repeat(64)}","instance":"coder"}\n`); +await new Promise((r) => setTimeout(r, 200)); +console.log(`host.close(0) at ${Date.now() - t0} ms`); +const closing = host.close(0).then((c) => `closed ${c}`); +const limit = new Promise((r) => setTimeout(() => r("still not closed"), Number(process.env.LIMIT ?? 25000))); +console.log(`${await Promise.race([closing, limit])} at ${Date.now() - t0} ms`); +s.destroy(); +console.log(`client destroyed; ${await closing} at ${Date.now() - t0} ms`); +rmSync(root, { recursive: true, force: true }); diff --git a/agents/darkwing/work/s6-review/r2/probe/close-kill.mjs b/agents/darkwing/work/s6-review/r2/probe/close-kill.mjs new file mode 100644 index 00000000..b806e8e4 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/close-kill.mjs @@ -0,0 +1,43 @@ +// Does launcher.close() SIGKILL a session whose runner never answers its +// SIGTERM? Launch the PM, SIGSTOP its runner from outside the namespace (a +// stopped process handles no signal but SIGKILL), close the host, and time +// it. Imports from the review worktree by absolute path. +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +const WT = process.env.WT; +const { bootConfig, loadSystem } = await import(`${WT}/packages/cli/src/config.mjs`); +const { startHost, startTimeOf } = await import(`${WT}/packages/cli/src/host.mjs`); +const { createLauncher } = await import(`${WT}/packages/cli/src/launcher.mjs`); +const { readSessions, launchLogFile } = await import(`${WT}/packages/seat/src/session.mjs`); +const { fixture } = await import(`${WT}/packages/cli/tests/helpers.mjs`); +const root = mkdtempSync(join(tmpdir(), "r41-kill-")); +const f = fixture(root, "acme", (doc) => { + doc.roles.coder.vars.model = "claude-sonnet-5-5"; + doc.launch = { by: "pm", instances: ["coder"], max: { opus: 1, sonnet: 1 } }; + return doc; +}); +mkdirSync(f.dataRoot, { mode: 0o700 }); +const adapter = join(root, "adapter.sh"); +writeFileSync(adapter, `exec '${process.execPath}' '${WT}/packages/harness/tests/fixtures/fake-adapter.mjs'\n`); +const system = loadSystem({ env: f.env }); +const logs = []; +const host = await startHost({ boot: bootConfig({ system, businessId: "acme", env: f.env }), business: "acme", log: (l) => logs.push(`host: ${l}`) }); +const launcher = createLauncher({ host, system, env: f.env, adapters: { pi: adapter, "claude-code": adapter }, sessionDefaults: { pollInterval: 100 }, versions: { pi: "0.85.1", claude: null }, log: (l) => logs.push(`launcher: ${l}`) }); +await launcher.listen(); +const pm = await launcher.launchPm(); +const [s] = readSessions(f.dataRoot); +const runnerLog = join(f.dataRoot, "launches", "acme", pm.run, "runner.log"); +for (let i = 0; i < 300 && !readFileSync(runnerLog, "utf8").includes("claimed by run"); i++) await new Promise((r) => setTimeout(r, 50)); +console.log(`pm run ${pm.run}: session pid ${s.pid}, runner pid ${s.runnerPid}, claimed`); +process.kill(s.runnerPid, "SIGSTOP"); +console.log("runner SIGSTOPped"); +const t0 = Date.now(); +const code = await host.close(0); +console.log(`host.close -> ${code} after ${Date.now() - t0} ms`); +console.log(`session pid alive: ${startTimeOf(s.pid) === s.startTime}; runner pid alive: ${startTimeOf(s.runnerPid) === s.runnerStartTime}`); +const ends = readFileSync(launchLogFile(f.dataRoot, "acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l)).filter((e) => e.event === "end"); +console.log("launch-log end lines:", JSON.stringify(ends.map((e) => ({ run: e.run, reason: e.reason, exitCode: e.exitCode, signal: e.signal })))); +console.log("sessions.json after:", JSON.stringify(readSessions(f.dataRoot))); +console.log(logs.filter((l) => /ended|run /.test(l)).join("\n")); +rmSync(root, { recursive: true, force: true }); diff --git a/agents/darkwing/work/s6-review/r2/probe/dangling-write.mjs b/agents/darkwing/work/s6-review/r2/probe/dangling-write.mjs new file mode 100644 index 00000000..766d12d0 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/dangling-write.mjs @@ -0,0 +1,27 @@ +// A dangling symlink in the workspace that points outside it. decide() walks +// up to the nearest existing ancestor with existsSync, which follows links, +// so the link itself counts as "missing" and the path passes. Then the same +// two calls pi's write tool makes (core/tools/write.js:45-48: mkdir of the +// dirname, recursive, then writeFile) create the file outside. +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync } from "node:fs"; +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { tmpdir } from "node:os"; +const { decide } = await import(`${process.env.WT}/packages/harness/src/gate.mjs`); +const root = mkdtempSync(join(tmpdir(), "r41-dangle-")); +const ws = join(root, "ws"); +const outside = join(root, "outside", "planted.txt"); +mkdirSync(dirname(outside), { recursive: true }); +mkdirSync(ws); +symlinkSync(outside, join(ws, "notes.md")); // as a checked-out repo could carry it +for (const harness of ["pi", "claude-code"]) { + const policy = { harness, workspace: ws, tools: ["write", "edit"], typed: [] }; + const [tool, field] = harness === "pi" ? ["write", "path"] : ["Write", "file_path"]; + console.log(`${harness} ${tool} {${field}: "notes.md"}:`, JSON.stringify(decide(policy, tool, { [field]: harness === "pi" ? "notes.md" : join(ws, "notes.md") }))); +} +console.log("outside file exists before:", existsSync(outside)); +const target = join(ws, "notes.md"); +await mkdir(dirname(target), { recursive: true }); +await writeFile(target, "written through the gate\n", "utf-8"); +console.log("outside file after pi's write calls:", existsSync(outside) ? JSON.stringify(readFileSync(outside, "utf8")) : "absent"); +rmSync(root, { recursive: true, force: true }); diff --git a/agents/darkwing/work/s6-review/r2/probe/desync.mjs b/agents/darkwing/work/s6-review/r2/probe/desync.mjs new file mode 100644 index 00000000..98d3d4d7 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/desync.mjs @@ -0,0 +1,69 @@ +// R1: host->broker IPC replies carry no id. Stall the broker past the 10 s +// firstReply wait with two requests queued, resume it, and see which reply +// each request gets. Imports from the review worktree by absolute path. +import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +const WT = process.env.WT; +const { Client } = await import(`${WT}/packages/bus/src/client.mjs`); +const { bootConfig, loadSystem } = await import(`${WT}/packages/cli/src/config.mjs`); +const { startHost, startTimeOf } = await import(`${WT}/packages/cli/src/host.mjs`); +const { fixture } = await import(`${WT}/packages/cli/tests/helpers.mjs`); + +const root = mkdtempSync(join(tmpdir(), "r41-desync-")); +const f = fixture(root, "acme", (doc) => { + doc.launch = { by: "pm", instances: ["coder", "reviewer", "cto"], max: { opus: 1, sonnet: 1 } }; + return doc; +}); +mkdirSync(f.dataRoot, { mode: 0o700 }); +const system = loadSystem({ env: f.env }); +const boot = bootConfig({ system, businessId: "acme", env: f.env }); +const host = await startHost({ boot, business: "acme", log: (l) => console.log("host:", l) }); +const bind = async (role, run) => { + const b = await host.bindLaunch({ business: "acme", role, run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) }); + await new Client({ path: host.path, cap: b.cap }).call("role.claim"); + return b.cap; +}; +const pm = await bind("pm", "pm-run"); +const cto = await bind("cto", "cto-run"); +const show = (p) => p.then((v) => ({ ok: v }), (e) => ({ err: e.code ?? e.message })); + +console.log("baseline, no stall:"); +console.log(" authorizeLaunch(pm, coder):", JSON.stringify(await show(host.op({ op: "authorizeLaunch", cap: pm, instance: "coder" })))); +console.log(" authorizeLaunch(cto, coder):", JSON.stringify(await show(host.op({ op: "authorizeLaunch", cap: cto, instance: "coder" })))); +console.log(" identity(bogus):", JSON.stringify(await show(host.op({ op: "identity", cap: "bogus" })))); + +console.log(`SIGSTOP broker ${host.pids.broker}`); +process.kill(host.pids.broker, "SIGSTOP"); +const t0 = Date.now(); +const p1 = show(host.op({ op: "authorizeLaunch", cap: pm, instance: "coder" })); +const p2 = show(host.op({ op: "authorizeLaunch", cap: cto, instance: "coder" })); +const p3 = show(host.op({ op: "identity", cap: "bogus" })); +const r1 = await p1; +console.log(`p1 authorizeLaunch(pm) after ${Date.now() - t0} ms:`, JSON.stringify(r1)); +process.kill(host.pids.broker, "SIGCONT"); +console.log("SIGCONT"); +console.log("p2 authorizeLaunch(cto), should refuse:", JSON.stringify(await p2)); +console.log("p3 identity(bogus), should refuse:", JSON.stringify(await p3)); +const p4 = await show(host.op({ op: "identity", cap: cto })); +console.log("p4 identity(cto), should be cto:", JSON.stringify(p4)); + +await new Promise((r) => setTimeout(r, 500)); +console.log("--- bind case: two binds queued during a stall"); +process.kill(host.pids.broker, "SIGSTOP"); +const rec = (run) => ({ business: "acme", role: "coder", run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) }); +const b1 = show(host.bindLaunch(rec("run-x"))); +const b2 = show(host.bindLaunch({ ...rec("run-y"), role: "reviewer" })); +console.log("b1 bind run-x:", JSON.stringify(await b1)); +process.kill(host.pids.broker, "SIGCONT"); +const r2 = await b2; +console.log("b2 bind run-y (reviewer) got:", r2.ok ? JSON.stringify({ run: r2.ok.run, role: r2.ok.role }) : JSON.stringify(r2)); +if (r2.ok?.cap) { + const who = await show(new Client({ path: host.path, cap: r2.ok.cap }).call("role.claim")); + console.log("claim with b2's cap:", JSON.stringify(who)); +} +await new Promise((r) => setTimeout(r, 3000)); +console.log("after 3 s idle, identity(pm), should be pm:", JSON.stringify(await show(host.op({ op: "identity", cap: pm })))); +console.log("then identity(pm) again:", JSON.stringify(await show(host.op({ op: "identity", cap: pm })))); +await host.close(0); +rmSync(root, { recursive: true, force: true }); diff --git a/agents/darkwing/work/s6-review/r2/probe/gate-edges.mjs b/agents/darkwing/work/s6-review/r2/probe/gate-edges.mjs new file mode 100644 index 00000000..0807ab87 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/gate-edges.mjs @@ -0,0 +1,28 @@ +// decide() on path spellings that might leave the workspace. +import { mkdirSync, mkdtempSync, symlinkSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +const { decide } = await import(`${process.env.WT}/packages/harness/src/gate.mjs`); +const root = mkdtempSync(join(tmpdir(), "r41-gate-")); +const ws = join(root, "ws"); +mkdirSync(join(ws, "sub"), { recursive: true }); +symlinkSync("/etc", join(ws, "etc-link")); +symlinkSync(join(root, "not-yet"), join(ws, "dangling")); +const pi = { harness: "pi", workspace: ws, tools: ["read", "write", "grep", "find", "ls"], typed: ["message.send"] }; +const cc = { ...pi, harness: "claude-code" }; +const cases = [ + [pi, "read", { path: "sub/../../x" }], [pi, "read", { path: "@/etc/passwd" }], [pi, "read", { path: "~/x" }], + [pi, "read", { path: "file:///etc/passwd" }], [pi, "read", { path: " /etc/passwd" }], [pi, "read", { path: "etc-link/passwd" }], + [pi, "write", { path: "dangling/x" }], [pi, "read", { path: "sub/./x" }], [pi, "grep", { path: "..", pattern: "x" }], + [pi, "find", { pattern: "{..,x}/*" }], [pi, "find", { pattern: "*", path: "/" }], [pi, "ls", {}], + [pi, "bash", { command: "cat /etc/passwd" }], [pi, "message.send", {}], + [cc, "Read", { file_path: "/etc/passwd" }], [cc, "Read", { file_path: `${ws}/sub/x` }], [cc, "Glob", { pattern: "/etc/*" }], + [cc, "Glob", { pattern: "**/x", path: "/" }], [cc, "Grep", { pattern: "x", path: "/etc" }], [cc, "Grep", { pattern: "x", glob: "../*" }], + [cc, "Bash", { command: "id" }], [cc, "WebFetch", { url: "http://x" }], [cc, "mcp__mosaic__message.send", {}], + [cc, "mcp__other__message.send", {}], [cc, "message.send", {}], [cc, "Read", { file_path: 7 }], +]; +for (const [p, tool, input] of cases) { + const d = decide(p, tool, input); + console.log(`${p.harness.padEnd(11)} ${tool.padEnd(28)} ${JSON.stringify(input).padEnd(40)} ${d.allow ? "ALLOW" : "deny: " + d.reason.replace("mosaic gate: ", "")}`); +} +rmSync(root, { recursive: true, force: true }); diff --git a/agents/darkwing/work/s6-review/r2/probe/gate-edges2.mjs b/agents/darkwing/work/s6-review/r2/probe/gate-edges2.mjs new file mode 100644 index 00000000..29a26264 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/gate-edges2.mjs @@ -0,0 +1,35 @@ +// decide() on the round 2 lstat walk: loops, chains, relative and parent links, +// plus the read names Pi 0.85.1 would fall back to (R4) at the gate level. +import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +const { decide } = await import(`${process.env.WT}/packages/harness/src/gate.mjs`); +const root = mkdtempSync(join(tmpdir(), "r41-gate2-")); +const ws = join(root, "ws"); +mkdirSync(join(ws, "sub"), { recursive: true }); +writeFileSync(join(root, "secret.txt"), "outside\n"); +symlinkSync("loop-b", join(ws, "loop-a")); symlinkSync("loop-a", join(ws, "loop-b")); +symlinkSync("chain-2", join(ws, "chain-1")); symlinkSync(join(root, "nowhere"), join(ws, "chain-2")); +symlinkSync("../../missing", join(ws, "sub", "rel-dangling")); +symlinkSync("..", join(ws, "up")); +symlinkSync("sub", join(ws, "link")); +symlinkSync(join(root, "no-dir", "no-file"), join(ws, "deep-dangling")); +symlinkSync(join(root, "secret.txt"), join(ws, "notes’s.txt")); +symlinkSync(join(root, "secret.txt"), join(ws, "résumé.txt".normalize("NFD"))); +symlinkSync(join(root, "secret.txt"), join(ws, "shot 9.41 AM.png")); +const pi = { harness: "pi", workspace: ws, tools: ["read", "write", "edit"], typed: [] }; +const cc = { ...pi, harness: "claude-code" }; +const cases = [ + [pi, "read", { path: "loop-a" }], [pi, "write", { path: "loop-a/x" }], + [pi, "write", { path: "chain-1" }], [pi, "write", { path: "sub/rel-dangling" }], + [pi, "read", { path: "up/x" }], [pi, "write", { path: "up/ws/x" }], [pi, "read", { path: "link/../x" }], + [pi, "write", { path: "deep-dangling" }], [pi, "edit", { path: "chain-1" }], [pi, "read", { path: "chain-1" }], + [cc, "Edit", { file_path: join(ws, "chain-1") }], [cc, "Read", { file_path: join(ws, "loop-b") }], + [pi, "read", { path: "notes's.txt" }], [pi, "read", { path: "résumé.txt" }], [pi, "read", { path: "shot 9.41 AM.png" }], +]; +for (const [p, tool, input] of cases) { + const d = decide(p, tool, input); + const shown = JSON.stringify(input).replace(ws, ""); + console.log(`${p.harness.padEnd(11)} ${tool.padEnd(6)} ${shown.padEnd(36)} ${d.allow ? "ALLOW" : "deny: " + d.reason.replace("mosaic gate: ", "").replace(ws, "")}`); +} +rmSync(root, { recursive: true, force: true }); diff --git a/agents/darkwing/work/s6-review/r2/probe/mock-api.mjs b/agents/darkwing/work/s6-review/r2/probe/mock-api.mjs new file mode 100644 index 00000000..4ae893af --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/mock-api.mjs @@ -0,0 +1,45 @@ +// A mock Messages API on 127.0.0.1: logs each request body to LOG and +// streams back one short text answer. No real model is reached. +import { createServer } from "node:http"; +import { appendFileSync } from "node:fs"; +const LOG = process.argv[2]; +// TOOL_USE: optional JSON {name, input}; the first streamed answer calls it. +let toolUse = process.env.TOOL_USE ? JSON.parse(process.env.TOOL_USE) : null; +const sse = (res, ev, data) => res.write(`event: ${ev}\ndata: ${JSON.stringify(data)}\n\n`); +const server = createServer((req, res) => { + let body = ""; + req.on("data", (b) => (body += b)); + req.on("end", () => { + appendFileSync(LOG, `${JSON.stringify({ method: req.method, url: req.url, body })}\n`); + if (req.method !== "POST" || !req.url.startsWith("/v1/messages") || req.url.includes("count_tokens")) { + res.writeHead(200, { "content-type": "application/json" }); + return res.end(req.url.includes("count_tokens") ? '{"input_tokens":1}' : "{}"); + } + let stream = false; + try { stream = JSON.parse(body).stream === true; } catch {} + const msg = { id: "msg_mock", type: "message", role: "assistant", model: "claude-sonnet-5-5", content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 1 } }; + if (!stream) { + res.writeHead(200, { "content-type": "application/json" }); + return res.end(JSON.stringify({ ...msg, content: [{ type: "text", text: "mock answer" }], stop_reason: "end_turn" })); + } + res.writeHead(200, { "content-type": "text/event-stream" }); + sse(res, "message_start", { type: "message_start", message: msg }); + if (toolUse) { + const t = toolUse; + toolUse = null; + sse(res, "content_block_start", { type: "content_block_start", index: 0, content_block: { type: "tool_use", id: "toolu_mock1", name: t.name, input: {} } }); + sse(res, "content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "input_json_delta", partial_json: JSON.stringify(t.input) } }); + sse(res, "content_block_stop", { type: "content_block_stop", index: 0 }); + sse(res, "message_delta", { type: "message_delta", delta: { stop_reason: "tool_use", stop_sequence: null }, usage: { output_tokens: 2 } }); + sse(res, "message_stop", { type: "message_stop" }); + return res.end(); + } + sse(res, "content_block_start", { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }); + sse(res, "content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "mock answer" } }); + sse(res, "content_block_stop", { type: "content_block_stop", index: 0 }); + sse(res, "message_delta", { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: 2 } }); + sse(res, "message_stop", { type: "message_stop" }); + res.end(); + }); +}); +server.listen(0, "127.0.0.1", () => console.log(server.address().port)); diff --git a/agents/darkwing/work/s6-review/r2/probe/pi-dangling.sh b/agents/darkwing/work/s6-review/r2/probe/pi-dangling.sh new file mode 100755 index 00000000..68237276 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/pi-dangling.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# R3 through a real Pi 0.85.1 session: the candidate adapters/pi/adapter.sh, +# the candidate pi-extension.mjs (the gate) loaded with -e, --tools write, +# and a mock Messages API on 127.0.0.1 whose first answer calls write on a +# dangling symlink in the workspace. Dummy key; no real model. +set -eu +WT=$1; OUT=$2 +R=$(mktemp -d "$TMPDIR/r41-pdangle-XXXX") +mkdir -p "$R/home" "$R/agent" "$R/ws" "$R/bundle" "$R/outside" +ln -s "$R/outside/planted.txt" "$R/ws/notes.md" +# MODE=existing: the control, the link target exists, so the link isn't dangling. +if [ "${MODE:-dangling}" = existing ]; then echo "was here before" > "$R/outside/planted.txt"; fi +echo "== MODE=${MODE:-dangling}" | tee -a "$OUT" +echo "the generated prompt" > "$R/bundle/prompt.md" +printf '{"harness":"pi","workspace":"%s","tools":["write"],"typed":[]}\n' "$R/ws" > "$R/bundle/policy.json" +echo '[]' > "$R/bundle/tools.json" +TOOL_USE='{"name":"write","input":{"path":"notes.md","content":"written through the gate\n"}}' \ + node "$(dirname "$0")/mock-api.mjs" "$R/api.log" > "$R/port" & MOCK=$! +while [ ! -s "$R/port" ]; do sleep 0.1; done +printf '{"providers":{"mock":{"baseUrl":"http://127.0.0.1:%s","api":"anthropic-messages","apiKey":"dummy-not-a-key","models":[{"id":"claude-sonnet-5-5"}]}}}\n' "$(cat "$R/port")" > "$R/agent/models.json" +set +e +env -i PATH="$WT/node_modules/.bin:$PATH" HOME="$R/home" USER="$USER" LANG=C.UTF-8 \ + PI_CODING_AGENT_DIR="$R/agent" PI_PROVIDER=mock PI_MODEL=claude-sonnet-5-5 \ + MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: write notes" \ + MOSAIC_WORKSPACE="$R/ws" MOSAIC_TOOLS=write \ + MOSAIC_EXTENSIONS="$WT/packages/harness/src/pi-extension.mjs" \ + MOSAIC_POLICY_FILE="$R/bundle/policy.json" MOSAIC_TOOLS_FILE="$R/bundle/tools.json" \ + MOSAIC_TOOL_SOCKET="$R/no-socket" MOSAIC_TURN_MARKER="$R/marker" \ + timeout 90 /bin/sh "$WT/adapters/pi/adapter.sh" < /dev/null > "$R/stdout" 2> "$R/stderr" +echo "exit $?" > "$R/exit" +kill $MOCK +{ + echo "$(cat "$R/exit"); stdout: $(head -c 200 "$R/stdout"); stderr: $(head -c 300 "$R/stderr")" + echo "POST /v1/messages: $(grep -c '"url":"/v1/messages' "$R/api.log")" + echo "tool_result sent back: $(node -e ' + const ls=require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n").map(l=>JSON.parse(l)).filter(l=>l.url.startsWith("/v1/messages")); + const b=JSON.parse(ls.at(-1).body); const r=b.messages.flatMap(m=>Array.isArray(m.content)?m.content:[]).filter(c=>c.type==="tool_result"); + console.log(JSON.stringify(r.map(c=>({is_error:c.is_error??false,content:c.content}))));' "$R/api.log")" + echo "turn marker: $( [ -e "$R/marker" ] && echo written || echo absent)" + echo "outside file: $( [ -e "$R/outside/planted.txt" ] && cat "$R/outside/planted.txt" || echo absent)" +} | tee -a "$OUT" diff --git a/agents/darkwing/work/s6-review/r2/probe/pi-variant.sh b/agents/darkwing/work/s6-review/r2/probe/pi-variant.sh new file mode 100755 index 00000000..04cbafb6 --- /dev/null +++ b/agents/darkwing/work/s6-review/r2/probe/pi-variant.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# Pi's read tool (0.85.1 dist/core/tools/path-utils.js resolveReadPathAsync) +# retries a missing path with macOS spellings: a narrow no-break space +# before AM/PM, NFD, and a curly apostrophe. The gate checks only the path +# as given. A real Pi session, the candidate adapter and pi-extension.mjs, +# --tools read, and a mock Messages API on 127.0.0.1 whose first answer +# reads NAME_ASKED. The workspace holds a link NAME_ON_DISK pointing at a +# file outside. Dummy key; no real model. +set -eu +WT=$1; OUT=$2 +R=$(mktemp -d "$TMPDIR/r41-pvariant-XXXX") +mkdir -p "$R/home" "$R/agent" "$R/ws" "$R/bundle" "$R/outside" +echo "SECRET-OUTSIDE-THE-WORKSPACE" > "$R/outside/secret.txt" +ASKED=$(node -e 'console.log({quote:"notes'"'"'s.txt",ampm:"shot 9.41 AM.png",nfd:"résumé.txt",plain:"plain.txt"}[process.argv[1]])' "$MODE") +DISK=$(node -e 'console.log({quote:"notes’s.txt",ampm:"shot 9.41 AM.png",nfd:"résumé.txt".normalize("NFD"),plain:"plain.txt"}[process.argv[1]])' "$MODE") +ln -s "$R/outside/secret.txt" "$R/ws/$DISK" +echo "== MODE=$MODE asked $(printf %s "$ASKED" | od -An -c | tr -s ' ' | head -c 120)" | tee -a "$OUT" +echo "the generated prompt" > "$R/bundle/prompt.md" +printf '{"harness":"pi","workspace":"%s","tools":["read"],"typed":[]}\n' "$R/ws" > "$R/bundle/policy.json" +echo '[]' > "$R/bundle/tools.json" +TOOL_USE=$(node -e 'console.log(JSON.stringify({name:"read",input:{path:process.argv[1]}}))' "$ASKED") \ + node "$(dirname "$0")/mock-api.mjs" "$R/api.log" > "$R/port" & MOCK=$! +while [ ! -s "$R/port" ]; do sleep 0.1; done +printf '{"providers":{"mock":{"baseUrl":"http://127.0.0.1:%s","api":"anthropic-messages","apiKey":"dummy-not-a-key","models":[{"id":"claude-sonnet-5-5"}]}}}\n' "$(cat "$R/port")" > "$R/agent/models.json" +echo "gate decide: $(WT=$WT node -e ' + const { decide } = await import(process.env.WT + "/packages/harness/src/gate.mjs"); + console.log(JSON.stringify(decide({harness:"pi",workspace:process.argv[1],tools:["read"],typed:[]},"read",{path:process.argv[2]})));' --input-type=module "$R/ws" "$ASKED")" | tee -a "$OUT" +set +e +env -i PATH="$WT/node_modules/.bin:$PATH" HOME="$R/home" USER="$USER" LANG=C.UTF-8 \ + PI_CODING_AGENT_DIR="$R/agent" PI_PROVIDER=mock PI_MODEL=claude-sonnet-5-5 \ + MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: read it" \ + MOSAIC_WORKSPACE="$R/ws" MOSAIC_TOOLS=read \ + MOSAIC_EXTENSIONS="$WT/packages/harness/src/pi-extension.mjs" \ + MOSAIC_POLICY_FILE="$R/bundle/policy.json" MOSAIC_TOOLS_FILE="$R/bundle/tools.json" \ + MOSAIC_TOOL_SOCKET="$R/no-socket" MOSAIC_TURN_MARKER="$R/marker" \ + timeout 90 /bin/sh "$WT/adapters/pi/adapter.sh" < /dev/null > "$R/stdout" 2> "$R/stderr" +echo "exit $?" > "$R/exit" +kill $MOCK +{ + echo "$(cat "$R/exit"); stderr: $(head -c 300 "$R/stderr")" + echo "tool_result sent back: $(node -e ' + const ls=require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n").map(l=>JSON.parse(l)).filter(l=>l.url.startsWith("/v1/messages")); + const b=JSON.parse(ls.at(-1).body); const r=b.messages.flatMap(m=>Array.isArray(m.content)?m.content:[]).filter(c=>c.type==="tool_result"); + console.log(JSON.stringify(r.map(c=>({is_error:c.is_error??false,content:c.content}))));' "$R/api.log")" +} | tee -a "$OUT" +rm -rf "$R" diff --git a/agents/darkwing/work/s6-review/review-r2.md b/agents/darkwing/work/s6-review/review-r2.md new file mode 100644 index 00000000..7aeabaea --- /dev/null +++ b/agents/darkwing/work/s6-review/review-r2.md @@ -0,0 +1,241 @@ +# Row 41, slice 1 S6 (meta-harness and launching), round 2 review (Darkwing) + +Issue #1523, request comment 27004, queue revs 253 and 254 (`b887ee1e`). +Packet: `agents/filbert/work/s6/` at `779e9780`, base `915e00e5`, gate at +`8dd5ff00`, 42 files, +4679/−63. Candidate manifest sha256 +`fd21bdc27573f080d965da3121a390b148a3370acd8d9dfa5d34fcd59460eb02`, +`build.patch` sha256 +`f8cbf7bde355312253265d2071e5756f4b04d116bac06cfcf0a4a2c0474da942`. +Round 1: `review-r1.md`, comment 26995. + +Verdict: **changes**, comment 27010. R1, R2 and R3 are fixed, and +notes 1 to 5 are dealt with. One new finding blocks. R4: Pi's `read` tool +doesn't open the path the gate checked when that path doesn't exist. It +tries three spelling variants of the name first (a narrow no-break space +before AM/PM, NFD, a curly apostrophe). A workspace symlink with a variant +name that points outside the workspace is read through the gate. A real Pi +session returned the outside file's content in all three variants. The +same flaw was in round 1, and I missed it then. Claude Code's `Read` does +not have it. + +## Method + +- A detached worktree at `8dd5ff00`, then `git apply --index build.patch` + and `sha256sum -c candidate-manifest.sha256`: 42 OK. After the probes and + mutants I checked again: 42 OK (`r2/mut/manifest-after.txt`), and `git diff --name-only` empty. +- I read the interdiff against round 1 (`r2/interdiff.patch`, 17 files, + +415/−46): `host.mjs`, `process.mjs`, `gate.mjs`, `launcher.mjs`, + `runner.mjs`, `adapters/claude/adapter.sh`, the READMEs and every new + test. For R4 I read the pinned Pi 0.85.1's `dist/core/tools/read.js` and + `path-utils.js`, and the other path tools. +- Probes in `r2/probe/`. They import from my scratch worktree by absolute + path (`WT`), so they won't run as committed without setting it. Model + calls go to `r2/probe/mock-api.mjs`, a mock Messages API on 127.0.0.1 with + a dummy key; nothing was spent. Claude Code is 2.1.296, Pi is 0.85.1. + I reran every round 1 probe against this round and added + `pi-variant.sh`, `claude-variant.sh` and `gate-edges2.mjs`. +- Twenty-two mutants (`r2/mut/mutate.py`, `r2/mut/run.sh`): the fourteen from + round 1, updated where the code moved, and eight for the round 2 code. + Each ran the node suites of the packages it touches, and the file was + restored from a backup copy. + +Node is v26.8.1, `TMPDIR=~/darkwing-scratch/tmp`. `gate.sh` set +`DOCKER_HOST=unix:///nonexistent.sock`. + +## Suites + +| Suite | Result | +|---|---| +| harness | 50/0 | +| seat | 27/0 | +| cli | 82/0 | +| bus | 74/0 | +| business | 60/0 | +| test-auth | 15/0 | +| test-config | 24/0 | +| test-conductor | 17/0 | +| test-queue | 27/0 | +| test-foundation | 44/0 | +| test-extension-package | 18/0 | +| test-discord | 66/0 | +| test-release | 4/0 with Docker blocked; 14/0 run alone with Docker (`r2/out/test-release-rerun.txt`) | +| test-task | 26/2: "user recall run succeeds" and "recalled user name" | + +No test was skipped. The two test-task failures are the live recall, a +real Pi worker in Docker against zai. With Docker blocked they can't pass, +and with Docker they'd be a paid call I didn't make. Filbert reports the +base failing them the same way. cli and harness match Filbert's counts. + +## R4: Pi's `read` opens a name the gate never checked (blocking) + +`packages/harness/src/gate.mjs:65-70` checks the path as given. +Pi 0.85.1's `read` (`dist/core/tools/read.js:56`) resolves it with +`resolveReadPathAsync` (`dist/core/tools/path-utils.js:72-98`). When the +resolved path doesn't exist, that function tries, in order, and takes the +first that exists: + +1. ` AM.` or ` PM.` with the space replaced by U+202F; +2. the NFD form of the whole path; +3. `'` replaced by U+2019; +4. NFD plus U+2019. + +The gate sees a name that doesn't exist. `real()` walks up to the +workspace, so the path counts as inside and is allowed. Pi then opens the +variant. If the variant is a symlink that points outside the workspace, +the session reads the outside file. Write, edit, grep, find and ls use +`resolveToCwd`, which is lexical with no variants, so only `read` is +affected. + +`probe/pi-variant.sh` runs a real Pi session through the candidate +adapter and `pi-extension.mjs`, `--tools read`, with the mock API asking +for one `read`. The workspace holds a link with the variant name that +points to `outside/secret.txt` (`r2/out/probe-pi-variant.txt`): + +``` +plain asked plain.txt, link plain.txt gate: deny, "read path is outside the workspace" +quote asked notes's.txt, link notes’s.txt gate: allow; tool_result "SECRET-OUTSIDE-THE-WORKSPACE" +ampm asked "shot 9.41 AM.png", link with U+202F gate: allow; tool_result "SECRET-OUTSIDE-THE-WORKSPACE" +nfd asked NFC résumé.txt, link in NFD gate: allow; tool_result "SECRET-OUTSIDE-THE-WORKSPACE" +``` + +`probe/gate-edges2.mjs` shows the same three allows from `decide()` +alone. + +The precondition is a link already in the workspace, for example from a +cloned repository. Pi's `write` can't create one. That is the threat the +gate already tests for ("a symlink inside the workspace that points out is +outside"), and the `plain` line shows it holding for the exact name. A +prompt in the repository only has to ask for the ASCII spelling. + +Claude Code doesn't have this. `probe/claude-variant.sh`, the same setup +through the hook (`r2/out/probe-claude-variant.txt`): `quote` and `nfd` come +back "File does not exist". For `ampm`, Claude Code tries the U+202F name +itself but refuses: "its symlink resolution changed after permission was +checked". The hook still allowed the call; Claude Code's own check stopped +it. + +Fix: for Pi `read`, the gate builds the same candidate list from the +resolved path, in Pi's order, and refuses if any candidate that exists +resolves outside the workspace. Checking every existing candidate, not +only the one Pi would pick, is simpler and doesn't depend on which one +exists at open time. The variants apply to the whole path, directory +names included, so the check has to run on the full resolved path. Add a +`gate.test.mjs` case per variant and a `pi-session.test.mjs` read through +a curly-apostrophe link. This depends on Pi's code, so the pin note in the +harness README should name `path-utils.js` as something to recheck on a +Pi upgrade. + +## What holds + +- **R1, request ids.** `host.mjs:160-205`: `request()` sends one message + at a time with `id = ++seq`, and the message handler resolves only when + the reply's id matches the waiting one. Anything else breaks the + channel: a reply with another id or none, a reply with nothing waiting, + or no reply within 10 s. Once broken, the waiting request and every + later one refuse with `broker-channel-broken`, and the host closes with + exit 1. `process.mjs:15` `tag()` echoes a safe-integer id on every + launch-op reply and on the startup error. All five launch ops are + synchronous in `runtime.mjs`, so one reply per request holds. Close + hooks during a break refuse at once, and a late reply after a break + returns early. `probe/desync.mjs` (`r2/out/probe-desync.txt`): after a 10 s + SIGSTOP, p1 to p4 all refuse with `broker-channel-broken`. Round 1 gave + p2 "allowed" for a refused CTO launch. The probe's bind half then ends + in `kill ESRCH`, because the host had already stopped the broker; the + stall test in `host.test.mjs` covers the queued bind. +- **R2, half-open clients.** `launcher.mjs:118, 340-341, 381-387`: the + launcher tracks `launch.sock` connections and destroys them after + `server.close`, before it waits. `probe/close-hang.mjs`: `closed 0` 25 ms + after `host.close(0)` in `silent` mode and 10 ms after in `line` mode. + Round 1 waited until the probe gave up at 25 s. +- **R3, dangling links.** `gate.mjs:48-63`: `real()` walks up with + `lstat`, so a dangling link is an existing name, and `realpathSync` on it + gives ENOENT, refused as "goes through a dangling symlink". Rerun: + `pi-dangling.sh` refuses the write and leaves the outside file absent, + and with the target present it refuses as outside and leaves the content + alone. `claude-dangling.sh` refuses through the hook. `dangling-write.mjs` + refuses in both harnesses. `gate-edges.mjs` matches Filbert's account: + the only change from round 1 is `write dangling/x`, now refused. + `gate-edges2.mjs` adds the edges I wanted to see: a symlink loop is + refused as ELOOP, final or in the middle; a chain ending in a dangling + link, a relative dangling link and a link to a missing directory are + refused; `up -> ..` refuses `up/x` and allows `up/ws/x`, which really is + inside. `link/../x` is allowed, and that's right, because Pi resolves + `..` lexically the same way the gate does. +- **Note 1, `--restricted`.** `claude-memory.sh`: none of the four markers + reaches the request with the candidate, all four without the flag. The + README and adapter comment say what it does, and both new tests are + real. +- **Note 3, policy parse.** `runner.mjs:259-268`: the parse is inside the + `try`, exit 2 before the claim, with a test. +- **Notes 2 and 4** are README lines, and they read correctly. +- **The 30 s close.** `close-kill.mjs` gives the same result as round 1: + a SIGSTOPped runner is SIGKILLed at 30 s, the launch log says `killed`, + and `sessions.json` is empty. + +## Mutants + +`r2/mut/summary.txt` has the raw lines. + +| Mutant | Change | Result | Killed by | +|---|---|---|---| +| Ma | signal handlers installed after setup | harness 47/3, cli 80/2 | the runner's SIGTERM tests | +| Mb | broker records the run before its checks | bus 73/1, cli 82/0 | a restarted broker refuses to rebind an ended run | +| Mc | no `run-ended` refusal on rebind | bus 73/1, cli 80/2 | the ended-run rebind test, both host-died-hard tests | +| Md | no instance-running check | cli 80/2 | `bus start --pm`, the PM launches a coder | +| Me | no `authorizeLaunch` before start | cli 81/1 | the PM launches a coder; refusals name their code | +| Mf | hook command without `\|\| exit 2` | harness 49/1 | a claude-code bundle adds the wrapped gate hook | +| Mg | `founderCheck` finds no founder variables | harness 48/2, cli 82/0 | the unit test and "founder credentials stop before the claim"; no hang now | +| Mh | no SIGKILL after 30 s in `close()` | cli 81/1 | a runner that ignores SIGTERM is killed when the host closes | +| Mi | no SIGKILL in `recover()` | cli 81/1 | the host-died-hard test, which now kills leftovers | +| Mj | no 4096-byte stdin cap | harness 49/1 | no capability, or a malformed one, on stdin exits 2 | +| Mk | no `LINE_MAX` on `launch.sock` | cli 81/1 | an over-long launch request is refused at once | +| Ml | no `..` check on glob patterns | harness 49/1 | glob patterns stay inside the workspace | +| Mm | no `launch-revoked` refusal | bus 73/1, cli 82/0 | launches off refuses role.launch | +| Mn | `recover()` doesn't end the run | cli 80/2 | both host-died-hard tests | +| Mo | policy parse back outside the `try` | harness 49/1 | a missing or malformed policy exits 2 | +| Mp | host takes any reply, whatever its id | cli 81/1 | a reply with another id, or none, breaks the channel | +| Mq | no 10 s request timer | cli 81/0, 1 cancelled | the stall test times out at 30 s | +| Mr | a reply with nothing waiting is ignored | cli 82/0 | **survives** | +| Ms | `real()` walks with a link-following check | harness 48/2 | the dangling-at-any-depth test and the real Pi write | +| Mt | `close()` doesn't destroy connections | cli 81/1 | a launch client that never closes its side | +| Mu | adapter without `--restricted` | harness 48/2 | both `--restricted` tests | +| Mv | `tag()` doesn't echo the id | bus 73/1, cli 69/7 | the broker launch-ops test, both new host tests, five more; `launcher.test.mjs` hangs (below) | + +Twenty-one of 22 killed. Round 1's four survivors (Mh, Mi, Mj, Mk) are +now killed, and Mg fails instead of hanging. + +## Notes (not blocking) + +1. **Mr survives.** No test sends a reply when nothing is waiting. The + ids already stop such a reply from answering a later request: it would + arrive with the wrong id and break the channel then. So it's a second + layer, and the code is right. A test with a broker child that sends one + extra message would hold it. +2. **Under Mv, `launcher.test.mjs` hangs** instead of failing. With no id + echo, every request breaks the channel and the host exits 1, and some + test in that file then waits forever; my 900 s limit killed it. Mv is + still killed by the other tests. A `timeout` on the launcher tests, as + round 1 suggested for Mg, would turn a hang into a failure. +3. **Claude Code's `Read` has the same blind spot in the gate.** It's + Claude Code's own symlink check that stops the AM/PM case, not the + hook. It's worth giving `Read` the same variant check if it's cheap, + so the gate doesn't depend on that. +4. **A leftover after Ma.** After the Ma mutant's harness run, a + `fake-adapter.mjs` process was still running, reparented to init. I + killed it by PID. The mutant caused it. I didn't trace which test + spawned it, so I can't say whether that test cleans up when it fails. + +## Files + +Round 1's files stay where they were. Round 2's are under `r2/`. + +- `review-r2.md`: this file. +- `r2/files.txt`, `r2/candidate-manifest.sha256`: the candidate as + reviewed. +- `r2/gate.sh`, `r2/out/`: the suite runs (`summary.txt` first) and every + probe output (`probe-*.txt`). +- `r2/probe/`: the round 1 probes as rerun, plus `pi-variant.sh` and + `claude-variant.sh` (R4) and `gate-edges2.mjs`. +- `r2/mut/`: `mutate.py`, `run.sh`, `all.sh`, `summary.txt`, one output + per mutant and package, and `manifest-after.txt`. +- `r2/interdiff.patch`: round 1 against round 2.