2025-04-16 14:16:53 -07:00
|
|
|
|
import type { SafetyAssessment } from "../src/approvals";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-16 14:16:53 -07:00
|
|
|
|
import { canAutoApprove } from "../src/approvals";
|
2025-04-21 09:52:11 -07:00
|
|
|
|
import { describe, test, expect } from "vitest";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
describe("canAutoApprove()", () => {
|
|
|
|
|
|
const env = {
|
|
|
|
|
|
PATH: "/usr/local/bin:/usr/bin:/bin",
|
|
|
|
|
|
HOME: "/home/user",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const writeablePaths: Array<string> = [];
|
2025-04-17 17:32:53 -07:00
|
|
|
|
const check = (command: ReadonlyArray<string>): SafetyAssessment =>
|
when a shell tool call invokes apply_patch, resolve relative paths against workdir, if specified (#556)
Previously, we were ignoring the `workdir` field in an `ExecInput` when
running it through `canAutoApprove()`. For ordinary `exec()` calls, that
was sufficient, but for `apply_patch`, we need the `workdir` to resolve
relative paths in the `apply_patch` argument so that we can check them
in `isPathConstrainedTowritablePaths()`.
Likewise, we also need the workdir when running `execApplyPatch()`
because the paths need to be resolved again.
Ideally, the `ApplyPatchCommand` returned by `canAutoApprove()` would
not be a simple `patch: string`, but the parsed patch with all of the
paths resolved, in which case `execApplyPatch()` could expect absolute
paths and would not need `workdir`.
2025-04-22 14:07:47 -07:00
|
|
|
|
canAutoApprove(
|
|
|
|
|
|
command,
|
|
|
|
|
|
/* workdir */ undefined,
|
|
|
|
|
|
"suggest",
|
|
|
|
|
|
writeablePaths,
|
|
|
|
|
|
env,
|
|
|
|
|
|
);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-17 17:32:53 -07:00
|
|
|
|
test("simple safe commands", () => {
|
|
|
|
|
|
expect(check(["ls"])).toEqual({
|
2025-04-16 12:56:08 -04:00
|
|
|
|
type: "auto-approve",
|
|
|
|
|
|
reason: "List directory",
|
|
|
|
|
|
group: "Searching",
|
|
|
|
|
|
runInSandbox: false,
|
|
|
|
|
|
});
|
2025-04-17 17:32:53 -07:00
|
|
|
|
expect(check(["cat", "file.txt"])).toEqual({
|
2025-04-16 12:56:08 -04:00
|
|
|
|
type: "auto-approve",
|
|
|
|
|
|
reason: "View file contents",
|
|
|
|
|
|
group: "Reading files",
|
|
|
|
|
|
runInSandbox: false,
|
|
|
|
|
|
});
|
2025-04-17 17:32:53 -07:00
|
|
|
|
expect(check(["pwd"])).toEqual({
|
2025-04-16 12:56:08 -04:00
|
|
|
|
type: "auto-approve",
|
|
|
|
|
|
reason: "Print working directory",
|
|
|
|
|
|
group: "Navigating",
|
|
|
|
|
|
runInSandbox: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-04-17 17:32:53 -07:00
|
|
|
|
test("simple safe commands within a `bash -lc` call", () => {
|
|
|
|
|
|
expect(check(["bash", "-lc", "ls"])).toEqual({
|
2025-04-16 12:56:08 -04:00
|
|
|
|
type: "auto-approve",
|
|
|
|
|
|
reason: "List directory",
|
|
|
|
|
|
group: "Searching",
|
|
|
|
|
|
runInSandbox: false,
|
|
|
|
|
|
});
|
2025-04-17 17:32:53 -07:00
|
|
|
|
expect(check(["bash", "-lc", "ls $HOME"])).toEqual({
|
2025-04-16 12:56:08 -04:00
|
|
|
|
type: "auto-approve",
|
|
|
|
|
|
reason: "List directory",
|
|
|
|
|
|
group: "Searching",
|
|
|
|
|
|
runInSandbox: false,
|
|
|
|
|
|
});
|
2025-04-17 17:32:53 -07:00
|
|
|
|
expect(check(["bash", "-lc", "git show ab9811cb90"])).toEqual({
|
2025-04-16 12:56:08 -04:00
|
|
|
|
type: "auto-approve",
|
|
|
|
|
|
reason: "Git show",
|
|
|
|
|
|
group: "Using git",
|
|
|
|
|
|
runInSandbox: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("bash -lc commands with unsafe redirects", () => {
|
|
|
|
|
|
expect(check(["bash", "-lc", "echo hello > file.txt"])).toEqual({
|
|
|
|
|
|
type: "ask-user",
|
|
|
|
|
|
});
|
2025-04-17 17:32:53 -07:00
|
|
|
|
// In theory, we could make our checker more sophisticated to auto-approve
|
|
|
|
|
|
// This previously required approval, but now that we consider safe
|
|
|
|
|
|
// operators like "&&" the entire expression can be auto‑approved.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
expect(check(["bash", "-lc", "ls && pwd"])).toEqual({
|
|
|
|
|
|
type: "auto-approve",
|
|
|
|
|
|
reason: "List directory",
|
|
|
|
|
|
group: "Searching",
|
|
|
|
|
|
runInSandbox: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-04-17 17:32:53 -07:00
|
|
|
|
test("true command is considered safe", () => {
|
|
|
|
|
|
expect(check(["true"])).toEqual({
|
2025-04-16 12:56:08 -04:00
|
|
|
|
type: "auto-approve",
|
2025-04-21 12:33:57 -04:00
|
|
|
|
reason: "No-op (true)",
|
2025-04-16 12:56:08 -04:00
|
|
|
|
group: "Utility",
|
|
|
|
|
|
runInSandbox: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("commands that should require approval", () => {
|
|
|
|
|
|
// Should this be on the auto-approved list?
|
|
|
|
|
|
expect(check(["printenv"])).toEqual({ type: "ask-user" });
|
|
|
|
|
|
|
|
|
|
|
|
expect(check(["git", "commit"])).toEqual({ type: "ask-user" });
|
|
|
|
|
|
|
|
|
|
|
|
expect(check(["pytest"])).toEqual({ type: "ask-user" });
|
|
|
|
|
|
|
|
|
|
|
|
expect(check(["cargo", "build"])).toEqual({ type: "ask-user" });
|
|
|
|
|
|
});
|
2025-04-21 10:29:58 -07:00
|
|
|
|
|
|
|
|
|
|
test("find", () => {
|
|
|
|
|
|
expect(check(["find", ".", "-name", "file.txt"])).toEqual({
|
|
|
|
|
|
type: "auto-approve",
|
|
|
|
|
|
reason: "Find files or directories",
|
|
|
|
|
|
group: "Searching",
|
|
|
|
|
|
runInSandbox: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Options that can execute arbitrary commands.
|
|
|
|
|
|
expect(
|
|
|
|
|
|
check(["find", ".", "-name", "file.txt", "-exec", "rm", "{}", ";"]),
|
|
|
|
|
|
).toEqual({
|
|
|
|
|
|
type: "ask-user",
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(
|
|
|
|
|
|
check(["find", ".", "-name", "*.py", "-execdir", "python3", "{}", ";"]),
|
|
|
|
|
|
).toEqual({
|
|
|
|
|
|
type: "ask-user",
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(
|
|
|
|
|
|
check(["find", ".", "-name", "file.txt", "-ok", "rm", "{}", ";"]),
|
|
|
|
|
|
).toEqual({
|
|
|
|
|
|
type: "ask-user",
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(
|
|
|
|
|
|
check(["find", ".", "-name", "*.py", "-okdir", "python3", "{}", ";"]),
|
|
|
|
|
|
).toEqual({
|
|
|
|
|
|
type: "ask-user",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Option that deletes matching files.
|
|
|
|
|
|
expect(check(["find", ".", "-delete", "-name", "file.txt"])).toEqual({
|
|
|
|
|
|
type: "ask-user",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Options that write pathnames to a file.
|
|
|
|
|
|
expect(check(["find", ".", "-fls", "/etc/passwd"])).toEqual({
|
|
|
|
|
|
type: "ask-user",
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(check(["find", ".", "-fprint", "/etc/passwd"])).toEqual({
|
|
|
|
|
|
type: "ask-user",
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(check(["find", ".", "-fprint0", "/etc/passwd"])).toEqual({
|
|
|
|
|
|
type: "ask-user",
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(
|
|
|
|
|
|
check(["find", ".", "-fprintf", "/root/suid.txt", "%#m %u %p\n"]),
|
|
|
|
|
|
).toEqual({
|
|
|
|
|
|
type: "ask-user",
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2025-04-16 12:56:08 -04:00
|
|
|
|
});
|