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