docs(review): row 41 round 4 review packet, approve (darkwing)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
Executable
+14
@@ -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
|
||||
@@ -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 = {
|
||||
+ // <dir>/a/ws -> <dir>/deep/store/ws: up two is <dir> as given, <dir>/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 <dir>/data -> <dir>/deep/store, the workspace under it as the
|
||||
+ // launcher builds it: up four is <dir> as given, <dir>/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);
|
||||
@@ -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.<anonymous> (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'
|
||||
}
|
||||
@@ -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.<anonymous> (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'
|
||||
}
|
||||
@@ -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.<anonymous> (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.<anonymous> (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'
|
||||
}
|
||||
@@ -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.<anonymous> (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.<anonymous> (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.<anonymous> (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'
|
||||
}
|
||||
@@ -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.<anonymous> (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'
|
||||
}
|
||||
@@ -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.<anonymous> (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.<anonymous> (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.<anonymous> (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'
|
||||
}
|
||||
@@ -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.<anonymous> (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'
|
||||
}
|
||||
@@ -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.<anonymous> (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'
|
||||
}
|
||||
@@ -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.<anonymous> (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.<anonymous> (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.<anonymous> (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'
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+9
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,78 @@
|
||||
# mutate.py <file> <id>: 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)
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# run.sh <mutant> <pkg>...: 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,12 @@
|
||||
== MODE=absread given workspace <R>/data/ws, real <R>/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 <R>/deep/store/ws."}]
|
||||
outside after the run: secret.txt
|
||||
workspace after the run:
|
||||
== MODE=abswrite given workspace <R>/data/ws, real <R>/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: <R>/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
|
||||
@@ -0,0 +1,12 @@
|
||||
== MODE=read given workspace <R>/data/ws, real <R>/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 <R>/bundle/policy.json || exit 2]: mosaic gate: Read path is outside the workspace: <R>/deep/data/ws/secret.txt\n"}]
|
||||
outside after the run: secret.txt
|
||||
workspace after the run:
|
||||
== MODE=write given workspace <R>/data/ws, real <R>/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 <R>/bundle/policy.json || exit 2]: mosaic gate: Write path is outside the workspace: <R>/deep/data/ws/planted.txt\n"}]
|
||||
outside after the run: secret.txt
|
||||
workspace after the run:
|
||||
@@ -0,0 +1,12 @@
|
||||
== MODE=absread given workspace <R>/data/ws, real <R>/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 '<R>/data/ws/secret.txt'"}]
|
||||
outside after the run: secret.txt
|
||||
workspace after the run:
|
||||
== MODE=abswrite given workspace <R>/data/ws, real <R>/deep/store/ws
|
||||
gate decide: {"allow":true}
|
||||
exit 0; stderr:
|
||||
tool_result sent back: [{"is_error":false,"content":"Successfully wrote to <R>/data/ws/../../data/ws/planted.txt"}]
|
||||
outside after the run: secret.txt
|
||||
workspace after the run: planted.txt
|
||||
@@ -0,0 +1,12 @@
|
||||
== MODE=read given workspace <R>/data/ws, real <R>/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 <R>/data/ws, real <R>/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:
|
||||
@@ -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\""}]
|
||||
@@ -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 -> "<ws>/shot 9.41 am)
|
||||
pi would open "<ws>/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 -> "<ws>/a’b’c.txt")
|
||||
pi would open "<ws>/a’b’c.txt"
|
||||
ok 3 @ prefix [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: @notes's.txt -> "<ws>/notes’s.txt")
|
||||
pi would open "<ws>/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 -> "<ws>/shot 9.41 AM)
|
||||
pi would open "<ws>/shot 9.41 AM.png"
|
||||
ok 5 file:// URL [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: file://<ws>/notes's.txt -> "/home/jwol)
|
||||
pi would open "<ws>/notes’s.txt"
|
||||
ok 6 file:// URL, %27 [pi]: refuse (mosaic gate: read path is outside the workspace under another spelling: file://<ws>/notes%27s.txt -> "/home/jw)
|
||||
pi would open "<ws>/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 -> "<ws>/dir’s/x")
|
||||
pi would open "<ws>/dir’s/x"
|
||||
ok 8 respelled dir inside, plain file [pi]: allow
|
||||
pi would open "<ws>/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 "<ws>/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 "<ws>/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 -> "<ws>/notes’s.txt")
|
||||
pi would open "<ws>/notes's.txt"
|
||||
ok 12 all families in one name, only AM/PM+NFD+curly on disk [pi]: allow
|
||||
pi would open "<ws>/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 -> "<ws>/it’s )
|
||||
pi would open "<ws>/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 -> "<ws>/x’s.txt")
|
||||
pi would open "<ws>/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 -> "<ws>/shot 9.41 am)
|
||||
pi would open "<ws>/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 -> "<ws>/a’b’c.txt")
|
||||
pi would open "<ws>/a’b’c.txt"
|
||||
ok 3 @ prefix [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: @notes's.txt -> "<ws>/notes’s.txt")
|
||||
pi would open "<ws>/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 -> "<ws>/shot 9.41 AM)
|
||||
pi would open "<ws>/shot 9.41 AM.png"
|
||||
ok 5 file:// URL [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: file://<ws>/notes's.txt -> "/home/jwol)
|
||||
pi would open "<ws>/notes’s.txt"
|
||||
ok 6 file:// URL, %27 [claude-code]: refuse (mosaic gate: Read path is outside the workspace under another spelling: file://<ws>/notes%27s.txt -> "/home/jw)
|
||||
pi would open "<ws>/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 -> "<ws>/dir’s/x")
|
||||
pi would open "<ws>/dir’s/x"
|
||||
ok 8 respelled dir inside, plain file [claude-code]: allow
|
||||
pi would open "<ws>/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 "<ws>/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 "<ws>/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 -> "<ws>/notes’s.txt")
|
||||
pi would open "<ws>/notes's.txt"
|
||||
ok 12 all families in one name, only AM/PM+NFD+curly on disk [claude-code]: allow
|
||||
pi would open "<ws>/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 -> "<ws>/it’s )
|
||||
pi would open "<ws>/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 -> "<ws>/x’s.txt")
|
||||
pi would open "<ws>/x’s.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
|
||||
@@ -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
|
||||
@@ -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 <new-branch-name>
|
||||
|
||||
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 <new-branch-name>
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
# "<R>/data/ws/../../data/ws/X". Lexically that is the workspace; walked
|
||||
# through the link it is <R>/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 <R>/data/ws, real <R>/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>"));' "$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"
|
||||
@@ -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 <R>/data/ws, real <R>/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>"));' "$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"
|
||||
@@ -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));
|
||||
@@ -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
|
||||
# "<R>/data/ws/../../data/ws/X". Lexically that is the workspace; walked
|
||||
# through the link it is <R>/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 <R>/data/ws, real <R>/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>"));' "$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"
|
||||
@@ -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 <R>/data/ws, real <R>/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>"));' "$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"
|
||||
+46
@@ -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"
|
||||
@@ -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, "<ws>").slice(0, 110)})` : ""}`);
|
||||
console.log(` pi would open ${JSON.stringify(opens.replace?.(ws, "<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);
|
||||
@@ -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 `<R>/deep/data/ws/secret.txt` | same |
|
||||
| Claude Code | write | hook refuses `<R>/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: `<R>/data/ws/../../data/ws/X`. Lexically that's the workspace, and
|
||||
walked through the link it's `<R>/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/<b>/<i>` 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/<id>-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.
|
||||
Reference in New Issue
Block a user