Files
TJWaterAgent/tests/routes/chatPermissionPolicy.test.ts
jiang 80cfc1f2ab
Generic Container CI/CD / test-build-publish (push) Successful in 2m44s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Successful in 2m44s
feat(agent): sandbox conversation analysis
2026-08-25 16:08:33 +08:00

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 });
},
);
});