import { mkdir, mkdtemp, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { beforeAll, describe, expect, it } from "vitest"; import { assertPublicUrl, createPolicyHook, isPrivateIp } from "./policy.js"; let ws: string; let outside: string; const denied: string[] = []; let hook: ReturnType; const call = (tool_name: string, tool_input: Record) => hook( { hook_event_name: "PreToolUse", tool_name, tool_input, tool_use_id: "t", session_id: "s", transcript_path: "", cwd: ws, } as never, "t", { signal: new AbortController().signal }, ); const isDeny = (r: unknown) => (r as { hookSpecificOutput?: { permissionDecision?: string } }).hookSpecificOutput?.permissionDecision === "deny"; beforeAll(async () => { ws = await mkdtemp(join(tmpdir(), "as-ws-")); outside = await mkdtemp(join(tmpdir(), "as-outside-")); await mkdir(join(ws, "results"), { recursive: true }); await writeFile(join(ws, "CLAUDE.md"), "brief"); await symlink(outside, join(ws, "results/link")); hook = createPolicyHook({ workspace: ws, frozenPaths: ["CLAUDE.md", "input", "provenance"], onDenied: (t, r) => denied.push(`${t}:${r}`), }); }); describe("platform policy hook", () => { it("denies privilege escalation, remote shells, system admin, LAN access and credential paths in Bash", async () => { for (const command of [ "sudo -n true", "ssh M1M32 uptime", "launchctl list", "pm2 ls", "brew install wget", "curl -s http://192.168.2.72:22", "cat /etc/autoscholar/worker.env", "cat ~/.ssh/id_ed25519", "ls /Users/simon-pierreboucher", "cat /var/autoscholar/state/worker-token", "echo $ANTHROPIC_API_KEY", "nc -zv 10.0.0.1 22", ]) { expect(isDeny(await call("Bash", { command })), command).toBe(true); } expect(isDeny(await call("Bash", { command: "ls", dangerouslyDisableSandbox: true }))).toBe(true); expect(isDeny(await call("Bash", { command: "python3 solve.py", run_in_background: true }))).toBe(true); expect(isDeny(await call("Bash", { command: "nohup python3 solve.py > results/solve.log 2>&1 &" }))).toBe( false, ); }); it("allows ordinary research commands", async () => { for (const command of [ "uv venv .venv && uv pip install numpy pandas", "python3 code/analysis.py --seed 1", "tectonic -X compile main.tex", "shasum -a 256 data/raw.csv", "curl -s https://api.openalex.org/works?search=x", ]) { expect(isDeny(await call("Bash", { command })), command).toBe(false); } }); it("confines writes to the workspace, blocks frozen paths and symlink escapes", async () => { expect(isDeny(await call("Write", { file_path: join(ws, "results/metrics.json") }))).toBe(false); expect(isDeny(await call("Write", { file_path: join(ws, "../escape.txt") }))).toBe(true); expect(isDeny(await call("Write", { file_path: "/tmp/anything.txt" }))).toBe(true); expect(isDeny(await call("Edit", { file_path: join(ws, "CLAUDE.md") }))).toBe(true); expect(isDeny(await call("Write", { file_path: join(ws, "input/run.json") }))).toBe(true); expect(isDeny(await call("Write", { file_path: join(ws, ".claude/settings.json") }))).toBe(true); expect(isDeny(await call("Write", { file_path: join(ws, "code/.claude/settings.json") }))).toBe(true); expect(isDeny(await call("Write", { file_path: join(ws, "code/.git/hooks/pre-commit") }))).toBe(true); expect(isDeny(await call("Write", { file_path: join(ws, "results/link/evil.txt") }))).toBe(true); // symlink to outside }); it("reviewer sessions may only write under review/", async () => { const rev = createPolicyHook({ workspace: ws, frozenPaths: ["CLAUDE.md"], writableOnly: ["review/"] }); const c = (p: string) => rev( { hook_event_name: "PreToolUse", tool_name: "Write", tool_input: { file_path: join(ws, p) }, tool_use_id: "t", session_id: "s", transcript_path: "", cwd: ws, } as never, "t", { signal: new AbortController().signal }, ); expect(isDeny(await c("review/review-1.md"))).toBe(false); expect(isDeny(await c("manuscript/main.tex"))).toBe(true); }); it("blocks SSRF targets for WebFetch/MCP tools and reads of sensitive paths", async () => { for (const url of [ "http://192.168.2.72:8470/", "http://localhost:5432/", "http://127.0.0.1/", "http://M3U96b.maclustr.io:8470/", "http://100.69.26.91/", "file:///etc/passwd", "http://169.254.169.254/latest/meta-data", ]) { expect(isDeny(await call("WebFetch", { url })), url).toBe(true); } expect(isDeny(await call("Read", { file_path: "/Users/simon-pierreboucher/.ssh/id_ed25519" }))).toBe( true, ); expect(isDeny(await call("Read", { file_path: "/etc/autoscholar/worker.env" }))).toBe(true); expect(isDeny(await call("Read", { file_path: join(ws, "data/x.csv") }))).toBe(false); expect(isDeny(await call("SomeUnknownTool", {}))).toBe(true); }); it("classifies private addresses", async () => { expect(isPrivateIp("10.1.2.3")).toBe(true); expect(isPrivateIp("192.168.2.85")).toBe(true); expect(isPrivateIp("100.100.1.1")).toBe(true); expect(isPrivateIp("8.8.8.8")).toBe(false); await expect(assertPublicUrl("http://10.0.0.1/")).rejects.toThrow(); await expect(assertPublicUrl("ftp://example.org/")).rejects.toThrow(); }); }); describe("no false positives on common research commands", () => { it("allows python open() and prose containing 'open'", async () => { for (const command of [ "python3 - <<'EOF'\nt=open('plan/x.md').read()\nEOF", "echo 'an open question' >> notes.md", "grep -n 'say' results/robustness.md", ]) { expect(isDeny(await call("Bash", { command })), command).toBe(false); } expect(isDeny(await call("Bash", { command: "open -a Safari https://x" }))).toBe(true); expect(isDeny(await call("Bash", { command: "ls; open ." }))).toBe(true); }); });