From c38bc5466365c6a44ea0182ffa4498aac360d902 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 9 Oct 2026 22:39:59 -0500 Subject: [PATCH] docs(review): row 41 round 4 review packet, approve (darkwing) Co-Authored-By: Claude Opus 5.5 --- .../s6-review/r4/candidate-manifest.sha256 | 42 ++++ agents/darkwing/work/s6-review/r4/files.txt | 42 ++++ agents/darkwing/work/s6-review/r4/gate.sh | 14 ++ .../work/s6-review/r4/interdiff.patch | 191 ++++++++++++++++++ .../s6-review/r4/mut/MA-pi-only-harness.txt | 88 ++++++++ .../r4/mut/MB-no-real-base-harness.txt | 88 ++++++++ .../r4/mut/MC-no-nfd-curly-harness.txt | 106 ++++++++++ .../r4/mut/MD-given-only-harness.txt | 124 ++++++++++++ .../s6-review/r4/mut/ME-real-only-harness.txt | 88 ++++++++ .../s6-review/r4/mut/MF-either-harness.txt | 124 ++++++++++++ .../r4/mut/MG-inside-no-normalise-harness.txt | 88 ++++++++ .../r4/mut/Ml-gate-pattern-harness.txt | 88 ++++++++ .../r4/mut/Ms-follow-walk-harness.txt | 128 ++++++++++++ .../s6-review/r4/mut/Mw-ampm-case-harness.txt | 64 ++++++ .../r4/mut/Mx-curly-once-harness.txt | 64 ++++++ .../r4/mut/My-lstat-error-skips-harness.txt | 64 ++++++ .../r4/mut/Mz-spell-no-normalise-harness.txt | 64 ++++++ agents/darkwing/work/s6-review/r4/mut/all.sh | 9 + .../work/s6-review/r4/mut/manifest-after.txt | 42 ++++ .../darkwing/work/s6-review/r4/mut/mutate.py | 78 +++++++ agents/darkwing/work/s6-review/r4/mut/run.sh | 19 ++ .../work/s6-review/r4/mut/summary.txt | 14 ++ .../work/s6-review/r4/out/node-bus.txt | 82 ++++++++ .../work/s6-review/r4/out/node-business.txt | 68 +++++++ .../work/s6-review/r4/out/node-cli.txt | 93 +++++++++ .../work/s6-review/r4/out/node-harness.txt | 64 ++++++ .../work/s6-review/r4/out/node-seat.txt | 35 ++++ .../r4/out/probe-claude-abs-dotdot.txt | 12 ++ .../r4/out/probe-claude-cwd-dotdot.txt | 12 ++ .../s6-review/r4/out/probe-pi-abs-dotdot.txt | 12 ++ .../s6-review/r4/out/probe-pi-cwd-dotdot.txt | 12 ++ .../s6-review/r4/out/probe-pi-variant.txt | 16 ++ .../s6-review/r4/out/probe-spell-edges.txt | 56 +++++ .../work/s6-review/r4/out/summary.txt | 15 ++ .../work/s6-review/r4/out/test-auth.txt | 17 ++ .../work/s6-review/r4/out/test-conductor.txt | 55 +++++ .../work/s6-review/r4/out/test-config.txt | 26 +++ .../work/s6-review/r4/out/test-discord.txt | 70 +++++++ .../r4/out/test-extension-package.txt | 21 ++ .../work/s6-review/r4/out/test-foundation.txt | 53 +++++ .../work/s6-review/r4/out/test-queue.txt | 35 ++++ .../s6-review/r4/out/test-release-docker.txt | 16 ++ .../work/s6-review/r4/out/test-release.txt | 7 + .../work/s6-review/r4/out/test-task.txt | 33 +++ .../s6-review/r4/probe/claude-abs-dotdot.sh | 51 +++++ .../s6-review/r4/probe/claude-cwd-dotdot.sh | 46 +++++ .../work/s6-review/r4/probe/mock-api.mjs | 45 +++++ .../work/s6-review/r4/probe/pi-abs-dotdot.sh | 61 ++++++ .../work/s6-review/r4/probe/pi-cwd-dotdot.sh | 56 +++++ .../work/s6-review/r4/probe/pi-variant.sh | 46 +++++ .../work/s6-review/r4/probe/spell-edges.mjs | 51 +++++ agents/darkwing/work/s6-review/review-r4.md | 175 ++++++++++++++++ 52 files changed, 2970 insertions(+) create mode 100644 agents/darkwing/work/s6-review/r4/candidate-manifest.sha256 create mode 100644 agents/darkwing/work/s6-review/r4/files.txt create mode 100755 agents/darkwing/work/s6-review/r4/gate.sh create mode 100644 agents/darkwing/work/s6-review/r4/interdiff.patch create mode 100644 agents/darkwing/work/s6-review/r4/mut/MA-pi-only-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/MB-no-real-base-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/MC-no-nfd-curly-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/MD-given-only-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/ME-real-only-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/MF-either-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/MG-inside-no-normalise-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/Ml-gate-pattern-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/Ms-follow-walk-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/Mw-ampm-case-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/Mx-curly-once-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/My-lstat-error-skips-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/Mz-spell-no-normalise-harness.txt create mode 100755 agents/darkwing/work/s6-review/r4/mut/all.sh create mode 100644 agents/darkwing/work/s6-review/r4/mut/manifest-after.txt create mode 100644 agents/darkwing/work/s6-review/r4/mut/mutate.py create mode 100755 agents/darkwing/work/s6-review/r4/mut/run.sh create mode 100644 agents/darkwing/work/s6-review/r4/mut/summary.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/node-bus.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/node-business.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/node-cli.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/node-harness.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/node-seat.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/probe-claude-abs-dotdot.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/probe-claude-cwd-dotdot.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/probe-pi-abs-dotdot.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/probe-pi-cwd-dotdot.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/probe-pi-variant.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/probe-spell-edges.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/summary.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/test-auth.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/test-conductor.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/test-config.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/test-discord.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/test-extension-package.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/test-foundation.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/test-queue.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/test-release-docker.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/test-release.txt create mode 100644 agents/darkwing/work/s6-review/r4/out/test-task.txt create mode 100755 agents/darkwing/work/s6-review/r4/probe/claude-abs-dotdot.sh create mode 100755 agents/darkwing/work/s6-review/r4/probe/claude-cwd-dotdot.sh create mode 100644 agents/darkwing/work/s6-review/r4/probe/mock-api.mjs create mode 100755 agents/darkwing/work/s6-review/r4/probe/pi-abs-dotdot.sh create mode 100755 agents/darkwing/work/s6-review/r4/probe/pi-cwd-dotdot.sh create mode 100755 agents/darkwing/work/s6-review/r4/probe/pi-variant.sh create mode 100644 agents/darkwing/work/s6-review/r4/probe/spell-edges.mjs create mode 100644 agents/darkwing/work/s6-review/review-r4.md diff --git a/agents/darkwing/work/s6-review/r4/candidate-manifest.sha256 b/agents/darkwing/work/s6-review/r4/candidate-manifest.sha256 new file mode 100644 index 00000000..31f59600 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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 +0a2a452fdb3a4ea68478e54b3ca6694c51d074fa29a0ad8ea3740eb1b44ab780 packages/cli/tests/host.test.mjs +1dbdb8166e8c47290bb4499b330ea32bec9a6f07179eef816edaa35603b1c408 packages/cli/tests/launcher.test.mjs +709bd331bb0d76f28b464e518f4e1fc3bb0b303614e79d7e82479dcd679043cc packages/cli/tests/verbs.test.mjs +f996119e0afe972516408c8058c49d93192cabebbc48778cf9da5a9bbfa94a7c packages/harness/README.md +34ef8212e27f052223443c66830839517f7a54ecb6ef7d85795b3e89c65b4d4c packages/harness/package.json +22efd0bed7991561920ad29f6bf9a1ab2d7384185fab4267684a037e026d8e85 packages/harness/src/bundle.mjs +32b5090760c95eea0e1232ff36ec13a1d9b58335b2eade621196dec1ebdbe784 packages/harness/src/claude-gate.mjs +38219ee9ea8bc9239e6de375a79e98a431338ec51eee0b71e865c4efea620765 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 +c406566d92f79dc74f52588549303309e6d843bc8013885c33fb8e5e12d89195 packages/harness/tests/gate.test.mjs +6e7509cfe6ad909440c25b750528360c0ba617c6c68b05d400bcd94b481c1f76 packages/harness/tests/helpers.mjs +793eb11f970e4a8eecddec4dc48c24331e4de795bc04bfadf806a2fc3a15a8b4 packages/harness/tests/mcp-server.test.mjs +6d31a44a8e9835406df201e111d88fb3e6307c5993e471cce0d533153237e451 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/r4/files.txt b/agents/darkwing/work/s6-review/r4/files.txt new file mode 100644 index 00000000..67685134 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r4/gate.sh b/agents/darkwing/work/s6-review/r4/gate.sh new file mode 100755 index 00000000..6570ea98 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/gate.sh @@ -0,0 +1,14 @@ +#!/bin/bash +export TMPDIR=~/darkwing-scratch/tmp DOCKER_HOST=unix:///nonexistent.sock +cd ~/darkwing-scratch/r41d/wt +O=~/darkwing-scratch/r41d/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 DONE >> $O/summary.txt diff --git a/agents/darkwing/work/s6-review/r4/interdiff.patch b/agents/darkwing/work/s6-review/r4/interdiff.patch new file mode 100644 index 00000000..164b1d50 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/interdiff.patch @@ -0,0 +1,191 @@ +--- r3wt/packages/harness/README.md 2026-10-09 22:31:22.166070702 -0500 ++++ wt/packages/harness/README.md 2026-10-09 22:31:15.039068214 -0500 +@@ -83,6 +83,19 @@ + directories first. Pi calls it from the extension, Claude Code from + `claude-gate.mjs`. + ++A relative path is resolved the way Pi resolves it: against its working ++directory. Both adapters `cd` into the workspace, and a process's working ++directory is the real path, so `..` climbs the real path's parents. With ++the workspace or the dataRoot behind a symlink, those differ from the ++parents of the path as given, and `../../data/ws/x` can be inside as given ++but outside for Pi. The gate requires a relative path to be inside from ++both the real path and the path as given. The second check is stricter ++than Pi: a path that only climbs out and back in through the real path's ++parents is refused. That keeps the gate fail-closed whichever directory it ++runs in. Claude Code isn't affected the same way: it makes a path absolute ++against its own (real) working directory before the `PreToolUse` hook ++sees it, so the gate gets the path Claude Code will open. ++ + Pi's `read` doesn't always open the name it is given. When that name + doesn't exist, it tries other spellings of the whole resolved path: a + narrow no-break space (U+202F) before ` AM.` or ` PM.`, the NFD form, a +--- r3wt/packages/harness/src/gate.mjs 2026-10-09 22:31:22.166883857 -0500 ++++ wt/packages/harness/src/gate.mjs 2026-10-09 22:31:15.039927044 -0500 +@@ -68,9 +68,15 @@ + return target === root || target.startsWith(root + sep); + } + ++// A relative path is resolved the way the tool resolves it: against its ++// cwd. Both adapters cd into the workspace, and a process's cwd is the real ++// path, so `..` climbs the real path's parents, not those of a workspace or ++// dataRoot given through a symlink. It must be inside against the path as ++// given too, so the check doesn't rest on how the harness was started. + export function insideWorkspace(workspace, p) { + const s = normalise(p); +- return within(workspace, isAbsolute(s) ? resolve(s) : resolve(workspace, s)); ++ if (isAbsolute(s)) return within(workspace, resolve(s)); ++ return within(workspace, resolve(realpathSync(workspace), s)) && within(workspace, resolve(workspace, s)); + } + + // Pi's read (dist/core/tools/path-utils.js resolveReadPathAsync) opens +--- r3wt/packages/harness/tests/gate.test.mjs 2026-10-09 22:31:22.167166446 -0500 ++++ wt/packages/harness/tests/gate.test.mjs 2026-10-09 22:31:15.040230821 -0500 +@@ -1,8 +1,8 @@ + import { test } from "node:test"; + import assert from "node:assert/strict"; +-import { mkdirSync, symlinkSync, writeFileSync } from "node:fs"; ++import { mkdirSync, realpathSync, symlinkSync, writeFileSync } from "node:fs"; + import { homedir } from "node:os"; +-import { join } from "node:path"; ++import { join, resolve } from "node:path"; + import { pathToFileURL } from "node:url"; + import { claudeBuiltins, decide, insideWorkspace } from "../src/gate.mjs"; + import { scratch } from "./helpers.mjs"; +@@ -158,6 +158,53 @@ + blocked(decide(viaLink, "read", { path: "x.txt" }), /outside the workspace under another spelling/); + }); + ++// Both adapters cd into the workspace, and the tool's cwd is its real path, ++// so `..` climbs the real path's parents. Through a symlinked workspace or ++// dataRoot those differ from the given path's. ++const CLIMBS = { ++ // /a/ws -> /deep/store/ws: up two is as given, /deep for pi. ++ workspace(dir) { ++ mkdirSync(join(dir, "deep", "store", "ws"), { recursive: true }); ++ mkdirSync(join(dir, "a")); ++ symlinkSync(join(dir, "deep", "store", "ws"), join(dir, "a", "ws")); ++ return { workspace: join(dir, "a", "ws"), outside: join(dir, "deep", "a", "ws"), escape: "../../a/ws", stay: "../ws", strict: "../../store/ws" }; ++ }, ++ // dataRoot /data -> /deep/store, the workspace under it as the ++ // launcher builds it: up four is as given, /deep for pi. ++ dataRoot(dir) { ++ mkdirSync(join(dir, "deep", "store", "workspaces", "b", "i"), { recursive: true }); ++ symlinkSync(join(dir, "deep", "store"), join(dir, "data")); ++ return { workspace: join(dir, "data", "workspaces", "b", "i"), outside: join(dir, "deep", "data", "workspaces", "b", "i"), escape: "../../../../data/workspaces/b/i", stay: "../i", strict: "../../../../store/workspaces/b/i" }; ++ }, ++}; ++ ++test("a relative path climbs from the workspace's real path, in both harnesses", (t) => { ++ for (const [name, build] of Object.entries(CLIMBS)) { ++ for (const harness of ["pi", "claude-code"]) { ++ const dir = scratch(t); ++ const { workspace, outside, escape, stay, strict } = build(dir); ++ writeFileSync(join(workspace, "a.txt"), "a"); ++ mkdirSync(outside, { recursive: true }); ++ writeFileSync(join(outside, "secret.txt"), "s"); ++ const policy = { harness, workspace, tools: ["read", "write"], typed: [] }; ++ const [read, write, field] = harness === "pi" ? ["read", "write", "path"] : ["Read", "Write", "file_path"]; ++ const at = `${name}, ${harness}`; ++ // As given, the escape is inside; from the real path it is outside. ++ assert.equal(resolve(workspace, escape), workspace, at); ++ assert.equal(resolve(realpathSync(workspace), escape), outside, at); ++ blocked(decide(policy, read, { [field]: `${escape}/secret.txt` }), /outside the workspace/); ++ blocked(decide(policy, write, { [field]: `${escape}/planted.txt` }), /outside the workspace/); ++ // Climbing out and back in by the same name stays inside on both paths. ++ allowed(decide(policy, read, { [field]: `${stay}/a.txt` })); ++ allowed(decide(policy, write, { [field]: `${stay}/new.txt` })); ++ // The other way round, inside for pi but outside as given, is refused ++ // too: the gate doesn't rest on the cwd the harness started in. ++ assert.equal(resolve(realpathSync(workspace), strict), realpathSync(workspace), at); ++ blocked(decide(policy, read, { [field]: `${strict}/a.txt` }), /outside the workspace/); ++ } ++ } ++}); ++ + 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") })); +--- r3wt/packages/harness/tests/pi-session.test.mjs 2026-10-09 22:31:22.167166446 -0500 ++++ wt/packages/harness/tests/pi-session.test.mjs 2026-10-09 22:31:15.040230821 -0500 +@@ -12,11 +12,11 @@ + + const ADAPTER = join(REPO, "adapters", "pi", "adapter.sh"); + +-async function session(t, script) { ++async function session(t, script, place = (dir) => join(dir, "ws")) { + const dir = scratch(t); +- const workspace = join(dir, "ws"); ++ const workspace = place(dir); + const agentDir = join(dir, "pi-agent"); +- mkdirSync(workspace); ++ mkdirSync(workspace, { recursive: true }); + mkdirSync(agentDir); + writeFileSync(join(workspace, "notes.txt"), "inside\n"); + writeFileSync(join(dir, "secret.txt"), "outside\n"); +@@ -153,6 +153,65 @@ + assert.ok(existsSync(s.turnMarker)); + }); + ++// The adapter cds into the workspace, and pi resolves `..` from that real ++// path. Each layout: where the workspace is given, and where `escape` lands ++// for pi (as given, it is the workspace itself). ++const CLIMBS = { ++ workspace: { ++ place(dir) { ++ mkdirSync(join(dir, "deep", "store", "ws"), { recursive: true }); ++ mkdirSync(join(dir, "a")); ++ symlinkSync(join(dir, "deep", "store", "ws"), join(dir, "a", "ws")); ++ return join(dir, "a", "ws"); ++ }, ++ outside: (dir) => join(dir, "deep", "a", "ws"), ++ escape: "../../a/ws", ++ stay: "../ws", ++ }, ++ dataRoot: { ++ place(dir) { ++ mkdirSync(join(dir, "deep", "store", "workspaces", "b", "i"), { recursive: true }); ++ symlinkSync(join(dir, "deep", "store"), join(dir, "data")); ++ return join(dir, "data", "workspaces", "b", "i"); ++ }, ++ outside: (dir) => join(dir, "deep", "data", "workspaces", "b", "i"), ++ escape: "../../../../data/workspaces/b/i", ++ stay: "../i", ++ }, ++}; ++ ++for (const [name, { place, outside, escape, stay }] of Object.entries(CLIMBS)) { ++ test(`pi: a relative path climbs from the real path of a ${name} behind a symlink`, async (t) => { ++ const { dir, workspace, s, env } = await session( ++ t, ++ [ ++ { name: "read", input: { path: `${escape}/secret.txt` } }, ++ { name: "write", input: { path: `${escape}/planted.txt`, content: "planted\n" } }, ++ { name: "read", input: { path: `${stay}/notes.txt` } }, ++ { name: "write", input: { path: `${stay}/fine.md`, content: "inside\n" } }, ++ ], ++ place, ++ ); ++ mkdirSync(outside(dir), { recursive: true }); ++ writeFileSync(join(outside(dir), "secret.txt"), "SECRET-OUTSIDE\n"); ++ const r = await turn(env, "Message 1 from jason, class REQUEST:\n\nread and write", workspace); ++ assert.equal(r.code, 0, r.stderr); ++ assert.doesNotMatch(r.stdout, /SECRET/); ++ const results = JSON.parse(r.stdout.trim().slice("ANSWER ".length)); ++ assert.equal(results.length, 4); ++ for (const i of [0, 1]) { ++ assert.equal(results[i][0], true); ++ assert.match(results[i][1], /outside the workspace/); ++ } ++ assert.deepEqual(readdirSync(outside(dir)), ["secret.txt"]); ++ assert.ok(!existsSync(join(workspace, "planted.txt"))); ++ assert.deepEqual(results[2], [false, "inside\n"]); ++ assert.equal(results[3][0], false); ++ assert.ok(existsSync(join(workspace, "fine.md")), "the write inside landed"); ++ 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/agents/darkwing/work/s6-review/r4/mut/MA-pi-only-harness.txt b/agents/darkwing/work/s6-review/r4/mut/MA-pi-only-harness.txt new file mode 100644 index 00000000..afdee6ae --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/MA-pi-only-harness.txt @@ -0,0 +1,88 @@ +✔ sessionModel: agent vars win, then the system's execution settings (9.580256ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.875628ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.264411ms) +✔ a bundle is written once: an existing file refuses (2.413773ms) +✔ a path with a single quote can't go into the hook command (2.401045ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (120.752066ms) +✔ a missing or wrong policy, or a bad event, exits 2 (89.898924ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1094.43891ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (771.164098ms) +✔ claude: the hook alone blocks a path outside the workspace (475.143096ms) +✔ claude: a second turn resumes the first turn's session (752.227287ms) +✔ claude adapter: --restricted is always passed (5.06431ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (705.819831ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.500421ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.919269ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.736461ms) +✔ file tool paths must resolve inside the workspace (1.265928ms) +✔ pi's own path normalisation can't be used to step out (0.898539ms) +✔ a symlink inside the workspace that points out is outside (0.774188ms) +✔ a dangling symlink is refused at any depth, in both harnesses (2.936794ms) +✖ read is checked under every spelling pi's read would open, in both harnesses (9.787623ms) +✔ other spellings cover directories, dangling links and pi's cwd (1.349726ms) +✔ a relative path climbs from the workspace's real path, in both harnesses (4.730941ms) +✔ claude path fields per tool (0.641296ms) +✔ glob patterns stay inside the workspace (0.59568ms) +✔ a path that can't be checked is blocked (0.503483ms) +✔ initialize, ping and tools/list (43.12883ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (33.600682ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (31.281717ms) +✔ a missing argument is a usage error (27.760206ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (352.619197ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (339.727109ms) +✔ pi: a read is refused when pi would open another spelling outside (325.182629ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (326.926759ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (315.373513ms) +✔ pi: a missing extension refuses before any model call (6.74773ms) +✔ pi: an extension without its configuration fails pi's start (259.176735ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.390225ms) +✔ turnRequest names the sender, class, reply and decision (0.201472ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (307.083097ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (139.973071ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (499.17115ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1380.389384ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (167.182531ms) +✔ founder credentials stop before the claim (20) (188.155837ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (225.922157ms) +✔ the launch ending under a running session exits 22 (146.624644ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (260.038018ms) +✔ a broker that is down at the claim exits 23, not 21 (89.780883ms) +✔ no capability, or a malformed one, on stdin exits 2 (225.852707ms) +✔ a missing or malformed policy exits 2 before the claim (120.417573ms) +✔ the PM gets launch, its task verbs and the reads (9.376178ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.997801ms) +✔ launch only when the business's launch block names the instance as launcher (1.988666ms) +✔ an action outside the instance's authority has no tool (2.235635ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.109859ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 55 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10598.778693 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:117:1 +✖ read is checked under every spelling pi's read would open, in both harnesses (9.787623ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:125: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' + } diff --git a/agents/darkwing/work/s6-review/r4/mut/MB-no-real-base-harness.txt b/agents/darkwing/work/s6-review/r4/mut/MB-no-real-base-harness.txt new file mode 100644 index 00000000..8f45cfef --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/MB-no-real-base-harness.txt @@ -0,0 +1,88 @@ +✔ sessionModel: agent vars win, then the system's execution settings (9.719171ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.201435ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.431407ms) +✔ a bundle is written once: an existing file refuses (2.102264ms) +✔ a path with a single quote can't go into the hook command (1.718452ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (116.794862ms) +✔ a missing or wrong policy, or a bad event, exits 2 (90.748979ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1101.993408ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (786.049055ms) +✔ claude: the hook alone blocks a path outside the workspace (469.934367ms) +✔ claude: a second turn resumes the first turn's session (742.318763ms) +✔ claude adapter: --restricted is always passed (4.883102ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (751.358808ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.625571ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (2.881547ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.744331ms) +✔ file tool paths must resolve inside the workspace (1.13976ms) +✔ pi's own path normalisation can't be used to step out (0.896935ms) +✔ a symlink inside the workspace that points out is outside (0.854833ms) +✔ a dangling symlink is refused at any depth, in both harnesses (2.812294ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (12.88844ms) +✖ other spellings cover directories, dangling links and pi's cwd (1.916041ms) +✔ a relative path climbs from the workspace's real path, in both harnesses (3.618899ms) +✔ claude path fields per tool (0.621012ms) +✔ glob patterns stay inside the workspace (0.605101ms) +✔ a path that can't be checked is blocked (0.30633ms) +✔ initialize, ping and tools/list (46.391592ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (35.489581ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (33.741286ms) +✔ a missing argument is a usage error (37.09461ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (366.303357ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (320.685111ms) +✔ pi: a read is refused when pi would open another spelling outside (325.031972ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (314.408294ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (321.338996ms) +✔ pi: a missing extension refuses before any model call (7.253326ms) +✔ pi: an extension without its configuration fails pi's start (275.660021ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.305136ms) +✔ turnRequest names the sender, class, reply and decision (0.219951ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (261.588566ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (109.294993ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (437.963508ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1324.780229ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (197.730832ms) +✔ founder credentials stop before the claim (20) (200.574545ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (199.070544ms) +✔ the launch ending under a running session exits 22 (147.394187ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (285.77662ms) +✔ a broker that is down at the claim exits 23, not 21 (90.9201ms) +✔ no capability, or a malformed one, on stdin exits 2 (205.906255ms) +✔ a missing or malformed policy exits 2 before the claim (124.520866ms) +✔ the PM gets launch, its task verbs and the reads (8.575108ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.715941ms) +✔ launch only when the business's launch block names the instance as launcher (2.251489ms) +✔ an action outside the instance's authority has no tool (2.805615ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (7.58421ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 55 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10444.401443 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:137:1 +✖ other spellings cover directories, dangling links and pi's cwd (1.916041ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:158:3) + 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/r4/mut/MC-no-nfd-curly-harness.txt b/agents/darkwing/work/s6-review/r4/mut/MC-no-nfd-curly-harness.txt new file mode 100644 index 00000000..c1d5c36c --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/MC-no-nfd-curly-harness.txt @@ -0,0 +1,106 @@ +✔ sessionModel: agent vars win, then the system's execution settings (14.299541ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (5.780738ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.604431ms) +✔ a bundle is written once: an existing file refuses (2.734256ms) +✔ a path with a single quote can't go into the hook command (2.413831ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (110.199454ms) +✔ a missing or wrong policy, or a bad event, exits 2 (78.793969ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1088.879771ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (714.853734ms) +✔ claude: the hook alone blocks a path outside the workspace (470.857176ms) +✔ claude: a second turn resumes the first turn's session (784.713208ms) +✔ claude adapter: --restricted is always passed (5.282364ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (758.968543ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.18475ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.258612ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.984727ms) +✔ file tool paths must resolve inside the workspace (1.751711ms) +✔ pi's own path normalisation can't be used to step out (1.275253ms) +✔ a symlink inside the workspace that points out is outside (1.244755ms) +✔ a dangling symlink is refused at any depth, in both harnesses (3.65246ms) +✖ read is checked under every spelling pi's read would open, in both harnesses (8.041349ms) +✔ other spellings cover directories, dangling links and pi's cwd (1.519293ms) +✔ a relative path climbs from the workspace's real path, in both harnesses (4.833057ms) +✔ claude path fields per tool (0.725909ms) +✔ glob patterns stay inside the workspace (0.611487ms) +✔ a path that can't be checked is blocked (0.668466ms) +✔ initialize, ping and tools/list (44.543011ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (30.236293ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (27.704757ms) +✔ a missing argument is a usage error (28.079649ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (341.746079ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (320.111724ms) +✖ pi: a read is refused when pi would open another spelling outside (314.753427ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (331.960128ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (334.444342ms) +✔ pi: a missing extension refuses before any model call (6.529418ms) +✔ pi: an extension without its configuration fails pi's start (266.191834ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.131708ms) +✔ turnRequest names the sender, class, reply and decision (0.183711ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (280.144771ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (100.874182ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (366.9824ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1296.333652ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (187.993487ms) +✔ founder credentials stop before the claim (20) (216.463256ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (265.774441ms) +✔ the launch ending under a running session exits 22 (165.246864ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (289.815346ms) +✔ a broker that is down at the claim exits 23, not 21 (129.57063ms) +✔ no capability, or a malformed one, on stdin exits 2 (223.535501ms) +✔ a missing or malformed policy exits 2 before the claim (155.040694ms) +✔ the PM gets launch, its task verbs and the reads (6.487345ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.088694ms) +✔ launch only when the business's launch block names the instance as launcher (1.70404ms) +✔ an action outside the instance's authority has no tool (1.440948ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (9.461132ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 54 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10346.50573 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:117:1 +✖ read is checked under every spelling pi's read would open, in both harnesses (8.041349ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:125: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:123:1 +✖ pi: a read is refused when pi would open another spelling outside (314.753427ms) + AssertionError [ERR_ASSERTION]: The input was expected to not match the regular expression /SECRET/. Input: + + `ANSWER [[true,"mosaic gate: read path is outside the workspace under another spelling: notes's."],[true,"mosaic gate: read path is outside the workspace under another spelling: shot 9.4"],[true,"mosaic gate: read path is outside the workspace under another spelling: résumé.t"],[false,"SECRET-OUTSIDE\\n"],[true,"mosaic gate: read path is outside the workspace under another spelling: l'été.tx"],[true,"mosaic gate: read path is outside the workspace under another spelling: l'été.md"],[false,"inside\\n"]]\n` + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/pi-session.test.mjs:145: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: `ANSWER [[true,"mosaic gate: read path is outside the workspace under another spelling: notes's."],[true,"mosaic gate: read path is outside the workspace under another spelling: shot 9.4"],[true,"mosaic gate: read path is outside the workspace under another spelling: résumé.t"],[false,"SECRET-OUTSIDE\\n"],[true,"mosaic gate: read path is outside the workspace under another spelling: l'été.tx"],[true,"mosaic gate: read path is outside the workspace under another spelling: l'été.md"],[false,"inside\\n"]]\n`, + expected: /SECRET/, + operator: 'doesNotMatch', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r4/mut/MD-given-only-harness.txt b/agents/darkwing/work/s6-review/r4/mut/MD-given-only-harness.txt new file mode 100644 index 00000000..088ee09d --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/MD-given-only-harness.txt @@ -0,0 +1,124 @@ +✔ sessionModel: agent vars win, then the system's execution settings (12.960219ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (5.112444ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.679235ms) +✔ a bundle is written once: an existing file refuses (2.812263ms) +✔ a path with a single quote can't go into the hook command (2.517504ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (136.603784ms) +✔ a missing or wrong policy, or a bad event, exits 2 (82.034771ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1094.979161ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (777.212211ms) +✔ claude: the hook alone blocks a path outside the workspace (472.515691ms) +✔ claude: a second turn resumes the first turn's session (762.499074ms) +✔ claude adapter: --restricted is always passed (4.735663ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (746.837511ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.681726ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.542476ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.71412ms) +✔ file tool paths must resolve inside the workspace (1.156309ms) +✔ pi's own path normalisation can't be used to step out (1.080151ms) +✔ a symlink inside the workspace that points out is outside (1.165845ms) +✔ a dangling symlink is refused at any depth, in both harnesses (4.072642ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (20.693322ms) +✔ other spellings cover directories, dangling links and pi's cwd (2.800193ms) +✖ a relative path climbs from the workspace's real path, in both harnesses (3.279667ms) +✔ claude path fields per tool (2.601182ms) +✔ glob patterns stay inside the workspace (1.257995ms) +✔ a path that can't be checked is blocked (0.55942ms) +✔ initialize, ping and tools/list (52.537718ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (40.673685ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (35.993545ms) +✔ a missing argument is a usage error (29.947767ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (384.717068ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (329.198526ms) +✔ pi: a read is refused when pi would open another spelling outside (337.808371ms) +✖ pi: a relative path climbs from the real path of a workspace behind a symlink (340.250878ms) +✖ pi: a relative path climbs from the real path of a dataRoot behind a symlink (318.836655ms) +✔ pi: a missing extension refuses before any model call (6.434745ms) +✔ pi: an extension without its configuration fails pi's start (272.153021ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.55466ms) +✔ turnRequest names the sender, class, reply and decision (0.996447ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (246.016744ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (101.349298ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (398.221516ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1294.309301ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (145.992858ms) +✔ founder credentials stop before the claim (20) (173.49626ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (185.73365ms) +✔ the launch ending under a running session exits 22 (141.709901ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (243.783028ms) +✔ a broker that is down at the claim exits 23, not 21 (105.666355ms) +✔ no capability, or a malformed one, on stdin exits 2 (184.835076ms) +✔ a missing or malformed policy exits 2 before the claim (137.674261ms) +✔ the PM gets launch, its task verbs and the reads (9.441748ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.792932ms) +✔ launch only when the business's launch block names the instance as launcher (2.844521ms) +✔ an action outside the instance's authority has no tool (2.336641ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (14.398564ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 53 +ℹ fail 3 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10316.014664 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:181:1 +✖ a relative path climbs from the workspace's real path, in both harnesses (3.279667ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:195: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:184:3 +✖ pi: a relative path climbs from the real path of a workspace behind a symlink (340.250878ms) + AssertionError [ERR_ASSERTION]: The input was expected to not match the regular expression /SECRET/. Input: + + 'ANSWER [[false,"SECRET-OUTSIDE\\n"],[false,"Successfully wrote to ../../a/ws/planted.txt"],[false,"inside\\n"],[false,"Successfully wrote to ../ws/fine.md"]]\n' + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/pi-session.test.mjs:199: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: 'ANSWER [[false,"SECRET-OUTSIDE\\n"],[false,"Successfully wrote to ../../a/ws/planted.txt"],[false,"inside\\n"],[false,"Successfully wrote to ../ws/fine.md"]]\n', + expected: /SECRET/, + operator: 'doesNotMatch', + diff: 'simple' + } + +test at packages/harness/tests/pi-session.test.mjs:184:3 +✖ pi: a relative path climbs from the real path of a dataRoot behind a symlink (318.836655ms) + AssertionError [ERR_ASSERTION]: The input was expected to not match the regular expression /SECRET/. Input: + + 'ANSWER [[false,"SECRET-OUTSIDE\\n"],[false,"Successfully wrote to ../../../../data/workspaces/b/i/planted.txt"],[false,"inside\\n"],[false,"Successfully wrote to ../i/fine.md"]]\n' + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/pi-session.test.mjs:199: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: 'ANSWER [[false,"SECRET-OUTSIDE\\n"],[false,"Successfully wrote to ../../../../data/workspaces/b/i/planted.txt"],[false,"inside\\n"],[false,"Successfully wrote to ../i/fine.md"]]\n', + expected: /SECRET/, + operator: 'doesNotMatch', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r4/mut/ME-real-only-harness.txt b/agents/darkwing/work/s6-review/r4/mut/ME-real-only-harness.txt new file mode 100644 index 00000000..97cce658 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/ME-real-only-harness.txt @@ -0,0 +1,88 @@ +✔ sessionModel: agent vars win, then the system's execution settings (13.166504ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (6.830098ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (6.181177ms) +✔ a bundle is written once: an existing file refuses (7.252436ms) +✔ a path with a single quote can't go into the hook command (5.229841ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (142.913603ms) +✔ a missing or wrong policy, or a bad event, exits 2 (82.446831ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1095.78911ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (784.398303ms) +✔ claude: the hook alone blocks a path outside the workspace (450.223193ms) +✔ claude: a second turn resumes the first turn's session (712.010899ms) +✔ claude adapter: --restricted is always passed (5.340269ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (755.954009ms) +✔ claude: a missing hook or MCP file refuses before claude starts (8.520012ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (4.15195ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (1.21445ms) +✔ file tool paths must resolve inside the workspace (2.766564ms) +✔ pi's own path normalisation can't be used to step out (2.158376ms) +✔ a symlink inside the workspace that points out is outside (1.458484ms) +✔ a dangling symlink is refused at any depth, in both harnesses (6.620982ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (26.532986ms) +✔ other spellings cover directories, dangling links and pi's cwd (2.795343ms) +✖ a relative path climbs from the workspace's real path, in both harnesses (2.936662ms) +✔ claude path fields per tool (0.717094ms) +✔ glob patterns stay inside the workspace (0.725578ms) +✔ a path that can't be checked is blocked (0.352763ms) +✔ initialize, ping and tools/list (61.47675ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (33.510512ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (37.845858ms) +✔ a missing argument is a usage error (33.268183ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (376.033974ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (349.931587ms) +✔ pi: a read is refused when pi would open another spelling outside (327.087561ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (307.436623ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (310.483057ms) +✔ pi: a missing extension refuses before any model call (6.451642ms) +✔ pi: an extension without its configuration fails pi's start (253.374058ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.798476ms) +✔ turnRequest names the sender, class, reply and decision (0.274713ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (255.701856ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (93.390217ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (431.337337ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1309.037288ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (151.031862ms) +✔ founder credentials stop before the claim (20) (176.839142ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (212.325895ms) +✔ the launch ending under a running session exits 22 (150.674328ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (244.375556ms) +✔ a broker that is down at the claim exits 23, not 21 (110.434316ms) +✔ no capability, or a malformed one, on stdin exits 2 (191.896952ms) +✔ a missing or malformed policy exits 2 before the claim (125.187036ms) +✔ the PM gets launch, its task verbs and the reads (11.317213ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (3.732743ms) +✔ launch only when the business's launch block names the instance as launcher (3.338526ms) +✔ an action outside the instance's authority has no tool (2.391658ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (11.414071ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 55 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10364.03871 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:181:1 +✖ a relative path climbs from the workspace's real path, in both harnesses (2.936662ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:203: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' + } diff --git a/agents/darkwing/work/s6-review/r4/mut/MF-either-harness.txt b/agents/darkwing/work/s6-review/r4/mut/MF-either-harness.txt new file mode 100644 index 00000000..94e2c9d6 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/MF-either-harness.txt @@ -0,0 +1,124 @@ +✔ sessionModel: agent vars win, then the system's execution settings (10.26592ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.407655ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.439738ms) +✔ a bundle is written once: an existing file refuses (2.565066ms) +✔ a path with a single quote can't go into the hook command (2.449872ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (116.969789ms) +✔ a missing or wrong policy, or a bad event, exits 2 (86.932395ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1093.719288ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (742.776859ms) +✔ claude: the hook alone blocks a path outside the workspace (454.012193ms) +✔ claude: a second turn resumes the first turn's session (725.734225ms) +✔ claude adapter: --restricted is always passed (5.519519ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (731.914825ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.393044ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (4.344071ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.939757ms) +✔ file tool paths must resolve inside the workspace (1.700489ms) +✔ pi's own path normalisation can't be used to step out (1.418286ms) +✔ a symlink inside the workspace that points out is outside (1.406512ms) +✔ a dangling symlink is refused at any depth, in both harnesses (5.263714ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (14.890815ms) +✔ other spellings cover directories, dangling links and pi's cwd (1.603789ms) +✖ a relative path climbs from the workspace's real path, in both harnesses (1.570464ms) +✔ claude path fields per tool (0.61444ms) +✔ glob patterns stay inside the workspace (0.603014ms) +✔ a path that can't be checked is blocked (0.35295ms) +✔ initialize, ping and tools/list (45.416085ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (34.045402ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (33.532246ms) +✔ a missing argument is a usage error (30.32903ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (370.652891ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (314.580098ms) +✔ pi: a read is refused when pi would open another spelling outside (312.164008ms) +✖ pi: a relative path climbs from the real path of a workspace behind a symlink (318.0124ms) +✖ pi: a relative path climbs from the real path of a dataRoot behind a symlink (309.389775ms) +✔ pi: a missing extension refuses before any model call (6.672674ms) +✔ pi: an extension without its configuration fails pi's start (252.480031ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.648398ms) +✔ turnRequest names the sender, class, reply and decision (0.265695ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (275.015356ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (97.595768ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (388.631163ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1274.623193ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (166.728434ms) +✔ founder credentials stop before the claim (20) (172.532777ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (192.895858ms) +✔ the launch ending under a running session exits 22 (148.216836ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (240.516646ms) +✔ a broker that is down at the claim exits 23, not 21 (106.237078ms) +✔ no capability, or a malformed one, on stdin exits 2 (205.691137ms) +✔ a missing or malformed policy exits 2 before the claim (129.43942ms) +✔ the PM gets launch, its task verbs and the reads (6.44726ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.776687ms) +✔ launch only when the business's launch block names the instance as launcher (1.913587ms) +✔ an action outside the instance's authority has no tool (1.525496ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (10.830046ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 53 +ℹ fail 3 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10309.973247 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:181:1 +✖ a relative path climbs from the workspace's real path, in both harnesses (1.570464ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:195: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:184:3 +✖ pi: a relative path climbs from the real path of a workspace behind a symlink (318.0124ms) + AssertionError [ERR_ASSERTION]: The input was expected to not match the regular expression /SECRET/. Input: + + 'ANSWER [[false,"SECRET-OUTSIDE\\n"],[false,"Successfully wrote to ../../a/ws/planted.txt"],[false,"inside\\n"],[false,"Successfully wrote to ../ws/fine.md"]]\n' + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/pi-session.test.mjs:199: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: 'ANSWER [[false,"SECRET-OUTSIDE\\n"],[false,"Successfully wrote to ../../a/ws/planted.txt"],[false,"inside\\n"],[false,"Successfully wrote to ../ws/fine.md"]]\n', + expected: /SECRET/, + operator: 'doesNotMatch', + diff: 'simple' + } + +test at packages/harness/tests/pi-session.test.mjs:184:3 +✖ pi: a relative path climbs from the real path of a dataRoot behind a symlink (309.389775ms) + AssertionError [ERR_ASSERTION]: The input was expected to not match the regular expression /SECRET/. Input: + + 'ANSWER [[false,"SECRET-OUTSIDE\\n"],[false,"Successfully wrote to ../../../../data/workspaces/b/i/planted.txt"],[false,"inside\\n"],[false,"Successfully wrote to ../i/fine.md"]]\n' + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/pi-session.test.mjs:199: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: 'ANSWER [[false,"SECRET-OUTSIDE\\n"],[false,"Successfully wrote to ../../../../data/workspaces/b/i/planted.txt"],[false,"inside\\n"],[false,"Successfully wrote to ../i/fine.md"]]\n', + expected: /SECRET/, + operator: 'doesNotMatch', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/r4/mut/MG-inside-no-normalise-harness.txt b/agents/darkwing/work/s6-review/r4/mut/MG-inside-no-normalise-harness.txt new file mode 100644 index 00000000..8c321880 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/MG-inside-no-normalise-harness.txt @@ -0,0 +1,88 @@ +✔ sessionModel: agent vars win, then the system's execution settings (9.87935ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.875106ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.619702ms) +✔ a bundle is written once: an existing file refuses (3.115892ms) +✔ a path with a single quote can't go into the hook command (1.729883ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (121.876014ms) +✔ a missing or wrong policy, or a bad event, exits 2 (88.51807ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1092.263287ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (759.309882ms) +✔ claude: the hook alone blocks a path outside the workspace (472.195897ms) +✔ claude: a second turn resumes the first turn's session (724.679605ms) +✔ claude adapter: --restricted is always passed (5.303066ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (741.258526ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.418726ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.858675ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.887281ms) +✔ file tool paths must resolve inside the workspace (1.564652ms) +✖ pi's own path normalisation can't be used to step out (1.732209ms) +✔ a symlink inside the workspace that points out is outside (1.069819ms) +✔ a dangling symlink is refused at any depth, in both harnesses (3.765969ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (18.477165ms) +✔ other spellings cover directories, dangling links and pi's cwd (1.36167ms) +✔ a relative path climbs from the workspace's real path, in both harnesses (4.836506ms) +✔ claude path fields per tool (0.684994ms) +✔ glob patterns stay inside the workspace (0.582159ms) +✔ a path that can't be checked is blocked (0.325911ms) +✔ initialize, ping and tools/list (40.58713ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (31.77131ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (30.183541ms) +✔ a missing argument is a usage error (31.533311ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (354.180455ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (327.591937ms) +✔ pi: a read is refused when pi would open another spelling outside (318.33446ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (338.669318ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (309.963678ms) +✔ pi: a missing extension refuses before any model call (7.225595ms) +✔ pi: an extension without its configuration fails pi's start (261.687691ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.0956ms) +✔ turnRequest names the sender, class, reply and decision (0.219588ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (234.062414ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (111.15252ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (368.79309ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1285.997552ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (158.901003ms) +✔ founder credentials stop before the claim (20) (185.91479ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (187.351958ms) +✔ the launch ending under a running session exits 22 (154.766842ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (263.620185ms) +✔ a broker that is down at the claim exits 23, not 21 (95.091753ms) +✔ no capability, or a malformed one, on stdin exits 2 (200.368017ms) +✔ a missing or malformed policy exits 2 before the claim (132.846006ms) +✔ the PM gets launch, its task verbs and the reads (6.507392ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.297129ms) +✔ launch only when the business's launch block names the instance as launcher (2.20434ms) +✔ an action outside the instance's authority has no tool (2.706779ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (9.810235ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 55 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10262.068026 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:60:1 +✖ pi's own path normalisation can't be used to step out (1.732209ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:62:3) + 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/r4/mut/Ml-gate-pattern-harness.txt b/agents/darkwing/work/s6-review/r4/mut/Ml-gate-pattern-harness.txt new file mode 100644 index 00000000..9ee8cd8d --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/Ml-gate-pattern-harness.txt @@ -0,0 +1,88 @@ +✔ sessionModel: agent vars win, then the system's execution settings (10.369128ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.101077ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.522222ms) +✔ a bundle is written once: an existing file refuses (2.480421ms) +✔ a path with a single quote can't go into the hook command (2.208256ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (125.228714ms) +✔ a missing or wrong policy, or a bad event, exits 2 (99.007065ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1096.273556ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (751.155725ms) +✔ claude: the hook alone blocks a path outside the workspace (441.135402ms) +✔ claude: a second turn resumes the first turn's session (735.082802ms) +✔ claude adapter: --restricted is always passed (5.239573ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (739.084559ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.810106ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.098661ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.72834ms) +✔ file tool paths must resolve inside the workspace (1.366765ms) +✔ pi's own path normalisation can't be used to step out (1.135173ms) +✔ a symlink inside the workspace that points out is outside (1.183478ms) +✔ a dangling symlink is refused at any depth, in both harnesses (3.06165ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (17.10114ms) +✔ other spellings cover directories, dangling links and pi's cwd (1.481463ms) +✔ a relative path climbs from the workspace's real path, in both harnesses (3.849039ms) +✔ claude path fields per tool (0.575647ms) +✖ glob patterns stay inside the workspace (1.300289ms) +✔ a path that can't be checked is blocked (0.383337ms) +✔ initialize, ping and tools/list (43.723869ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (33.899289ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (32.230065ms) +✔ a missing argument is a usage error (28.925962ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (369.215026ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (320.066306ms) +✔ pi: a read is refused when pi would open another spelling outside (312.239331ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (306.779191ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (312.126907ms) +✔ pi: a missing extension refuses before any model call (6.510548ms) +✔ pi: an extension without its configuration fails pi's start (268.831889ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.279785ms) +✔ turnRequest names the sender, class, reply and decision (0.19562ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (251.909305ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (105.575145ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (376.329399ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1308.851736ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (167.787107ms) +✔ founder credentials stop before the claim (20) (170.54228ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (198.618677ms) +✔ the launch ending under a running session exits 22 (143.274433ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (241.465266ms) +✔ a broker that is down at the claim exits 23, not 21 (91.778621ms) +✔ no capability, or a malformed one, on stdin exits 2 (191.158646ms) +✔ a missing or malformed policy exits 2 before the claim (138.321017ms) +✔ the PM gets launch, its task verbs and the reads (6.713097ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.657396ms) +✔ launch only when the business's launch block names the instance as launcher (1.954434ms) +✔ an action outside the instance's authority has no tool (1.581718ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.690613ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 55 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10327.132921 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:218:1 +✖ glob patterns stay inside the workspace (1.300289ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:224: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/r4/mut/Ms-follow-walk-harness.txt b/agents/darkwing/work/s6-review/r4/mut/Ms-follow-walk-harness.txt new file mode 100644 index 00000000..86ef8f6b --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/Ms-follow-walk-harness.txt @@ -0,0 +1,128 @@ +✔ sessionModel: agent vars win, then the system's execution settings (13.951592ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (5.155778ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.474296ms) +✔ a bundle is written once: an existing file refuses (2.365986ms) +✔ a path with a single quote can't go into the hook command (2.112037ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (142.840425ms) +✔ a missing or wrong policy, or a bad event, exits 2 (94.614087ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1113.358863ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (901.454306ms) +✔ claude: the hook alone blocks a path outside the workspace (511.779436ms) +✔ claude: a second turn resumes the first turn's session (824.794016ms) +✔ claude adapter: --restricted is always passed (5.078018ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (748.925681ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.923018ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.70504ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.748106ms) +✔ file tool paths must resolve inside the workspace (1.600925ms) +✔ pi's own path normalisation can't be used to step out (1.056965ms) +✔ a symlink inside the workspace that points out is outside (1.456909ms) +✖ a dangling symlink is refused at any depth, in both harnesses (2.352632ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (28.519068ms) +✖ other spellings cover directories, dangling links and pi's cwd (1.248655ms) +✔ a relative path climbs from the workspace's real path, in both harnesses (5.971957ms) +✔ claude path fields per tool (0.756134ms) +✔ glob patterns stay inside the workspace (0.716178ms) +✔ a path that can't be checked is blocked (0.432944ms) +✔ initialize, ping and tools/list (50.004724ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (41.438932ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (36.295209ms) +✔ a missing argument is a usage error (34.276915ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (453.423865ms) +✖ pi: a write through a dangling symlink is blocked, and nothing appears outside (388.523149ms) +✔ pi: a read is refused when pi would open another spelling outside (364.03261ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (333.182119ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (376.888665ms) +✔ pi: a missing extension refuses before any model call (7.262079ms) +✔ pi: an extension without its configuration fails pi's start (280.114519ms) +✔ founderCheck: founder variables, then a needed service without a usable token (2.043332ms) +✔ turnRequest names the sender, class, reply and decision (0.270957ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (266.915596ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (104.932711ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (457.164815ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1342.709056ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (150.189063ms) +✔ founder credentials stop before the claim (20) (199.636928ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (195.51418ms) +✔ the launch ending under a running session exits 22 (142.652928ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (242.859152ms) +✔ a broker that is down at the claim exits 23, not 21 (91.526522ms) +✔ no capability, or a malformed one, on stdin exits 2 (187.48731ms) +✔ a missing or malformed policy exits 2 before the claim (131.742592ms) +✔ the PM gets launch, its task verbs and the reads (9.873905ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.67407ms) +✔ launch only when the business's launch block names the instance as launcher (2.385567ms) +✔ an action outside the instance's authority has no tool (2.079737ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (10.585924ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 53 +ℹ fail 3 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10444.499104 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:82:1 +✖ a dangling symlink is refused at any depth, in both harnesses (2.352632ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/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/gate.test.mjs:137:1 +✖ other spellings cover directories, dangling links and pi's cwd (1.248655ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/wt/packages/harness/tests/gate.test.mjs:146:3) + 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 (388.523149ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + false !== true + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41d/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/r4/mut/Mw-ampm-case-harness.txt b/agents/darkwing/work/s6-review/r4/mut/Mw-ampm-case-harness.txt new file mode 100644 index 00000000..4b8f5286 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/Mw-ampm-case-harness.txt @@ -0,0 +1,64 @@ +✔ sessionModel: agent vars win, then the system's execution settings (12.908249ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (6.241662ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.802171ms) +✔ a bundle is written once: an existing file refuses (2.286689ms) +✔ a path with a single quote can't go into the hook command (1.792504ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (146.942107ms) +✔ a missing or wrong policy, or a bad event, exits 2 (98.035985ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1098.836348ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (802.483352ms) +✔ claude: the hook alone blocks a path outside the workspace (499.584226ms) +✔ claude: a second turn resumes the first turn's session (707.490069ms) +✔ claude adapter: --restricted is always passed (5.085363ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (742.856032ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.308119ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.454864ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (1.036551ms) +✔ file tool paths must resolve inside the workspace (1.903732ms) +✔ pi's own path normalisation can't be used to step out (1.156018ms) +✔ a symlink inside the workspace that points out is outside (1.086308ms) +✔ a dangling symlink is refused at any depth, in both harnesses (2.923023ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (17.977038ms) +✔ other spellings cover directories, dangling links and pi's cwd (2.409777ms) +✔ a relative path climbs from the workspace's real path, in both harnesses (6.172826ms) +✔ claude path fields per tool (0.815392ms) +✔ glob patterns stay inside the workspace (0.622669ms) +✔ a path that can't be checked is blocked (0.373151ms) +✔ initialize, ping and tools/list (57.281691ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (37.215756ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (39.649493ms) +✔ a missing argument is a usage error (34.344617ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (405.306493ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (320.655773ms) +✔ pi: a read is refused when pi would open another spelling outside (307.185492ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (322.675224ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (311.126719ms) +✔ pi: a missing extension refuses before any model call (6.209092ms) +✔ pi: an extension without its configuration fails pi's start (247.97222ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.741181ms) +✔ turnRequest names the sender, class, reply and decision (0.26822ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (294.273799ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (119.187454ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (418.835251ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1478.017629ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (147.14452ms) +✔ founder credentials stop before the claim (20) (173.27824ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (205.366219ms) +✔ the launch ending under a running session exits 22 (153.348717ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (246.306522ms) +✔ a broker that is down at the claim exits 23, not 21 (90.996741ms) +✔ no capability, or a malformed one, on stdin exits 2 (186.732832ms) +✔ a missing or malformed policy exits 2 before the claim (133.298297ms) +✔ the PM gets launch, its task verbs and the reads (8.329872ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (3.876034ms) +✔ launch only when the business's launch block names the instance as launcher (2.80248ms) +✔ an action outside the instance's authority has no tool (2.310834ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (9.966718ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 56 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10570.889232 diff --git a/agents/darkwing/work/s6-review/r4/mut/Mx-curly-once-harness.txt b/agents/darkwing/work/s6-review/r4/mut/Mx-curly-once-harness.txt new file mode 100644 index 00000000..1cf79be7 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/Mx-curly-once-harness.txt @@ -0,0 +1,64 @@ +✔ sessionModel: agent vars win, then the system's execution settings (16.419969ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (7.907433ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (4.748261ms) +✔ a bundle is written once: an existing file refuses (3.551392ms) +✔ a path with a single quote can't go into the hook command (3.6116ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (154.868776ms) +✔ a missing or wrong policy, or a bad event, exits 2 (84.333949ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1086.679703ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (767.100937ms) +✔ claude: the hook alone blocks a path outside the workspace (436.596675ms) +✔ claude: a second turn resumes the first turn's session (729.009786ms) +✔ claude adapter: --restricted is always passed (5.051284ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (752.813721ms) +✔ claude: a missing hook or MCP file refuses before claude starts (9.431351ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (4.314906ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (1.05245ms) +✔ file tool paths must resolve inside the workspace (2.215003ms) +✔ pi's own path normalisation can't be used to step out (1.563926ms) +✔ a symlink inside the workspace that points out is outside (1.278773ms) +✔ a dangling symlink is refused at any depth, in both harnesses (4.68278ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (26.116575ms) +✔ other spellings cover directories, dangling links and pi's cwd (2.149847ms) +✔ a relative path climbs from the workspace's real path, in both harnesses (5.004587ms) +✔ claude path fields per tool (0.791412ms) +✔ glob patterns stay inside the workspace (0.664173ms) +✔ a path that can't be checked is blocked (0.371877ms) +✔ initialize, ping and tools/list (57.874523ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (44.19034ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (40.393765ms) +✔ a missing argument is a usage error (29.144246ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (389.968217ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (315.424366ms) +✔ pi: a read is refused when pi would open another spelling outside (313.603289ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (309.690487ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (329.985874ms) +✔ pi: a missing extension refuses before any model call (7.347651ms) +✔ pi: an extension without its configuration fails pi's start (252.241598ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.886259ms) +✔ turnRequest names the sender, class, reply and decision (0.25284ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (268.26198ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (106.707276ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (369.174258ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1292.260566ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (164.241797ms) +✔ founder credentials stop before the claim (20) (170.743016ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (208.541368ms) +✔ the launch ending under a running session exits 22 (147.66599ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (243.905838ms) +✔ a broker that is down at the claim exits 23, not 21 (92.452572ms) +✔ no capability, or a malformed one, on stdin exits 2 (184.969794ms) +✔ a missing or malformed policy exits 2 before the claim (130.261538ms) +✔ the PM gets launch, its task verbs and the reads (11.660568ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (4.20283ms) +✔ launch only when the business's launch block names the instance as launcher (2.073248ms) +✔ an action outside the instance's authority has no tool (1.784904ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (11.548405ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 56 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10368.226809 diff --git a/agents/darkwing/work/s6-review/r4/mut/My-lstat-error-skips-harness.txt b/agents/darkwing/work/s6-review/r4/mut/My-lstat-error-skips-harness.txt new file mode 100644 index 00000000..6d6e4d04 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/My-lstat-error-skips-harness.txt @@ -0,0 +1,64 @@ +✔ sessionModel: agent vars win, then the system's execution settings (13.841783ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (6.287276ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (4.087337ms) +✔ a bundle is written once: an existing file refuses (3.239927ms) +✔ a path with a single quote can't go into the hook command (2.598993ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (123.207972ms) +✔ a missing or wrong policy, or a bad event, exits 2 (98.727577ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1087.836441ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (782.019432ms) +✔ claude: the hook alone blocks a path outside the workspace (491.276084ms) +✔ claude: a second turn resumes the first turn's session (767.524694ms) +✔ claude adapter: --restricted is always passed (4.996153ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (746.117429ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.61189ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (4.492952ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.865491ms) +✔ file tool paths must resolve inside the workspace (1.684216ms) +✔ pi's own path normalisation can't be used to step out (1.876241ms) +✔ a symlink inside the workspace that points out is outside (1.085719ms) +✔ a dangling symlink is refused at any depth, in both harnesses (3.340783ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (20.843709ms) +✔ other spellings cover directories, dangling links and pi's cwd (2.281127ms) +✔ a relative path climbs from the workspace's real path, in both harnesses (4.161104ms) +✔ claude path fields per tool (0.717995ms) +✔ glob patterns stay inside the workspace (0.604427ms) +✔ a path that can't be checked is blocked (0.402529ms) +✔ initialize, ping and tools/list (48.632538ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (35.737239ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (32.617341ms) +✔ a missing argument is a usage error (39.132107ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (372.477226ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (328.647932ms) +✔ pi: a read is refused when pi would open another spelling outside (353.861388ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (315.988888ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (315.788017ms) +✔ pi: a missing extension refuses before any model call (6.271687ms) +✔ pi: an extension without its configuration fails pi's start (255.601498ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.197926ms) +✔ turnRequest names the sender, class, reply and decision (0.2689ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (268.276295ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (142.384566ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (520.416544ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1401.520636ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (204.356734ms) +✔ founder credentials stop before the claim (20) (278.91878ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (334.728922ms) +✔ the launch ending under a running session exits 22 (210.128341ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (304.579393ms) +✔ a broker that is down at the claim exits 23, not 21 (174.27166ms) +✔ no capability, or a malformed one, on stdin exits 2 (248.731471ms) +✔ a missing or malformed policy exits 2 before the claim (178.749977ms) +✔ the PM gets launch, its task verbs and the reads (7.78365ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (1.962966ms) +✔ launch only when the business's launch block names the instance as launcher (2.344147ms) +✔ an action outside the instance's authority has no tool (2.275202ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (10.731608ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 56 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10659.905003 diff --git a/agents/darkwing/work/s6-review/r4/mut/Mz-spell-no-normalise-harness.txt b/agents/darkwing/work/s6-review/r4/mut/Mz-spell-no-normalise-harness.txt new file mode 100644 index 00000000..39aaa1d9 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/Mz-spell-no-normalise-harness.txt @@ -0,0 +1,64 @@ +✔ sessionModel: agent vars win, then the system's execution settings (13.676129ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (5.412586ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.383246ms) +✔ a bundle is written once: an existing file refuses (2.825298ms) +✔ a path with a single quote can't go into the hook command (2.090268ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (117.297968ms) +✔ a missing or wrong policy, or a bad event, exits 2 (88.00604ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1086.177758ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (742.293868ms) +✔ claude: the hook alone blocks a path outside the workspace (513.929576ms) +✔ claude: a second turn resumes the first turn's session (745.910637ms) +✔ claude adapter: --restricted is always passed (5.032461ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (707.280629ms) +✔ claude: a missing hook or MCP file refuses before claude starts (8.387445ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.200965ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.63065ms) +✔ file tool paths must resolve inside the workspace (1.045475ms) +✔ pi's own path normalisation can't be used to step out (0.876209ms) +✔ a symlink inside the workspace that points out is outside (0.808567ms) +✔ a dangling symlink is refused at any depth, in both harnesses (2.586055ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (17.413263ms) +✔ other spellings cover directories, dangling links and pi's cwd (1.415811ms) +✔ a relative path climbs from the workspace's real path, in both harnesses (4.146039ms) +✔ claude path fields per tool (0.708978ms) +✔ glob patterns stay inside the workspace (0.61424ms) +✔ a path that can't be checked is blocked (0.368175ms) +✔ initialize, ping and tools/list (44.145606ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (31.410294ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (28.633656ms) +✔ a missing argument is a usage error (27.843755ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (355.378997ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (319.122119ms) +✔ pi: a read is refused when pi would open another spelling outside (331.533845ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (338.818078ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (314.704983ms) +✔ pi: a missing extension refuses before any model call (5.987169ms) +✔ pi: an extension without its configuration fails pi's start (268.431565ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.073037ms) +✔ turnRequest names the sender, class, reply and decision (0.167589ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (269.596609ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (123.863191ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (421.121943ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1369.369925ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (178.960623ms) +✔ founder credentials stop before the claim (20) (210.084389ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (230.426167ms) +✔ the launch ending under a running session exits 22 (185.471755ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (279.435871ms) +✔ a broker that is down at the claim exits 23, not 21 (126.104629ms) +✔ no capability, or a malformed one, on stdin exits 2 (189.947621ms) +✔ a missing or malformed policy exits 2 before the claim (128.156013ms) +✔ the PM gets launch, its task verbs and the reads (6.680204ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (1.717751ms) +✔ launch only when the business's launch block names the instance as launcher (1.52598ms) +✔ an action outside the instance's authority has no tool (2.066738ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.417549ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 56 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10477.502097 diff --git a/agents/darkwing/work/s6-review/r4/mut/all.sh b/agents/darkwing/work/s6-review/r4/mut/all.sh new file mode 100755 index 00000000..33ece3d0 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/all.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Round 4: the gate mutants on the harness suite. The other round 3 mutants +# change source and tests that round 4 doesn't touch, so they weren't rerun. +R=~/darkwing-scratch/r41d/mut +rm -f $R/summary.txt $R/M*-*.txt +for m in Ml-gate-pattern Ms-follow-walk Mw-ampm-case Mx-curly-once My-lstat-error-skips Mz-spell-no-normalise MA-pi-only MB-no-real-base MC-no-nfd-curly MD-given-only ME-real-only MF-either MG-inside-no-normalise; do + $R/run.sh $m harness +done +echo DONE >> $R/summary.txt diff --git a/agents/darkwing/work/s6-review/r4/mut/manifest-after.txt b/agents/darkwing/work/s6-review/r4/mut/manifest-after.txt new file mode 100644 index 00000000..e0d5cd2a --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r4/mut/mutate.py b/agents/darkwing/work/s6-review/r4/mut/mutate.py new file mode 100644 index 00000000..461c72ee --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/mutate.py @@ -0,0 +1,78 @@ +# 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;"), + # Round 3: the R4 spelling check. + "Mw-ampm-case": ("packages/harness/src/gate.mjs", "/ (AM|PM)\\./gi", "/ (AM|PM)\\./g"), + "Mx-curly-once": ("packages/harness/src/gate.mjs", "v.replace(/'/g, \"\\u2019\")", "v.replace(/'/, \"\\u2019\")"), + "My-lstat-error-skips": ("packages/harness/src/gate.mjs", + "if (error.code === \"ENOTDIR\") continue;\n return `${tool} path can't be checked under another spelling: ${error.code ?? error.message}`;", + "continue;"), + "Mz-spell-no-normalise": ("packages/harness/src/gate.mjs", "const s = normalise(p);\n const bases", "const s = p;\n const bases"), + "MA-pi-only": ("packages/harness/src/gate.mjs", "if (tool === (claude ? \"Read\" : \"read\")) {", "if (tool === \"read\") {"), + "MB-no-real-base": ("packages/harness/src/gate.mjs", + "[resolve(workspace, s), resolve(realpathSync(workspace), s)]", "[resolve(workspace, s)]"), + "MC-no-nfd-curly": ("packages/harness/src/gate.mjs", "curly(r), curly(nfd)]", "curly(r)]"), + # Round 4: the R5 relative-path check. + "MD-given-only": ("packages/harness/src/gate.mjs", ' return within(workspace, resolve(realpathSync(workspace), s)) && within(workspace, resolve(workspace, s));', " return within(workspace, resolve(workspace, s));"), + "ME-real-only": ("packages/harness/src/gate.mjs", ' return within(workspace, resolve(realpathSync(workspace), s)) && within(workspace, resolve(workspace, s));', " return within(workspace, resolve(realpathSync(workspace), s));"), + "MF-either": ("packages/harness/src/gate.mjs", ' return within(workspace, resolve(realpathSync(workspace), s)) && within(workspace, resolve(workspace, s));', ' return within(workspace, resolve(realpathSync(workspace), s)) || within(workspace, resolve(workspace, s));'), + "MG-inside-no-normalise": ("packages/harness/src/gate.mjs", "const s = normalise(p);\n if (isAbsolute(s)) return within", "const s = p;\n if (isAbsolute(s)) return within"), +} +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/r4/mut/run.sh b/agents/darkwing/work/s6-review/r4/mut/run.sh new file mode 100755 index 00000000..a999fe1d --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r41d/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/r4/mut/summary.txt b/agents/darkwing/work/s6-review/r4/mut/summary.txt new file mode 100644 index 00000000..da7d853f --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/mut/summary.txt @@ -0,0 +1,14 @@ +Ml-gate-pattern (packages/harness/src/gate.mjs): harness rc 1 pass 55 fail 1; +Ms-follow-walk (packages/harness/src/gate.mjs): harness rc 1 pass 53 fail 3; +Mw-ampm-case (packages/harness/src/gate.mjs): harness rc 0 pass 56 fail 0; +Mx-curly-once (packages/harness/src/gate.mjs): harness rc 0 pass 56 fail 0; +My-lstat-error-skips (packages/harness/src/gate.mjs): harness rc 0 pass 56 fail 0; +Mz-spell-no-normalise (packages/harness/src/gate.mjs): harness rc 0 pass 56 fail 0; +MA-pi-only (packages/harness/src/gate.mjs): harness rc 1 pass 55 fail 1; +MB-no-real-base (packages/harness/src/gate.mjs): harness rc 1 pass 55 fail 1; +MC-no-nfd-curly (packages/harness/src/gate.mjs): harness rc 1 pass 54 fail 2; +MD-given-only (packages/harness/src/gate.mjs): harness rc 1 pass 53 fail 3; +ME-real-only (packages/harness/src/gate.mjs): harness rc 1 pass 55 fail 1; +MF-either (packages/harness/src/gate.mjs): harness rc 1 pass 53 fail 3; +MG-inside-no-normalise (packages/harness/src/gate.mjs): harness rc 1 pass 55 fail 1; +DONE diff --git a/agents/darkwing/work/s6-review/r4/out/node-bus.txt b/agents/darkwing/work/s6-review/r4/out/node-bus.txt new file mode 100644 index 00000000..9072c079 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/node-bus.txt @@ -0,0 +1,82 @@ +✔ launch identity is stamped, payload identity is refused and stale holder cannot send (141.265652ms) +✔ decision classes route from policy; gated resolution is human-only, choice and target must match (256.147039ms) +✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (246.534686ms) +✔ launch events require a human CLI capability; generic emit cannot forge authority events (145.552624ms) +✔ within-role decisions close atomically and invalid options or blocking omissions refuse (142.905147ms) +✔ observer capabilities read human inbox but cannot mutate or forge launch identity (126.014714ms) +✔ task action subjects and linked decision trail are complete and ordered (124.901599ms) +✔ launch binding is durable and reconnecting requires the identical trusted record (82.577206ms) +✔ business isolation includes inherited object names and cross-business message references (158.98533ms) +✔ authority never transfers between action, run, target, unresolved or replaced role holder (214.172783ms) +✔ task projection uses schema current view, skipping earlier and equal-start polls (115.636835ms) +✔ revocation permanently bars the old run from reclaiming first, including after broker restart (155.986702ms) +✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (100.211715ms) +✔ both arbiters require human resolution when their cross-role route is themselves (170.699063ms) +✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.448913ms) +✔ only validated broker references load; returned data and exceptions cannot expose a known token (5.338933ms) +✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (3.616005ms) +✔ expiry refuses use and env references never become client data (0.835552ms) +✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.509074ms) +✔ opaque tokens shorter than 16 characters refuse before use (0.363752ms) +✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (112.749681ms) +✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (132.696513ms) +✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (148.142442ms) +✔ endLaunch leaves a claim another run took alone (158.162757ms) +✔ refuse records action.refused against the caller with the code only (124.764072ms) +✔ launches off refuses role.launch with launch-revoked until launches on (163.377777ms) +✔ broker process: launch ops authorize role.launch, record refusals and end runs (222.275652ms) +✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.812658ms) +✔ process reader gets own kernel identity without exposing environment values (2.022907ms) +✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.21422ms) +✔ real pid 1 remains inspectable when its environment is protected (0.432171ms) +✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (175.291927ms) +✔ missing and foreign-business citations refuse and roll back message and grant (174.715701ms) +✔ cross-role sends still need a matching resolved decision and consume it once (235.165957ms) +✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (180.878894ms) +✔ startup token refusal returns safe code without value or partial listening broker (39.45217ms) +✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (171.063865ms) +✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (164.432592ms) +✔ trusted host registers later launches; socket clients never have a registration verb (159.992085ms) +✔ runtime excludes declared project roots even when host supplies no repoRoots (46.259516ms) +✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (143.94251ms) +✔ v3b prototype refusals, views and append-only mutations (942.561092ms) +✔ gated approval authorizes once, survives store reopen, and fresh approval works (234.75139ms) +✔ another run cannot consume an approval; a failed check leaves it usable (205.88139ms) +✔ two scheduled callers have exactly one grant and one consumed refusal (157.200154ms) +✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (270.865391ms) +✔ class drift gated to cross-role refuses before consumption (176.065011ms) +✔ class drift cross-role to gated refuses before consumption (169.649872ms) +✔ class drift gated to within-role refuses before consumption (169.582233ms) +✔ class drift cross-role to within-role refuses before consumption (164.357356ms) +✔ class drift within-role to gated refuses before consumption (143.054783ms) +✔ class drift within-role to cross-role refuses before consumption (136.84595ms) +✔ message.send consumes approval and prevents a later send or authorize (163.811431ms) +✔ role.revoke consumes approval and prevents a later revoke or authorize (184.763789ms) +✔ creates private WAL store and excludes a second writer until explicit close (92.384573ms) +✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (170.88004ms) +✔ existing empty database and symlink runtime directory refuse, never initialize over damage (167.724067ms) +✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (156.6242ms) +✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (100.089282ms) +✔ async transactions refuse before invoking their function (84.719282ms) +✔ recordTask keeps sync reads and a role write apart (164.288089ms) +✔ read_at must be one canonical UTC format, so the projection compares strings safely (102.184227ms) +✔ a bad entry refuses the whole record (99.082849ms) +✔ taskView reads the projection for one business (114.742986ms) +✔ requestTask hands only a holder and a task verb to the handler, and records refusals (230.908121ms) +✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (381.414015ms) +✔ without an adapter the server refuses every task verb (165.28934ms) +✔ the runtime refuses an invalid adapter and closes a valid one (178.819415ms) +✔ the process loads the S3 adapter from plain-data trackers (213.587599ms) +✔ socket capability stamps launch identity; shared views use wire, no SQL client (132.432256ms) +✔ two wire claims serialize; a lost reply never automatically retries (171.083375ms) +✔ malformed, oversized and identity-forging envelopes refuse without echoing input (104.965622ms) +✔ client preserves UTF-8 when a response divides a multibyte character (11.948306ms) +✔ committed mutation followed by dropped reply reports unknown and is never retried (136.629406ms) +ℹ tests 74 +ℹ suites 0 +ℹ pass 74 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 2281.398677 diff --git a/agents/darkwing/work/s6-review/r4/out/node-business.txt b/agents/darkwing/work/s6-review/r4/out/node-business.txt new file mode 100644 index 00000000..e169ecdd --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/node-business.txt @@ -0,0 +1,68 @@ +✔ config directory and file path follow MOSAIC_CONFIG (1.553587ms) +✔ the fixture business validates and comes back frozen (6.315213ms) +✔ two instances may share a definition (2.077018ms) +✔ top-level refusals (6.265561ms) +✔ arbiters and projects (7.887733ms) +✔ role instances (3.085341ms) +✔ Vikunja bots (8.808245ms) +✔ a role without Vikunja takes no tracker block (2.60627ms) +✔ credential references match the definition's services (3.647953ms) +✔ launch (11.703973ms) +✔ loadBusiness: file checks (2.04872ms) +✔ loadBusiness: not a regular file (49.944343ms) +✔ loading writes nothing (1.251757ms) +✔ names that are Object.prototype properties don't count as declared (5.435482ms) +✔ the shipped example refuses as written and validates once filled in (0.601228ms) +✔ usage errors exit 4 (315.229357ms) +✔ validate: a good business exits 0 and prints instance digests (73.51181ms) +✔ validate: project files (351.75333ms) +✔ validate: missing files and a broken system config (253.645234ms) +✔ validate: credential reference problems exit 2 and name each one (64.627904ms) +✔ validate: a token file inside the repository is refused (67.852495ms) +✔ validate: role definitions come from MOSAIC_ROLES_DIR (207.234442ms) +✔ resolve: prints one instance's record (222.046246ms) +✔ resolve: refusals (430.638885ms) +✔ parse: exactly one of file or env, plus the service's date (3.669091ms) +✔ check: a good file has no problems (1.129649ms) +✔ check never opens the file: a write-only token passes (0.483008ms) +✔ check: file problems (1.23747ms) +✔ check: token files can't live in the repository or dataRoot, even through a linked directory (1.274092ms) +✔ check: dates and environment references (0.577429ms) +✔ path and load (3.182412ms) +✔ refusals (1.88194ms) +✔ systemVars flattens the validated config (2.833551ms) +✔ precedence: system, business, project, project role, agent (7.176254ms) +✔ limits narrow the definition and never widen it (3.531924ms) +✔ role.launch stays within-role only for the instance the launch block names (6.071093ms) +✔ limits.authority without role.launch leaves the launcher with no launch block (1.975976ms) +✔ limits.authority narrows cross-role actions too (1.058494ms) +✔ classify (1.218652ms) +✔ the record carries what the broker and launcher need (0.928912ms) +✔ digest: key order doesn't matter, any value change does (6.44603ms) +✔ refusals (2.922372ms) +✔ the four shipped version 2 roles load (4.591684ms) +✔ shipped role scopes match addendum B section 2 and the SR runbook (1.653387ms) +✔ shipped authority follows the note's table (0.810481ms) +✔ version 1 files keep loading with no authority (1.311308ms) +✔ the conductor policy isn't a role (0.295062ms) +✔ a missing role file is exit 4, a symbolic link too (0.539465ms) +✔ version 2 refusals (1.857376ms) +✔ authority: closed vocabulary, no gated-only action, no overlap (3.343617ms) +✔ credentials: Gitea scopes (1.283982ms) +✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.767491ms) +✔ credentials: services (0.909478ms) +✔ contract: a non-empty regular Markdown file beside the role file (1.29829ms) +✔ every key names known layers and a merge rule (1.03559ms) +✔ unknown keys and wrong layers refuse (0.899343ms) +✔ types (2.135088ms) +✔ merge: defaults, then the most specific layer wins (0.34324ms) +✔ merge: limits only narrow, and provenance lists each source (0.43299ms) +✔ merge doesn't change its inputs (0.16448ms) +ℹ tests 60 +ℹ suites 0 +ℹ pass 60 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 2061.893204 diff --git a/agents/darkwing/work/s6-review/r4/out/node-cli.txt b/agents/darkwing/work/s6-review/r4/out/node-cli.txt new file mode 100644 index 00000000..c94d4ce6 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/node-cli.txt @@ -0,0 +1,93 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (108.493399ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (124.592277ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (108.130943ms) +✔ decide prints a declining choice as declining (103.347326ms) +✔ an unknown outcome is reported once and never resent (77.235557ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (72.170582ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (86.223951ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (54.324436ms) +✔ every human command refuses inside an agent run before it touches the bus (68.666738ms) +✔ usage errors exit 4; no business and no host is a usage error (58.485534ms) +✔ agents and tasks print through the broker (82.026326ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.255183ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (40.98501ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (31.673433ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (31.44595ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (29.305545ms) +✔ a business without tracker.baseUrl gets no trackers entry (28.189137ms) +✔ an unknown business and a broken system config refuse with exit 3 (63.026964ms) +✔ empty views say so (0.654093ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.815179ms) +✔ tasks print the tracker fields the snapshot carries (0.133138ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (879.576381ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (199.796083ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (103.058991ms) +✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1115.845944ms) +✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (263.139734ms) +✔ a broker reply with no request waiting breaks the channel and the host exits 1 (139.535284ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (160.307462ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (177.42429ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (102.02552ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (142.149473ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (114.204878ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (158.924375ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (23.419602ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.500881ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (214.80008ms) +✔ bus start refuses with exit 3 without a notifier config (93.202427ms) +✔ bus start runs until bus stop; status reports it while it runs (658.007157ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.468007ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1251.061453ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (593.182486ms) +✔ a runner that stops at once ends its launch with the runner's reason (193.38297ms) +✔ 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 (701.514915ms) +✔ 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 (3577.193237ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (103.004381ms) +✔ an over-long launch request is refused at once, not at the 10 s idle timeout (114.206499ms) +✔ a launch client that never closes its side doesn't hold the host's close (115.389874ms) +✔ a runner that ignores SIGTERM is killed when the host closes (1228.912194ms) +✔ zoned uses the IANA zone across DST (23.609043ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (106.289446ms) +✔ two blocking decisions get two DMs with different nonces (116.570341ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.220875ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (102.121619ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (94.352037ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (94.506708ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (92.56958ms) +✔ 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.592641ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (110.682111ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (95.64294ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (58.221098ms) +✔ an inbox read failure is logged and the next poll retries (0.602797ms) +✔ no Discord id reaches the journal or the log (56.407946ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (10.479439ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (26.194974ms) +✔ the journal: a whole file that is one torn line truncates to empty (10.314475ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.535884ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.564367ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.75454ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.306269ms) +✔ 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.457156ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.377302ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.426368ms) +✔ digest content stays within Discord's 2000 characters (0.267179ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.853715ms) +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.731428ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (37.40083ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2140.922764ms) +✔ busExit and refuseInsideAgent (0.406241ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (194.330924ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1126.296746ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (58.771617ms) +✔ launches off and on go to the broker and change the business's launch state (72.968752ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (69.017531ms) +ℹ tests 83 +ℹ suites 0 +ℹ pass 83 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 7952.992063 diff --git a/agents/darkwing/work/s6-review/r4/out/node-harness.txt b/agents/darkwing/work/s6-review/r4/out/node-harness.txt new file mode 100644 index 00000000..e68afa87 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/node-harness.txt @@ -0,0 +1,64 @@ +✔ sessionModel: agent vars win, then the system's execution settings (12.174642ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (5.164613ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.418064ms) +✔ a bundle is written once: an existing file refuses (1.722343ms) +✔ a path with a single quote can't go into the hook command (1.658098ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (119.743025ms) +✔ a missing or wrong policy, or a bad event, exits 2 (77.766471ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1086.900813ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (733.236148ms) +✔ claude: the hook alone blocks a path outside the workspace (545.313347ms) +✔ claude: a second turn resumes the first turn's session (734.516306ms) +✔ claude adapter: --restricted is always passed (5.339234ms) +✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (736.377038ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.585714ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.375545ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.881108ms) +✔ file tool paths must resolve inside the workspace (1.118282ms) +✔ pi's own path normalisation can't be used to step out (0.80142ms) +✔ a symlink inside the workspace that points out is outside (0.842261ms) +✔ a dangling symlink is refused at any depth, in both harnesses (2.728819ms) +✔ read is checked under every spelling pi's read would open, in both harnesses (13.971212ms) +✔ other spellings cover directories, dangling links and pi's cwd (1.336928ms) +✔ a relative path climbs from the workspace's real path, in both harnesses (4.249754ms) +✔ claude path fields per tool (0.778025ms) +✔ glob patterns stay inside the workspace (0.582081ms) +✔ a path that can't be checked is blocked (0.389253ms) +✔ initialize, ping and tools/list (41.590382ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (29.687455ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (27.839299ms) +✔ a missing argument is a usage error (29.511723ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (342.258848ms) +✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (329.954183ms) +✔ pi: a read is refused when pi would open another spelling outside (391.03747ms) +✔ pi: a relative path climbs from the real path of a workspace behind a symlink (337.070562ms) +✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (310.591766ms) +✔ pi: a missing extension refuses before any model call (6.795663ms) +✔ pi: an extension without its configuration fails pi's start (273.888723ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.401858ms) +✔ turnRequest names the sender, class, reply and decision (0.245937ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (242.13304ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (96.795615ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (399.58747ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1379.730931ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (168.713214ms) +✔ founder credentials stop before the claim (20) (165.371255ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (189.675151ms) +✔ the launch ending under a running session exits 22 (142.691479ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (253.317151ms) +✔ a broker that is down at the claim exits 23, not 21 (95.220818ms) +✔ no capability, or a malformed one, on stdin exits 2 (197.689545ms) +✔ a missing or malformed policy exits 2 before the claim (121.8547ms) +✔ the PM gets launch, its task verbs and the reads (7.064528ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (1.962884ms) +✔ launch only when the business's launch block names the instance as launcher (2.011183ms) +✔ an action outside the instance's authority has no tool (2.153315ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.350813ms) +ℹ tests 56 +ℹ suites 0 +ℹ pass 56 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10413.058302 diff --git a/agents/darkwing/work/s6-review/r4/out/node-seat.txt b/agents/darkwing/work/s6-review/r4/out/node-seat.txt new file mode 100644 index 00000000..ed3b41ac --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/node-seat.txt @@ -0,0 +1,35 @@ +✔ resolveSeat: by name under --repo resolves the repo layout (1.224063ms) +✔ resolveSeat: by path resolves the fleet layout (0.339743ms) +✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.674543ms) +✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.647433ms) +✔ makeRegistration produces a record that validates; each shape violation throws SeatError (0.780124ms) +✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (1.232878ms) +✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (1.122885ms) +✔ CLI launch: registers, execs the fake launch script, and passes args through (32.595514ms) +✔ CLI launch: --harness lands in the record (28.937557ms) +✔ CLI launch: the launch script's own exit code passes through (29.237677ms) +✔ CLI launch: relaunching a seat rewrites the one registration record (58.156763ms) +✔ CLI launch: omitting --task records an empty string, not null (29.421817ms) +✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (81.737995ms) +✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (128.631197ms) +✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.465785ms) +✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (0.653742ms) +✔ 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.7817ms) +✔ 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 (7.655627ms) +✔ 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 (271.310362ms) +✔ family: exactly one launch.max key in the model name, else null (0.609701ms) +✔ sessionEnv passes only the allowlist, the repo's bin on PATH, and the run id (0.657394ms) +✔ newRun: short ids, 0700 directories, and a refusal when the socket path won't fit (1.481122ms) +✔ session file and launch log: 0600, the session file written once (0.945966ms) +✔ endReason maps the runner's exit codes; a signal is killed (0.10512ms) +✔ Registry mirrors to sessions.json; readSessions marks live entries; bad files refuse (1.293113ms) +✔ stopSession refuses an unknown run, reports a stale one, and won't signal a pid that isn't a runner (1.029557ms) +✔ a session runs under unshare as pid 1 of its namespace, claims, answers, and stops on mosaic stop (248.082544ms) +ℹ tests 27 +ℹ suites 0 +ℹ pass 27 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 729.094244 diff --git a/agents/darkwing/work/s6-review/r4/out/probe-claude-abs-dotdot.txt b/agents/darkwing/work/s6-review/r4/out/probe-claude-abs-dotdot.txt new file mode 100644 index 00000000..5a2ae6e7 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/probe-claude-abs-dotdot.txt @@ -0,0 +1,12 @@ +== MODE=absread given workspace /data/ws, real /deep/store/ws +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 /deep/store/ws."}] +outside after the run: secret.txt +workspace after the run: +== MODE=abswrite given workspace /data/ws, real /deep/store/ws +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":false,"content":"File created successfully at: /data/ws/../../data/ws/planted.txt (file state is current in your context — no need to Read it back)"}] +outside after the run: secret.txt +workspace after the run: planted.txt diff --git a/agents/darkwing/work/s6-review/r4/out/probe-claude-cwd-dotdot.txt b/agents/darkwing/work/s6-review/r4/out/probe-claude-cwd-dotdot.txt new file mode 100644 index 00000000..d3bf59e2 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/probe-claude-cwd-dotdot.txt @@ -0,0 +1,12 @@ +== MODE=read given workspace /data/ws, real /deep/store/ws +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/r41d/wt/packages/harness/src/claude-gate.mjs /bundle/policy.json || exit 2]: mosaic gate: Read path is outside the workspace: /deep/data/ws/secret.txt\n"}] +outside after the run: secret.txt +workspace after the run: +== MODE=write given workspace /data/ws, real /deep/store/ws +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/r41d/wt/packages/harness/src/claude-gate.mjs /bundle/policy.json || exit 2]: mosaic gate: Write path is outside the workspace: /deep/data/ws/planted.txt\n"}] +outside after the run: secret.txt +workspace after the run: diff --git a/agents/darkwing/work/s6-review/r4/out/probe-pi-abs-dotdot.txt b/agents/darkwing/work/s6-review/r4/out/probe-pi-abs-dotdot.txt new file mode 100644 index 00000000..0557fe81 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/probe-pi-abs-dotdot.txt @@ -0,0 +1,12 @@ +== MODE=absread given workspace /data/ws, real /deep/store/ws +gate decide: {"allow":true} +exit 0; stderr: +tool_result sent back: [{"is_error":true,"content":"ENOENT: no such file or directory, access '/data/ws/secret.txt'"}] +outside after the run: secret.txt +workspace after the run: +== MODE=abswrite given workspace /data/ws, real /deep/store/ws +gate decide: {"allow":true} +exit 0; stderr: +tool_result sent back: [{"is_error":false,"content":"Successfully wrote to /data/ws/../../data/ws/planted.txt"}] +outside after the run: secret.txt +workspace after the run: planted.txt diff --git a/agents/darkwing/work/s6-review/r4/out/probe-pi-cwd-dotdot.txt b/agents/darkwing/work/s6-review/r4/out/probe-pi-cwd-dotdot.txt new file mode 100644 index 00000000..1bf79886 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/probe-pi-cwd-dotdot.txt @@ -0,0 +1,12 @@ +== MODE=read given workspace /data/ws, real /deep/store/ws +gate decide: {"allow":false,"reason":"mosaic gate: read path is outside the workspace: ../../data/ws/secret.txt"} +exit 0; stderr: +tool_result sent back: [{"is_error":true,"content":"mosaic gate: read path is outside the workspace: ../../data/ws/secret.txt"}] +outside after the run: secret.txt +workspace after the run: +== MODE=write given workspace /data/ws, real /deep/store/ws +gate decide: {"allow":false,"reason":"mosaic gate: write path is outside the workspace: ../../data/ws/planted.txt"} +exit 0; stderr: +tool_result sent back: [{"is_error":true,"content":"mosaic gate: write path is outside the workspace: ../../data/ws/planted.txt"}] +outside after the run: secret.txt +workspace after the run: diff --git a/agents/darkwing/work/s6-review/r4/out/probe-pi-variant.txt b/agents/darkwing/work/s6-review/r4/out/probe-pi-variant.txt new file mode 100644 index 00000000..d01d99b9 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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":false,"reason":"mosaic gate: read path is outside the workspace under another spelling: notes's.txt -> \"/home/jwoltje/darkwing-scratch/tmp/r41-pvariant-NS5I/ws/notes’s.txt\""} +exit 0; stderr: +tool_result sent back: [{"is_error":true,"content":"mosaic gate: read path is outside the workspace under another spelling: notes's.txt -> \"/home/jwoltje/darkwing-scratch/tmp/r41-pvariant-NS5I/ws/notes’s.txt\""}] +== MODE=ampm asked s h o t 9 . 4 1 A M . p n g +gate decide: {"allow":false,"reason":"mosaic gate: read path is outside the workspace under another spelling: shot 9.41 AM.png -> \"/home/jwoltje/darkwing-scratch/tmp/r41-pvariant-cSjW/ws/shot 9.41 AM.png\""} +exit 0; stderr: +tool_result sent back: [{"is_error":true,"content":"mosaic gate: read path is outside the workspace under another spelling: shot 9.41 AM.png -> \"/home/jwoltje/darkwing-scratch/tmp/r41-pvariant-cSjW/ws/shot 9.41 AM.png\""}] +== MODE=nfd asked r 303 251 s u m 303 251 . t x t +gate decide: {"allow":false,"reason":"mosaic gate: read path is outside the workspace under another spelling: résumé.txt -> \"/home/jwoltje/darkwing-scratch/tmp/r41-pvariant-ffre/ws/résumé.txt\""} +exit 0; stderr: +tool_result sent back: [{"is_error":true,"content":"mosaic gate: read path is outside the workspace under another spelling: résumé.txt -> \"/home/jwoltje/darkwing-scratch/tmp/r41-pvariant-ffre/ws/résumé.txt\""}] diff --git a/agents/darkwing/work/s6-review/r4/out/probe-spell-edges.txt b/agents/darkwing/work/s6-review/r4/out/probe-spell-edges.txt new file mode 100644 index 00000000..a17d4eab --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/probe-spell-edges.txt @@ -0,0 +1,56 @@ +ok 1 lowercase am [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: shot 9.41 am.png -> "/shot 9.41 am) + pi would open "/shot 9.41 am.png" +ok 2 two apostrophes [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: a'b'c.txt -> "/a’b’c.txt") + pi would open "/a’b’c.txt" +ok 3 @ prefix [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: @notes's.txt -> "/notes’s.txt") + pi would open "/notes’s.txt" +ok 4 NBSP before AM in the asked name [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: shot 9.41 AM.png -> "/shot 9.41 AM) + pi would open "/shot 9.41 AM.png" +ok 5 file:// URL [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: file:///notes's.txt -> "/home/jwol) + pi would open "/notes’s.txt" +ok 6 file:// URL, %27 [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: file:///notes%27s.txt -> "/home/jw) + pi would open "/notes’s.txt" +ok 7 respelled dir inside, link out below it [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: dir's/x -> "/dir’s/x") + pi would open "/dir’s/x" +ok 8 respelled dir inside, plain file [pi]: allow + pi would open "/dir’s/x" +ok 9 respelled self-loop [pi]: refuse (mosaic gate: read path can't be checked under another spelling: ELOOP) + pi would open "/loop's" +ok 10 path below a respelled self-loop [pi]: refuse (mosaic gate: read path can't be checked under another spelling: ELOOP) + pi would open "/loop's/x" +ok 11 asked name exists inside, other spelling out [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: notes's.txt -> "/notes’s.txt") + pi would open "/notes's.txt" +ok 12 all families in one name, only AM/PM+NFD+curly on disk [pi]: allow + pi would open "/it's résumé 9.41 PM.txt" +ok 13 NFD+curly with a plain PM [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: it's résumé 9.41 PM.txt -> "/it’s ) + pi would open "/it’s résumé 9.41 PM.txt" +ok 14 ../ws/ form [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: ../ws/x's.txt -> "/x’s.txt") + pi would open "/x’s.txt" +ok 1 lowercase am [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: shot 9.41 am.png -> "/shot 9.41 am) + pi would open "/shot 9.41 am.png" +ok 2 two apostrophes [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: a'b'c.txt -> "/a’b’c.txt") + pi would open "/a’b’c.txt" +ok 3 @ prefix [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: @notes's.txt -> "/notes’s.txt") + pi would open "/notes’s.txt" +ok 4 NBSP before AM in the asked name [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: shot 9.41 AM.png -> "/shot 9.41 AM) + pi would open "/shot 9.41 AM.png" +ok 5 file:// URL [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: file:///notes's.txt -> "/home/jwol) + pi would open "/notes’s.txt" +ok 6 file:// URL, %27 [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: file:///notes%27s.txt -> "/home/jw) + pi would open "/notes’s.txt" +ok 7 respelled dir inside, link out below it [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: dir's/x -> "/dir’s/x") + pi would open "/dir’s/x" +ok 8 respelled dir inside, plain file [claude-code]: allow + pi would open "/dir’s/x" +ok 9 respelled self-loop [claude-code]: refuse (mosaic gate: Read path can't be checked under another spelling: ELOOP) + pi would open "/loop's" +ok 10 path below a respelled self-loop [claude-code]: refuse (mosaic gate: Read path can't be checked under another spelling: ELOOP) + pi would open "/loop's/x" +ok 11 asked name exists inside, other spelling out [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: notes's.txt -> "/notes’s.txt") + pi would open "/notes's.txt" +ok 12 all families in one name, only AM/PM+NFD+curly on disk [claude-code]: allow + pi would open "/it's résumé 9.41 PM.txt" +ok 13 NFD+curly with a plain PM [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: it's résumé 9.41 PM.txt -> "/it’s ) + pi would open "/it’s résumé 9.41 PM.txt" +ok 14 ../ws/ form [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: ../ws/x's.txt -> "/x’s.txt") + pi would open "/x’s.txt" diff --git a/agents/darkwing/work/s6-review/r4/out/summary.txt b/agents/darkwing/work/s6-review/r4/out/summary.txt new file mode 100644 index 00000000..d731cb80 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/summary.txt @@ -0,0 +1,15 @@ +node-harness exit=0 ℹ pass 56 ℹ fail 0 +node-seat exit=0 ℹ pass 27 ℹ fail 0 +node-cli exit=0 ℹ pass 83 ℹ 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 +DONE diff --git a/agents/darkwing/work/s6-review/r4/out/test-auth.txt b/agents/darkwing/work/s6-review/r4/out/test-auth.txt new file mode 100644 index 00000000..ac228760 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r4/out/test-conductor.txt b/agents/darkwing/work/s6-review/r4/out/test-conductor.txt new file mode 100644 index 00000000..2f03a2f4 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/test-conductor.txt @@ -0,0 +1,55 @@ +Note: switching to '0a4c8f13b09b8882ea55f6061d26949330385ee8'. + +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 '0a4c8f13b09b8882ea55f6061d26949330385ee8'. + +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/r4/out/test-config.txt b/agents/darkwing/work/s6-review/r4/out/test-config.txt new file mode 100644 index 00000000..76c05ef7 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r4/out/test-discord.txt b/agents/darkwing/work/s6-review/r4/out/test-discord.txt new file mode 100644 index 00000000..b013f9ff --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r4/out/test-extension-package.txt b/agents/darkwing/work/s6-review/r4/out/test-extension-package.txt new file mode 100644 index 00000000..02141c11 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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: 2927964 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/r4/out/test-foundation.txt b/agents/darkwing/work/s6-review/r4/out/test-foundation.txt new file mode 100644 index 00000000..6338e1e6 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r4/out/test-queue.txt b/agents/darkwing/work/s6-review/r4/out/test-queue.txt new file mode 100644 index 00000000..9cc88f45 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r41d/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/r4/out/test-release-docker.txt b/agents/darkwing/work/s6-review/r4/out/test-release-docker.txt new file mode 100644 index 00000000..7202b85d --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/out/test-release-docker.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/r4/out/test-release.txt b/agents/darkwing/work/s6-review/r4/out/test-release.txt new file mode 100644 index 00000000..1d6ed050 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r4/out/test-task.txt b/agents/darkwing/work/s6-review/r4/out/test-task.txt new file mode 100644 index 00000000..85aa46f0 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r4/probe/claude-abs-dotdot.sh b/agents/darkwing/work/s6-review/r4/probe/claude-abs-dotdot.sh new file mode 100755 index 00000000..7222ef8f --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/probe/claude-abs-dotdot.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# pi-cwd-dotdot.sh through a real Claude Code session: the same layout +# ($R/data -> $R/deep/store, workspace given as $R/data/ws), the candidate +# adapter, a settings.json in the bundle's form running the real +# claude-gate.mjs, and a mock API whose first answer calls Read or Write on +# "../../data/ws/X". Dummy key, 127.0.0.1 only. MODE is read or write. +# Round 4 addition: absread/abswrite give the absolute path +# "/data/ws/../../data/ws/X". Lexically that is the workspace; walked +# through the link it is /deep/data/ws/X, outside. +set -eu +WT=$1; OUT=$2 +R=$(mktemp -d "$TMPDIR/r41-ccwd-XXXX") +mkdir -p "$R/home" "$R/sess" "$R/bundle" "$R/deep/store/ws" "$R/deep/data/ws" +ln -s "$R/deep/store" "$R/data" +echo "SECRET-OUTSIDE-THE-WORKSPACE" > "$R/deep/data/ws/secret.txt" +WS="$R/data/ws" +case "$MODE" in + read) TOOL='{"name":"Read","input":{"file_path":"../../data/ws/secret.txt"}}'; TOOLS=Read ;; + write) TOOL='{"name":"Write","input":{"file_path":"../../data/ws/planted.txt","content":"PLANTED\n"}}'; TOOLS=Write ;; + absread) TOOL="{\"name\":\"Read\",\"input\":{\"file_path\":\"$WS/../../data/ws/secret.txt\"}}"; TOOLS=Read ;; + abswrite) TOOL="{\"name\":\"Write\",\"input\":{\"file_path\":\"$WS/../../data/ws/planted.txt\",\"content\":\"PLANTED\\n\"}}"; TOOLS=Write ;; +esac +echo "== MODE=$MODE given workspace /data/ws, real /deep/store/ws" | tee -a "$OUT" +echo "the generated prompt" > "$R/bundle/prompt.md" +printf '{"harness":"claude-code","workspace":"%s","tools":["read","write"],"typed":[]}\n' "$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=$TOOL 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=$TOOLS \ + MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: read it" \ + MOSAIC_WORKSPACE="$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}))).split(process.argv[2]).join(""));' "$R/api.log" "$R")" + echo "outside after the run: $(cd "$R/deep/data/ws" && ls)" + echo "workspace after the run: $(cd "$R/deep/store/ws" && ls)" +} | tee -a "$OUT" +rm -rf "$R" diff --git a/agents/darkwing/work/s6-review/r4/probe/claude-cwd-dotdot.sh b/agents/darkwing/work/s6-review/r4/probe/claude-cwd-dotdot.sh new file mode 100755 index 00000000..e629c570 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/probe/claude-cwd-dotdot.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# pi-cwd-dotdot.sh through a real Claude Code session: the same layout +# ($R/data -> $R/deep/store, workspace given as $R/data/ws), the candidate +# adapter, a settings.json in the bundle's form running the real +# claude-gate.mjs, and a mock API whose first answer calls Read or Write on +# "../../data/ws/X". Dummy key, 127.0.0.1 only. MODE is read or write. +set -eu +WT=$1; OUT=$2 +R=$(mktemp -d "$TMPDIR/r41-ccwd-XXXX") +mkdir -p "$R/home" "$R/sess" "$R/bundle" "$R/deep/store/ws" "$R/deep/data/ws" +ln -s "$R/deep/store" "$R/data" +echo "SECRET-OUTSIDE-THE-WORKSPACE" > "$R/deep/data/ws/secret.txt" +WS="$R/data/ws" +case "$MODE" in + read) TOOL='{"name":"Read","input":{"file_path":"../../data/ws/secret.txt"}}'; TOOLS=Read ;; + write) TOOL='{"name":"Write","input":{"file_path":"../../data/ws/planted.txt","content":"PLANTED\n"}}'; TOOLS=Write ;; +esac +echo "== MODE=$MODE given workspace /data/ws, real /deep/store/ws" | tee -a "$OUT" +echo "the generated prompt" > "$R/bundle/prompt.md" +printf '{"harness":"claude-code","workspace":"%s","tools":["read","write"],"typed":[]}\n' "$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=$TOOL 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=$TOOLS \ + MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: read it" \ + MOSAIC_WORKSPACE="$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}))).split(process.argv[2]).join(""));' "$R/api.log" "$R")" + echo "outside after the run: $(cd "$R/deep/data/ws" && ls)" + echo "workspace after the run: $(cd "$R/deep/store/ws" && ls)" +} | tee -a "$OUT" +rm -rf "$R" diff --git a/agents/darkwing/work/s6-review/r4/probe/mock-api.mjs b/agents/darkwing/work/s6-review/r4/probe/mock-api.mjs new file mode 100644 index 00000000..4ae893af --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r4/probe/pi-abs-dotdot.sh b/agents/darkwing/work/s6-review/r4/probe/pi-abs-dotdot.sh new file mode 100755 index 00000000..c7677f7c --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/probe/pi-abs-dotdot.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# The primary check resolves a relative path against the workspace as +# given; Pi resolves it against its cwd, which is the workspace's real path +# (the adapter cds into it and process.cwd() is getcwd). When the given path +# runs through a link whose target sits elsewhere, ".." walks up different +# directories. Layout: +# $R/data -> $R/deep/store (the link, like a symlinked dataRoot) +# $R/deep/store/ws the workspace's real path +# $R/deep/data/ws/secret.txt outside the workspace +# Given workspace $R/data/ws. "../../data/ws/X" is $R/data/ws/X to the +# gate (inside) and $R/deep/data/ws/X to Pi (outside). +# A real Pi session, the candidate adapter and pi-extension.mjs, a mock +# Messages API on 127.0.0.1 and a dummy key. MODE is read or write. +# Round 4 addition: absread/abswrite give the absolute path +# "/data/ws/../../data/ws/X". Lexically that is the workspace; walked +# through the link it is /deep/data/ws/X, outside. +set -eu +WT=$1; OUT=$2 +R=$(mktemp -d "$TMPDIR/r41-cwd-XXXX") +mkdir -p "$R/home" "$R/agent" "$R/bundle" "$R/deep/store/ws" "$R/deep/data/ws" +ln -s "$R/deep/store" "$R/data" +echo "SECRET-OUTSIDE-THE-WORKSPACE" > "$R/deep/data/ws/secret.txt" +WS="$R/data/ws" +case "$MODE" in + read) TOOL='{"name":"read","input":{"path":"../../data/ws/secret.txt"}}' ;; + write) TOOL='{"name":"write","input":{"path":"../../data/ws/planted.txt","content":"PLANTED\n"}}' ;; + absread) TOOL="{\"name\":\"read\",\"input\":{\"path\":\"$WS/../../data/ws/secret.txt\"}}" ;; + abswrite) TOOL="{\"name\":\"write\",\"input\":{\"path\":\"$WS/../../data/ws/planted.txt\",\"content\":\"PLANTED\\n\"}}" ;; +esac +echo "== MODE=$MODE given workspace /data/ws, real /deep/store/ws" | tee -a "$OUT" +echo "the generated prompt" > "$R/bundle/prompt.md" +printf '{"harness":"pi","workspace":"%s","tools":["read","write"],"typed":[]}\n' "$WS" > "$R/bundle/policy.json" +echo '[]' > "$R/bundle/tools.json" +TOOL_USE=$TOOL 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 --input-type=module -e ' + const { decide } = await import(process.env.WT + "/packages/harness/src/gate.mjs"); + const t = JSON.parse(process.argv[2]); + console.log(JSON.stringify(decide({harness:"pi",workspace:process.argv[1],tools:["read","write"],typed:[]},t.name,t.input)));' "$WS" "$TOOL")" | 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: do it" \ + MOSAIC_WORKSPACE="$WS" MOSAIC_TOOLS=read,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"); 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}))).split(process.argv[2]).join(""));' "$R/api.log" "$R")" + echo "outside after the run: $(cd "$R/deep/data/ws" && ls)" + echo "workspace after the run: $(cd "$R/deep/store/ws" && ls)" +} | tee -a "$OUT" +rm -rf "$R" diff --git a/agents/darkwing/work/s6-review/r4/probe/pi-cwd-dotdot.sh b/agents/darkwing/work/s6-review/r4/probe/pi-cwd-dotdot.sh new file mode 100755 index 00000000..0ee58694 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/probe/pi-cwd-dotdot.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# The primary check resolves a relative path against the workspace as +# given; Pi resolves it against its cwd, which is the workspace's real path +# (the adapter cds into it and process.cwd() is getcwd). When the given path +# runs through a link whose target sits elsewhere, ".." walks up different +# directories. Layout: +# $R/data -> $R/deep/store (the link, like a symlinked dataRoot) +# $R/deep/store/ws the workspace's real path +# $R/deep/data/ws/secret.txt outside the workspace +# Given workspace $R/data/ws. "../../data/ws/X" is $R/data/ws/X to the +# gate (inside) and $R/deep/data/ws/X to Pi (outside). +# A real Pi session, the candidate adapter and pi-extension.mjs, a mock +# Messages API on 127.0.0.1 and a dummy key. MODE is read or write. +set -eu +WT=$1; OUT=$2 +R=$(mktemp -d "$TMPDIR/r41-cwd-XXXX") +mkdir -p "$R/home" "$R/agent" "$R/bundle" "$R/deep/store/ws" "$R/deep/data/ws" +ln -s "$R/deep/store" "$R/data" +echo "SECRET-OUTSIDE-THE-WORKSPACE" > "$R/deep/data/ws/secret.txt" +WS="$R/data/ws" +case "$MODE" in + read) TOOL='{"name":"read","input":{"path":"../../data/ws/secret.txt"}}' ;; + write) TOOL='{"name":"write","input":{"path":"../../data/ws/planted.txt","content":"PLANTED\n"}}' ;; +esac +echo "== MODE=$MODE given workspace /data/ws, real /deep/store/ws" | tee -a "$OUT" +echo "the generated prompt" > "$R/bundle/prompt.md" +printf '{"harness":"pi","workspace":"%s","tools":["read","write"],"typed":[]}\n' "$WS" > "$R/bundle/policy.json" +echo '[]' > "$R/bundle/tools.json" +TOOL_USE=$TOOL 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 --input-type=module -e ' + const { decide } = await import(process.env.WT + "/packages/harness/src/gate.mjs"); + const t = JSON.parse(process.argv[2]); + console.log(JSON.stringify(decide({harness:"pi",workspace:process.argv[1],tools:["read","write"],typed:[]},t.name,t.input)));' "$WS" "$TOOL")" | 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: do it" \ + MOSAIC_WORKSPACE="$WS" MOSAIC_TOOLS=read,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"); 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}))).split(process.argv[2]).join(""));' "$R/api.log" "$R")" + echo "outside after the run: $(cd "$R/deep/data/ws" && ls)" + echo "workspace after the run: $(cd "$R/deep/store/ws" && ls)" +} | tee -a "$OUT" +rm -rf "$R" diff --git a/agents/darkwing/work/s6-review/r4/probe/pi-variant.sh b/agents/darkwing/work/s6-review/r4/probe/pi-variant.sh new file mode 100755 index 00000000..04cbafb6 --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/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/r4/probe/spell-edges.mjs b/agents/darkwing/work/s6-review/r4/probe/spell-edges.mjs new file mode 100644 index 00000000..4270dfda --- /dev/null +++ b/agents/darkwing/work/s6-review/r4/probe/spell-edges.mjs @@ -0,0 +1,51 @@ +// Round 3 edges of the R4 spelling check, on decide() alone. Each case +// builds a fresh workspace with one link (or directory) on disk under the +// spelling Pi's read would open, then asks for the plain spelling. Prints +// the decision and, for comparison, what Pi 0.85.1's own resolveReadPath +// would open (run with the workspace's real path as cwd). Imports from the +// review worktree by absolute path (WT). +import { mkdirSync, mkdtempSync, realpathSync, symlinkSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +const WT = process.env.WT; +const { decide } = await import(`${WT}/packages/harness/src/gate.mjs`); +const { resolveReadPath } = await import(`${WT}/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/path-utils.js`); + +function fresh() { + const dir = mkdtempSync(join(process.env.TMPDIR, "r41c-spell-")); + const ws = join(dir, "ws"); + mkdirSync(ws); + mkdirSync(join(dir, "outside")); + writeFileSync(join(dir, "outside", "secret.txt"), "s"); + writeFileSync(join(ws, "a.txt"), "a"); + return { dir, ws }; +} +function show(name, harness, ws, asked, expect) { + const tool = harness === "pi" ? "read" : "Read"; + const field = harness === "pi" ? "path" : "file_path"; + const d = decide({ harness, workspace: ws, tools: ["read"], typed: [] }, tool, { [field]: asked }); + let opens; + try { opens = resolveReadPath(asked, realpathSync(ws)); } catch (e) { opens = `throws ${e.code}`; } + const got = d.allow ? "allow" : "refuse"; + console.log(`${got === expect ? "ok " : "BAD "} ${name} [${harness}]: ${got}${d.reason ? ` (${d.reason.replace(ws, "").slice(0, 110)})` : ""}`); + console.log(` pi would open ${JSON.stringify(opens.replace?.(ws, "") ?? opens)}`); +} +const out = (dir) => join(dir, "outside", "secret.txt"); + +for (const harness of ["pi", "claude-code"]) { + { const { dir, ws } = fresh(); symlinkSync(out(dir), join(ws, "shot 9.41 am.png")); show("1 lowercase am", harness, ws, "shot 9.41 am.png", "refuse"); } + { const { dir, ws } = fresh(); symlinkSync(out(dir), join(ws, "a’b’c.txt")); show("2 two apostrophes", harness, ws, "a'b'c.txt", "refuse"); } + { const { dir, ws } = fresh(); symlinkSync(out(dir), join(ws, "notes’s.txt")); show("3 @ prefix", harness, ws, "@notes's.txt", "refuse"); } + { const { dir, ws } = fresh(); symlinkSync(out(dir), join(ws, "shot 9.41 AM.png")); show("4 NBSP before AM in the asked name", harness, ws, "shot 9.41 AM.png", "refuse"); } + { const { dir, ws } = fresh(); symlinkSync(out(dir), join(ws, "notes’s.txt")); show("5 file:// URL", harness, ws, pathToFileURL(join(ws, "notes's.txt")).href, "refuse"); } + { const { dir, ws } = fresh(); symlinkSync(out(dir), join(ws, "notes’s.txt")); show("6 file:// URL, %27", harness, ws, `file://${ws}/notes%27s.txt`, "refuse"); } + { const { dir, ws } = fresh(); mkdirSync(join(ws, "dir’s")); symlinkSync(out(dir), join(ws, "dir’s", "x")); show("7 respelled dir inside, link out below it", harness, ws, "dir's/x", "refuse"); } + { const { dir, ws } = fresh(); mkdirSync(join(ws, "dir’s")); writeFileSync(join(ws, "dir’s", "x"), "x"); show("8 respelled dir inside, plain file", harness, ws, "dir's/x", "allow"); } + { const { ws } = fresh(); symlinkSync("loop’s", join(ws, "loop’s")); show("9 respelled self-loop", harness, ws, "loop's", "refuse"); } + { const { ws } = fresh(); symlinkSync("loop’s", join(ws, "loop’s")); show("10 path below a respelled self-loop", harness, ws, "loop's/x", "refuse"); } + { const { dir, ws } = fresh(); writeFileSync(join(ws, "notes's.txt"), "n"); symlinkSync(out(dir), join(ws, "notes’s.txt")); show("11 asked name exists inside, other spelling out", harness, ws, "notes's.txt", "refuse"); } + { const { dir, ws } = fresh(); symlinkSync(out(dir), join(ws, "it’s résumé 9.41 PM.txt".normalize("NFD"))); show("12 all families in one name, only AM/PM+NFD+curly on disk", harness, ws, "it's résumé 9.41 PM.txt", "allow"); } + { const { dir, ws } = fresh(); symlinkSync(out(dir), join(ws, "it’s résumé 9.41 PM.txt".normalize("NFD"))); show("13 NFD+curly with a plain PM", harness, ws, "it's résumé 9.41 PM.txt", "refuse"); } + { const { dir, ws } = fresh(); symlinkSync(out(dir), join(ws, "x’s.txt")); show("14 ../ws/ form", harness, ws, "../ws/x's.txt", "refuse"); } +} +process.exit(0); diff --git a/agents/darkwing/work/s6-review/review-r4.md b/agents/darkwing/work/s6-review/review-r4.md new file mode 100644 index 00000000..900b65c4 --- /dev/null +++ b/agents/darkwing/work/s6-review/review-r4.md @@ -0,0 +1,175 @@ +# Row 41, slice 1 S6 (meta-harness and launching), round 4 review (Darkwing) + +Issue #1523, request comment 27033, queue revs 274 and 275 (`6f102777`). +Packet: `agents/filbert/work/s6/` at `55bff3b2`, base `915e00e5`, gate at +`0a4c8f13`, 42 files. Candidate manifest sha256 +`08a78972e316cb3b9cba2050489bd65b2c0b1ec95eb7de9b91c266e89a0d0d66`. +Four files changed since round 3: `gate.mjs`, the harness README, +`gate.test.mjs` and `pi-session.test.mjs`. Round 3: `review-r3.md`, +comment 27032. Sage ruled that round 4 fixes R5 only and that R1 to R4 +stay closed. + +Verdict: **approve**, comment 27034. R5 is fixed. A relative path now has to resolve +inside from the workspace's real path, which is where Pi resolves it, and +from the path as given. My round 3 probes refuse in both modes and both +harnesses, the new tests cover both symlink layouts with a real Pi +session, and every mutant of the new line is killed. Nothing outside +`insideWorkspace` and its tests changed. The four spelling survivors from +round 3 still survive, as expected, since Sage kept them out of this +round. They stay follow-ups. + +## Method + +- A detached worktree at `0a4c8f13`, then `git apply --index build.patch` + and `sha256sum -c candidate-manifest.sha256`: 42 OK. After the probes and + mutants I checked again: 42 OK (`r4/mut/manifest-after.txt`), and no test + or probe process was left running. +- I rebuilt round 3's candidate beside it and diffed the two trees + (`r4/interdiff.patch`, 4 files, +126/−6). I read `gate.mjs` around the + change and both new tests in full. +- I reread Pi 0.85.1's `resolvePath` (`dist/utils/paths.js:82-86`). A + relative path is `path.resolve(cwd, p)`, and an absolute one is + `path.resolve(p)`. Both are lexical. +- Probes are in `r4/probe/`. They import from my scratch worktree by + absolute path (`WT`). Model calls go to `r4/probe/mock-api.mjs` on + 127.0.0.1 with a dummy key, so nothing was spent. Claude Code is + 2.1.296, and Pi is 0.85.1 from the repository's `node_modules`. +- 13 mutants on `gate.mjs` (`r4/mut/mutate.py`, `run.sh`, `all.sh`), each + run on the harness suite with the file restored from a backup copy. Nine + are round 3's gate mutants and four are new on the R5 line. The other 20 + round 3 mutants change source and tests that round 4 doesn't touch, so I + didn't rerun them. + +Node is v26.8.1, `TMPDIR=~/darkwing-scratch/tmp`. `gate.sh` set +`DOCKER_HOST=unix:///nonexistent.sock`. + +## Suites + +| Suite | Result | +|---|---| +| harness | 56/0 | +| seat | 27/0 | +| cli | 83/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-release | 4/0 without Docker, 14/0 with it (`test-release-docker.txt`) | +| test-discord | 66/0 | +| test-task | 26/2 | + +Harness gains the three R5 tests over round 3's 53. The two `test-task` +failures are "user recall run succeeds" and "recalled user name", the live +worker check. It needs Docker and a paid model call, which I didn't make. +Filbert's base run fails the same two, and the follow-up for it is already +in BUILD.md. + +## R5: fixed + +`insideWorkspace` (`gate.mjs:76-80`) keeps the absolute branch and changes +the relative one: + +```js +return within(workspace, resolve(realpathSync(workspace), s)) && within(workspace, resolve(workspace, s)); +``` + +`within` walks each candidate with `real()`, so a dangling link on either +path still refuses (R3). `spellings()` already built from both bases in +round 3, so `read`'s other-spelling check needed nothing new. + +Checking both bases is stricter than Sage's ruling, which asked for the +real cwd. Real-only matches Pi exactly. Both-bases refuses everything +real-only refuses, plus paths that climb out of the given path and come +back in through the real one, such as `../../store/ws/x`. BUILD.md and the +README name that as a choice. I'd recommended both bases in round 3, and I +think the extra refusal costs nothing: the plain relative path or the +absolute one reaches the same file. + +My round 3 probes, unchanged except for `WT`: + +| Probe | Mode | Round 3 | Round 4 | +|---|---|---|---| +| Pi, `../../data/ws/X` | read | allowed, secret in the tool result | gate refuses, nothing read | +| Pi | write | allowed, file created outside | gate refuses, nothing written anywhere | +| Claude Code | read | hook refuses `/deep/data/ws/secret.txt` | same | +| Claude Code | write | hook refuses `/deep/data/ws/planted.txt` | same | + +Output: `r4/out/probe-pi-cwd-dotdot.txt` and `probe-claude-cwd-dotdot.txt`. + +I also checked the absolute form of the same trick, which round 3 didn't +cover: `/data/ws/../../data/ws/X`. Lexically that's the workspace, and +walked through the link it's `/deep/data/ws/X`, outside. If either +harness passed it to the kernel unnormalised, the gate's lexical +`resolve(s)` would allow a path that opens outside. Neither does. Pi's +`resolvePath` normalises it, and Claude Code opens the normalised path too. +In a real session the write landed in the workspace as `planted.txt` and +the read found nothing (`r4/probe/pi-abs-dotdot.sh`, +`claude-abs-dotdot.sh`; output in `r4/out/probe-*-abs-dotdot.txt`). No +finding. + +The tests: + +- `gate.test.mjs`, "a relative path climbs from the workspace's real path, + in both harnesses". It runs two layouts, a symlinked workspace and a + symlinked dataRoot in the launcher's `workspaces//` shape, each in + Pi and Claude Code. It asserts that the layout really splits the two + resolutions before it checks the gate, so a fixture mistake can't make + the test pass for nothing. The escape is refused for read and write, the + climb back in is allowed, and the stricter case is refused. +- `pi-session.test.mjs` runs a real Pi session per layout through the + adapter. The escape read and write come back as gate errors, the secret + isn't in stdout, and the outside directory holds only the planted file. + The inside read returns its content and the inside write lands. + +## R4 and earlier: unchanged + +`r4/probe/spell-edges.mjs` gives output identical to round 3, path +prefixes aside: 28 cases as expected. `pi-variant.sh` in a real Pi session +refuses all four modes (plain, quote, ampm, nfd), as in round 3. + +## Mutants + +| Mutant | Change | Harness | Result | +|---|---|---|---| +| MD | given path only (round 3's code) | 53/3 | killed: the gate test and both Pi sessions | +| ME | real path only | 55/1 | killed: the gate test's stricter case | +| MF | `\|\|` instead of `&&` | 53/3 | killed, same three as MD | +| MG | no `normalise` in `insideWorkspace` | 55/1 | killed: "pi's own path normalisation can't be used to step out" | +| Ml | the glob pattern `..` check off | 55/1 | killed | +| Ms | `real()` walks with `realpathSync`, not `lstat` | 53/3 | killed | +| MA | other-spelling check on Pi's `read` only | 55/1 | killed | +| MB | spellings from the given base only | 55/1 | killed | +| MC | no NFD-with-U+2019 spelling | 54/2 | killed | +| Mw | AM/PM regex without `i` | 56/0 | survives (round 3 follow-up) | +| Mx | `'` replaced once | 56/0 | survives (round 3 follow-up) | +| My | any `lstat` error skipped | 56/0 | survives (round 3 follow-up) | +| Mz | no `normalise` in `spellings()` | 56/0 | survives (round 3 follow-up) | + +MD and ME match Filbert's table (16/3 and 18/1 on the two files). +Per-mutant output: `r4/mut/-harness.txt`, summary in +`r4/mut/summary.txt`. + +## Notes (not blocking) + +1. The README says Claude Code makes "a path" absolute before the hook. I + saw it for `file_path` on `Read` and `Write`. I didn't check `Grep` and + `Glob`'s `path`. It doesn't matter for safety, because the gate now + checks a relative path from both bases whichever harness sends it. +2. The follow-ups in BUILD.md match what I asked for: Mw to Mz, a + `realpathSync` in the seat's `workspaceDir`, and the Ma leftover. With + R5 fixed in the gate, the `workspaceDir` change is hygiene, not a fix. + +## Files + +- `r4/candidate-manifest.sha256`, `r4/files.txt`: copies of Filbert's. +- `r4/interdiff.patch`: round 3 candidate to round 4 candidate. +- `r4/gate.sh`, `r4/out/`: suite runs and probe outputs. +- `r4/probe/`: round 3's probes, plus `pi-abs-dotdot.sh` and + `claude-abs-dotdot.sh` (round 3's with the `absread` and `abswrite` + modes). +- `r4/mut/`: mutant definitions, driver, outputs and the manifest check + after the runs.