291 lines
7.8 KiB
TypeScript
291 lines
7.8 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { evaluateStrictWritePermission } from "./strictPermissionGuard";
|
|
import type { AcpPermissionRequest } from "./acpClient";
|
|
|
|
function makeRequest(
|
|
overrides?: Partial<AcpPermissionRequest>,
|
|
): AcpPermissionRequest {
|
|
const overrideToolCall = overrides?.toolCall || {};
|
|
return {
|
|
sessionId: "s-1",
|
|
toolCall: {
|
|
toolCallId: "tc-1",
|
|
kind: "edit",
|
|
title: "Edit",
|
|
rawInput: { file_path: "/workspace/a.txt" },
|
|
...overrideToolCall,
|
|
},
|
|
options: [
|
|
{ optionId: "allow-once", kind: "allow_once", name: "allow once" },
|
|
{ optionId: "allow-always", kind: "allow_always", name: "allow always" },
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("strictPermissionGuard", () => {
|
|
it("strict 关闭时不拦截", () => {
|
|
const result = evaluateStrictWritePermission(makeRequest(), {
|
|
strictEnabled: false,
|
|
workspaceDir: "/workspace",
|
|
appDataDir: "/home/me/.qimingclaw",
|
|
tempDirs: ["/tmp"],
|
|
});
|
|
|
|
expect(result.blocked).toBe(false);
|
|
expect(result.reason).toBe("strict_not_active");
|
|
});
|
|
|
|
it("允许 workspace 内写入", () => {
|
|
const result = evaluateStrictWritePermission(
|
|
makeRequest({
|
|
toolCall: {
|
|
toolCallId: "tc-2",
|
|
kind: "edit",
|
|
title: "Edit",
|
|
rawInput: { file_path: "/workspace/src/a.ts" },
|
|
},
|
|
}),
|
|
{
|
|
strictEnabled: true,
|
|
workspaceDir: "/workspace",
|
|
appDataDir: "/home/me/.qimingclaw",
|
|
tempDirs: ["/tmp"],
|
|
},
|
|
);
|
|
|
|
expect(result.isWriteRequest).toBe(true);
|
|
expect(result.blocked).toBe(false);
|
|
expect(result.reason).toBe("strict_paths_allowed");
|
|
});
|
|
|
|
it("相对路径按 workspace 解析,允许写入", () => {
|
|
const result = evaluateStrictWritePermission(
|
|
makeRequest({
|
|
toolCall: {
|
|
toolCallId: "tc-2b",
|
|
kind: "edit",
|
|
title: "Edit",
|
|
rawInput: { file_path: "src/a.ts" },
|
|
},
|
|
}),
|
|
{
|
|
strictEnabled: true,
|
|
workspaceDir: "/workspace",
|
|
appDataDir: "/home/me/.qimingclaw",
|
|
tempDirs: ["/tmp"],
|
|
},
|
|
);
|
|
|
|
expect(result.blocked).toBe(false);
|
|
expect(result.reason).toBe("strict_paths_allowed");
|
|
expect(result.resolvedPaths[0]).toBe("/workspace/src/a.ts");
|
|
});
|
|
|
|
it("允许 temp 目录写入", () => {
|
|
const result = evaluateStrictWritePermission(
|
|
makeRequest({
|
|
toolCall: {
|
|
toolCallId: "tc-3",
|
|
kind: "write",
|
|
title: "Write",
|
|
rawInput: { file_path: "/tmp/a.log" },
|
|
},
|
|
}),
|
|
{
|
|
strictEnabled: true,
|
|
workspaceDir: "/workspace",
|
|
appDataDir: "/home/me/.qimingclaw",
|
|
tempDirs: ["/tmp"],
|
|
},
|
|
);
|
|
|
|
expect(result.blocked).toBe(false);
|
|
expect(result.reason).toBe("strict_paths_allowed");
|
|
});
|
|
|
|
it("~ 路径按 isolatedHome 解析,允许写入", () => {
|
|
const result = evaluateStrictWritePermission(
|
|
makeRequest({
|
|
toolCall: {
|
|
toolCallId: "tc-3b",
|
|
kind: "write",
|
|
title: "Write",
|
|
rawInput: { file_path: "~/notes/a.txt" },
|
|
},
|
|
}),
|
|
{
|
|
strictEnabled: true,
|
|
workspaceDir: "/workspace",
|
|
isolatedHome: "/sandbox/home",
|
|
appDataDir: "/home/me/.qimingclaw",
|
|
tempDirs: ["/tmp"],
|
|
},
|
|
);
|
|
|
|
expect(result.blocked).toBe(false);
|
|
expect(result.reason).toBe("strict_paths_allowed");
|
|
expect(result.resolvedPaths[0]).toBe("/sandbox/home/notes/a.txt");
|
|
});
|
|
|
|
it("Windows: 绝对路径按 win32 语义解析且大小写不敏感", () => {
|
|
const result = evaluateStrictWritePermission(
|
|
makeRequest({
|
|
toolCall: {
|
|
toolCallId: "tc-win-1",
|
|
kind: "write",
|
|
title: "Write",
|
|
rawInput: { file_path: "c:\\workspace\\src\\a.ts" },
|
|
},
|
|
}),
|
|
{
|
|
strictEnabled: true,
|
|
workspaceDir: "C:\\Workspace",
|
|
appDataDir: "C:\\Users\\me\\.qimingclaw",
|
|
tempDirs: ["C:\\Temp"],
|
|
platform: "win32",
|
|
},
|
|
);
|
|
|
|
expect(result.blocked).toBe(false);
|
|
expect(result.reason).toBe("strict_paths_allowed");
|
|
expect(result.resolvedPaths[0]).toBe("c:\\workspace\\src\\a.ts");
|
|
});
|
|
|
|
it("Windows: 绝对路径超出 writable roots 时拒绝", () => {
|
|
const result = evaluateStrictWritePermission(
|
|
makeRequest({
|
|
toolCall: {
|
|
toolCallId: "tc-win-2",
|
|
kind: "write",
|
|
title: "Write",
|
|
rawInput: { file_path: "D:\\outside\\a.ts" },
|
|
},
|
|
}),
|
|
{
|
|
strictEnabled: true,
|
|
workspaceDir: "C:\\Workspace",
|
|
appDataDir: "C:\\Users\\me\\.qimingclaw",
|
|
tempDirs: ["C:\\Temp"],
|
|
platform: "win32",
|
|
},
|
|
);
|
|
|
|
expect(result.blocked).toBe(true);
|
|
expect(result.reason).toBe("strict_path_outside_roots");
|
|
});
|
|
|
|
it("拒绝 workspace/temp/appData 外写入", () => {
|
|
const result = evaluateStrictWritePermission(
|
|
makeRequest({
|
|
toolCall: {
|
|
toolCallId: "tc-4",
|
|
kind: "write",
|
|
title: "Write",
|
|
rawInput: { file_path: "/etc/passwd" },
|
|
},
|
|
}),
|
|
{
|
|
strictEnabled: true,
|
|
workspaceDir: "/workspace",
|
|
appDataDir: "/home/me/.qimingclaw",
|
|
tempDirs: ["/tmp"],
|
|
},
|
|
);
|
|
|
|
expect(result.blocked).toBe(true);
|
|
expect(result.reason).toBe("strict_path_outside_roots");
|
|
});
|
|
|
|
it("strict 写入请求缺少路径时 fail-closed", () => {
|
|
const result = evaluateStrictWritePermission(
|
|
makeRequest({
|
|
toolCall: {
|
|
toolCallId: "tc-5",
|
|
kind: "edit",
|
|
title: "Edit",
|
|
rawInput: {},
|
|
},
|
|
}),
|
|
{
|
|
strictEnabled: true,
|
|
workspaceDir: "/workspace",
|
|
appDataDir: "/home/me/.qimingclaw",
|
|
tempDirs: ["/tmp"],
|
|
},
|
|
);
|
|
|
|
expect(result.blocked).toBe(true);
|
|
expect(result.reason).toBe("strict_missing_path");
|
|
});
|
|
|
|
it("识别 EditNotebook 的 target_notebook 路径并允许 workspace 内写入", () => {
|
|
const result = evaluateStrictWritePermission(
|
|
makeRequest({
|
|
toolCall: {
|
|
toolCallId: "tc-5b",
|
|
kind: "edit",
|
|
title: "Edit Notebook",
|
|
rawInput: { target_notebook: "/workspace/docs/demo.ipynb" },
|
|
},
|
|
}),
|
|
{
|
|
strictEnabled: true,
|
|
workspaceDir: "/workspace",
|
|
appDataDir: "/home/me/.qimingclaw",
|
|
tempDirs: ["/tmp"],
|
|
},
|
|
);
|
|
|
|
expect(result.blocked).toBe(false);
|
|
expect(result.reason).toBe("strict_paths_allowed");
|
|
expect(result.candidatePaths).toContain("/workspace/docs/demo.ipynb");
|
|
});
|
|
|
|
it("未知键名但值为相对路径时按路径处理", () => {
|
|
const result = evaluateStrictWritePermission(
|
|
makeRequest({
|
|
toolCall: {
|
|
toolCallId: "tc-5c",
|
|
kind: "edit",
|
|
title: "Edit",
|
|
rawInput: { destination: "./notes/todo.md" },
|
|
},
|
|
}),
|
|
{
|
|
strictEnabled: true,
|
|
workspaceDir: "/workspace",
|
|
appDataDir: "/home/me/.qimingclaw",
|
|
tempDirs: ["/tmp"],
|
|
},
|
|
);
|
|
|
|
expect(result.blocked).toBe(false);
|
|
expect(result.reason).toBe("strict_paths_allowed");
|
|
expect(result.resolvedPaths[0]).toBe("/workspace/notes/todo.md");
|
|
});
|
|
|
|
it("非写入请求不触发 strict 路径拦截", () => {
|
|
const result = evaluateStrictWritePermission(
|
|
makeRequest({
|
|
toolCall: {
|
|
toolCallId: "tc-6",
|
|
kind: "bash",
|
|
title: "Bash",
|
|
rawInput: { command: "ls -la" },
|
|
},
|
|
}),
|
|
{
|
|
strictEnabled: true,
|
|
workspaceDir: "/workspace",
|
|
appDataDir: "/home/me/.qimingclaw",
|
|
tempDirs: ["/tmp"],
|
|
},
|
|
);
|
|
|
|
expect(result.isWriteRequest).toBe(false);
|
|
expect(result.blocked).toBe(false);
|
|
expect(result.reason).toBe("not_write_request");
|
|
});
|
|
});
|