import { describe, expect, it } from "vitest"; import { isProtectedComputerLifecycleCommand } from "./executor.js"; import { isProtectedComputerLifecycleCommandReadingScripts, protectedScriptWorkspacePath, } from "./executor-shell-guard.js"; describe("computer lifecycle command guard", () => { it("rejects commands that can destroy a graphical bot's desktop", () => { for (const command of [ "pkill chromium", "killall chrome", "kill -9 1234", "k\\ill -9 1234", "xkill", "systemctl restart chromium", "systemctl --user restart chromium", "service chromium restart", "rm -rf ~/.browser-profiles/chromium", 'rm -rf "$HOME/.browser-profiles/chromium"', "rm -f /tmp/.X1-lock", "bash -c 'pkill chromium'", 'bash -lc "killall chrome"', "bash -o posix -c 'pkill chromium'", "bash --rcfile /tmp/bashrc -c 'pkill chromium'", "sh -c 'systemctl restart chromium'", `bash -c 'eval "pkill chromium"'`, 'bash -lc "source /tmp/kill-chrome.sh"', "printf 'pkill chromium\\n' > /tmp/x; . /tmp/x", "bash -c `pkill chromium`", 'bash <<< "pkill chromium"', "$KILLER chromium", 'rm -rf "$TARGET/.browser-profiles/chromium"', ]) { expect(isProtectedComputerLifecycleCommand(command)).toBe(true); } }); it("rejects kill/pkill invoked through interpreter inline programs", () => { for (const command of [ `python -c 'import os; os.system("pkill chromium")'`, `python3 -c 'import subprocess; subprocess.call(["pkill", "chromium"])'`, `python -c 'import os; os.kill(1234, 9)'`, `python3 -Oc 'import os; os.system("killall chrome")'`, `node -e 'require("child_process").execSync("pkill chromium")'`, `node --eval 'process.kill(1234, "SIGKILL")'`, `node --eval=process.kill(1,"SIGKILL")`, `nodejs -e 'require("child_process").spawnSync("xkill")'`, `perl -e 'system("pkill chromium")'`, `ruby -e 'system("pkill chromium")'`, `php -r 'system("pkill chromium");'`, `bash -c 'python -c "import os; os.system(\\"pkill chromium\\")"'`, ]) { expect(isProtectedComputerLifecycleCommand(command)).toBe(true); } }); it("rejects kill-family teardown in interpreter script files", () => { const files = new Map([ ["/tmp/kill.py", "import os\nos.system('pkill chromium')\n"], ["evil.js", "process.kill(1, 'SIGKILL')\n"], ["./scripts/stop.py", "import subprocess\nsubprocess.call(['killall', 'chrome'])\n"], ]); const readScript = (scriptPath: string) => files.get(scriptPath); for (const command of [ "python /tmp/kill.py", "node evil.js", "python3 ./scripts/stop.py", "python3.12 ./scripts/stop.py", "bash -c 'python evil.js'", "bash -lc 'node evil.js'", "node -r evil.js -e 'console.log(1)'", ]) { expect(isProtectedComputerLifecycleCommand(command, readScript)).toBe(true); } // Filename heuristic when the executor cannot read the script (container /tmp). expect(isProtectedComputerLifecycleCommand("python /tmp/kill.py")).toBe(true); expect(isProtectedComputerLifecycleCommand("perl /tmp/pkill.pl")).toBe(true); expect(isProtectedComputerLifecycleCommand("node /tmp/killall.js")).toBe(true); expect(isProtectedComputerLifecycleCommand("ruby xkill.rb")).toBe(true); expect(isProtectedComputerLifecycleCommand("/usr/bin/python3 /tmp/kill.py")).toBe(true); }); it("prefers script contents over the filename heuristic", async () => { await expect( isProtectedComputerLifecycleCommandReadingScripts( "python kill.py", async () => "print('hello')\n", ), ).resolves.toBe(false); await expect( isProtectedComputerLifecycleCommandReadingScripts("python kill.py", async () => undefined), ).resolves.toBe(true); await expect( isProtectedComputerLifecycleCommandReadingScripts( "bash -c 'node evil.js'", async () => "process.kill(1, 'SIGKILL')\n", ), ).resolves.toBe(true); }); it("keeps ordinary interpreter inline programs available", () => { expect(isProtectedComputerLifecycleCommand(`python -c 'print("hello")'`)).toBe(false); expect(isProtectedComputerLifecycleCommand(`python3 -c 'value = "$HOME"; print(value)'`)).toBe( false, ); expect(isProtectedComputerLifecycleCommand(`node -e 'console.log(1 + 1)'`)).toBe(false); expect(isProtectedComputerLifecycleCommand(`node -e 'const x = "kill switch"; console.log(x)'`)).toBe( false, ); }); it("keeps ordinary interpreter scripts available", () => { const files = new Map([ ["scripts/check.js", "console.log('ok')\n"], ["scripts/train.py", "print('epochs')\n"], ["kill.py", "print('hello')\n"], ["notes/harmless.py", "value = 'kill switch'\nprint(value)\n"], ["evil.js", "console.log('not a lifecycle command')\n"], ]); const readScript = (scriptPath: string) => files.get(scriptPath); expect(isProtectedComputerLifecycleCommand("node scripts/check.js", readScript)).toBe(false); expect( isProtectedComputerLifecycleCommand("python3 scripts/train.py --epochs 2", readScript), ).toBe(false); expect( isProtectedComputerLifecycleCommand("python -W ignore scripts/train.py", readScript), ).toBe(false); expect(isProtectedComputerLifecycleCommand("python kill.py", readScript)).toBe(false); expect(isProtectedComputerLifecycleCommand("python notes/harmless.py", readScript)).toBe(false); expect(isProtectedComputerLifecycleCommand("node evil.js", readScript)).toBe(false); expect(isProtectedComputerLifecycleCommand("echo python /tmp/kill.py")).toBe(false); expect(isProtectedComputerLifecycleCommand("node /tmp/plot.js")).toBe(false); expect(isProtectedComputerLifecycleCommand("python -c 'print(\"hello\")'")).toBe(false); }); it("maps interpreter scripts onto the workspace file API", () => { expect(protectedScriptWorkspacePath("./scripts/stop.py", undefined)).toBe("scripts/stop.py"); expect(protectedScriptWorkspacePath("evil.js", "notes")).toBe("notes/evil.js"); expect(protectedScriptWorkspacePath("stop.py", "bots/bot-1")).toBe("bots/bot-1/stop.py"); expect(protectedScriptWorkspacePath("evil.js", "/home/rakazo/notes")).toBe("notes/evil.js"); expect(protectedScriptWorkspacePath("/home/rakazo/scripts/stop.py", undefined)).toBe( "scripts/stop.py", ); expect(protectedScriptWorkspacePath("/home/user/rakazo-home/evil.js", undefined)).toBe( "evil.js", ); expect(protectedScriptWorkspacePath("evil.js", "/tmp")).toBe("tmp/evil.js"); expect(protectedScriptWorkspacePath("/tmp/kill.py", undefined)).toBeUndefined(); expect(protectedScriptWorkspacePath("../secret.py", "notes")).toBeUndefined(); expect(protectedScriptWorkspacePath("evil.js", "/home/user")).toBe("evil.js"); }); it("keeps ordinary shell work available", () => { expect(isProtectedComputerLifecycleCommand("pwd && ls -la")).toBe(false); expect(isProtectedComputerLifecycleCommand("node scripts/check.js")).toBe(false); expect(isProtectedComputerLifecycleCommand("systemctl status chromium")).toBe(false); expect(isProtectedComputerLifecycleCommand('rm -f "$WORKSPACE/tmp.txt"')).toBe(false); expect(isProtectedComputerLifecycleCommand("printf '%s\\n' *.txt && pwd")).toBe(false); }); it.each([ "find . -maxdepth 2 -type d -name .git -print", "git -C . status --short", "git add .", "git add then .", "ls . && git -C . worktree list --porcelain", "set -eu\npwd\nfind . -maxdepth 2 -type d -name .git -print", "git worktree add ../review-worktree origin/main", "find /tmp/. -maxdepth 1 -type d", "git diff -- .", "printf '%s' 'pk\\\nill chromium'", "printf '%s' 'pk\\\nill' chromium", "printf '%s' 'line one\nline two'", ])("allows repository paths without treating dot arguments as sourcing: %s", (command) => { expect(isProtectedComputerLifecycleCommand(command)).toBe(false); }); it.each([ ". /tmp/script.sh", "pwd; . /tmp/script.sh", "pwd\n. /tmp/script.sh", "find . -maxdepth 1 && . /tmp/script.sh", "command . /tmp/script.sh", "builtin . /tmp/script.sh", "command -p . /tmp/script.sh", "true && > /tmp/output . /tmp/script.sh", "2> /tmp/output . /tmp/script.sh", "if true; then . /tmp/script.sh; fi", "bash -c 'pwd\n. /tmp/script.sh'", "pk\\\nill chromium", "! . /tmp/script.sh", "if false; then :; elif . /tmp/script.sh; then :; fi", "{ . /tmp/script.sh; }", "coproc . /tmp/script.sh", "coproc worker . /tmp/script.sh", "function f { . /tmp/script.sh; }", "function f { . /tmp/script.sh; }; f", ])("continues blocking executable sourcing and lifecycle operations: %s", (command) => { expect(isProtectedComputerLifecycleCommand(command)).toBe(true); }); });