201 lines
6.5 KiB
TypeScript
201 lines
6.5 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { mkdtemp, mkdir, rm, symlink } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
|
|
import {
|
|
canAutoApprovePermission,
|
|
resolvePermissionApproval,
|
|
} from "../../src/routes/chatPermissionPolicy.js";
|
|
|
|
describe("permission approval policy", () => {
|
|
it.each([
|
|
"show_chart",
|
|
"web_search",
|
|
"skill",
|
|
])("allows low-risk permission %s", (permission) => {
|
|
expect(canAutoApprovePermission(permission)).toBe(true);
|
|
});
|
|
|
|
it("allows structured searches within the workspace", () => {
|
|
const workspaceRoot = process.cwd();
|
|
const context = {
|
|
workspaceRoot,
|
|
metadata: { path: join(workspaceRoot, "src"), include: "*.ts" },
|
|
patterns: ["*.ts"],
|
|
};
|
|
expect(canAutoApprovePermission("glob", context)).toBe(true);
|
|
expect(canAutoApprovePermission("grep", context)).toBe(true);
|
|
});
|
|
|
|
it("allows a glob with an explicit safe prefix from the workspace root", () => {
|
|
expect(
|
|
canAutoApprovePermission("glob", {
|
|
workspaceRoot: process.cwd(),
|
|
metadata: { path: process.cwd(), pattern: "src/**/*.ts" },
|
|
patterns: ["src/**/*.ts"],
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
{ metadata: { path: dirname(process.cwd()) }, patterns: ["*"] },
|
|
{ metadata: { path: join(process.cwd(), ".local.env") }, patterns: ["*"] },
|
|
{ metadata: { path: join(process.cwd(), "data") }, patterns: ["*"] },
|
|
{ metadata: { path: join(process.cwd(), "src") }, patterns: ["../logs/**"] },
|
|
{ metadata: { path: process.cwd() }, patterns: ["**/*.env"] },
|
|
{ metadata: { path: process.cwd() }, patterns: ["**/*"] },
|
|
{ metadata: { path: join(process.cwd(), "src") }, patterns: [".[e]nv"] },
|
|
])("keeps protected or external searches interactive", (request) => {
|
|
expect(
|
|
canAutoApprovePermission("glob", {
|
|
workspaceRoot: process.cwd(),
|
|
...request,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("keeps grep from the workspace root interactive", () => {
|
|
expect(
|
|
canAutoApprovePermission("grep", {
|
|
workspaceRoot: process.cwd(),
|
|
metadata: { path: process.cwd(), include: "*.ts" },
|
|
patterns: ["secret"],
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("rejects a workspace symlink that resolves outside the workspace", async () => {
|
|
const workspaceRoot = await mkdtemp(join(tmpdir(), "permission-workspace-"));
|
|
const externalRoot = await mkdtemp(join(tmpdir(), "permission-external-"));
|
|
try {
|
|
await mkdir(join(externalRoot, "src"));
|
|
const linkedPath = join(workspaceRoot, "linked");
|
|
await symlink(join(externalRoot, "src"), linkedPath, "dir");
|
|
|
|
expect(
|
|
canAutoApprovePermission("grep", {
|
|
workspaceRoot,
|
|
metadata: { path: linkedPath, include: "*.ts" },
|
|
patterns: ["secret"],
|
|
}),
|
|
).toBe(false);
|
|
} finally {
|
|
await Promise.all([
|
|
rm(workspaceRoot, { force: true, recursive: true }),
|
|
rm(externalRoot, { force: true, recursive: true }),
|
|
]);
|
|
}
|
|
});
|
|
|
|
it("rejects protected descendants below an otherwise safe search root", async () => {
|
|
const workspaceRoot = await mkdtemp(join(tmpdir(), "permission-descendant-"));
|
|
try {
|
|
const sourceRoot = join(workspaceRoot, "src");
|
|
await mkdir(join(sourceRoot, "data"), { recursive: true });
|
|
|
|
expect(
|
|
canAutoApprovePermission("grep", {
|
|
workspaceRoot,
|
|
metadata: { path: sourceRoot, include: "*.ts" },
|
|
patterns: ["secret"],
|
|
}),
|
|
).toBe(false);
|
|
} finally {
|
|
await rm(workspaceRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
"bash",
|
|
"edit",
|
|
"external_directory",
|
|
"store_render_ref",
|
|
"tjwater_server_query",
|
|
"tjwater_tjwater_server_query",
|
|
])(
|
|
"requires confirmation for permission %s",
|
|
(permission) => {
|
|
expect(canAutoApprovePermission(permission)).toBe(false);
|
|
},
|
|
);
|
|
|
|
it("resolves request, auto, and always modes", () => {
|
|
expect(resolvePermissionApproval("request", "show_chart").autoApprove).toBe(false);
|
|
expect(resolvePermissionApproval("auto", "show_chart").autoApprove).toBe(true);
|
|
expect(resolvePermissionApproval("auto", "bash").autoApprove).toBe(false);
|
|
expect(resolvePermissionApproval("always", "bash")).toMatchObject({
|
|
autoApprove: true,
|
|
title: "已按始终允许模式放行",
|
|
});
|
|
});
|
|
|
|
it("auto approves sandboxed shell and file access in a conversation workspace", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "permission-conversations-"));
|
|
const conversationRoot = join(root, "conversation-workspaces");
|
|
const workspaceRoot = join(conversationRoot, "conversation-test");
|
|
try {
|
|
await mkdir(workspaceRoot, { recursive: true });
|
|
expect(
|
|
resolvePermissionApproval("auto", "bash", {
|
|
workspaceRoot,
|
|
metadata: { command: "python3 analysis.py" },
|
|
}),
|
|
).toMatchObject({ autoApprove: true, autoReject: false });
|
|
expect(
|
|
canAutoApprovePermission("edit", {
|
|
workspaceRoot,
|
|
metadata: { filePath: join(workspaceRoot, "result.json") },
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
canAutoApprovePermission("glob", {
|
|
workspaceRoot,
|
|
metadata: { path: workspaceRoot, pattern: "**/*.json" },
|
|
patterns: ["**/*.json"],
|
|
}),
|
|
).toBe(true);
|
|
} finally {
|
|
await rm(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
"rm -rf ./target",
|
|
"rm -rf ./target",
|
|
"rm -Rf ./target",
|
|
"/bin/rm -rf ./target",
|
|
"command rm --force --recursive ./target",
|
|
"env LANG=C rm -r -f ./target",
|
|
"SAFE=1 rm -rf ./target",
|
|
"env -u HOME rm -rf ./target",
|
|
"r\"\"m -rf ./target",
|
|
"(rm -rf ./target)",
|
|
"! rm -rf ./target",
|
|
"npm test && rm --recursive --force ./target",
|
|
])("rejects direct recursive force removal in always mode: %s", (command) => {
|
|
expect(
|
|
resolvePermissionApproval("always", "bash", {
|
|
metadata: { command },
|
|
patterns: [command],
|
|
}),
|
|
).toMatchObject({
|
|
autoApprove: false,
|
|
autoReject: true,
|
|
title: "已拒绝递归强制删除",
|
|
});
|
|
});
|
|
|
|
it.each(["rm tmp.txt", "rm -f tmp.txt", "rm -r tmp-dir", "echo 'rm -rf tmp'"])(
|
|
"allows non-force-recursive or non-executed removal text in always mode: %s",
|
|
(command) => {
|
|
expect(
|
|
resolvePermissionApproval("always", "bash", {
|
|
metadata: { command },
|
|
patterns: [command],
|
|
}),
|
|
).toMatchObject({ autoApprove: true, autoReject: false });
|
|
},
|
|
);
|
|
});
|