fix(agent): request approval for protected path access
This commit is contained in:
@@ -124,10 +124,22 @@ describe("permission approval policy", () => {
|
||||
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("auto", "external_directory")).toMatchObject({
|
||||
autoApprove: false,
|
||||
autoReject: false,
|
||||
});
|
||||
expect(resolvePermissionApproval("always", "bash")).toMatchObject({
|
||||
autoApprove: true,
|
||||
title: "已按始终允许模式放行",
|
||||
});
|
||||
expect(resolvePermissionApproval("always", "external_directory")).toMatchObject({
|
||||
autoApprove: false,
|
||||
autoReject: false,
|
||||
});
|
||||
expect(resolvePermissionApproval("always", "read")).toMatchObject({
|
||||
autoApprove: false,
|
||||
autoReject: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("auto approves sandboxed shell and file access in a conversation workspace", async () => {
|
||||
@@ -142,12 +154,40 @@ describe("permission approval policy", () => {
|
||||
metadata: { command: "python3 analysis.py" },
|
||||
}),
|
||||
).toMatchObject({ autoApprove: true, autoReject: false });
|
||||
for (const approvalMode of ["auto", "always"] as const) {
|
||||
expect(
|
||||
resolvePermissionApproval(approvalMode, "bash", {
|
||||
workspaceRoot,
|
||||
metadata: { command: "cat .env" },
|
||||
}),
|
||||
).toMatchObject({ autoApprove: false, autoReject: false });
|
||||
}
|
||||
expect(
|
||||
canAutoApprovePermission("edit", {
|
||||
workspaceRoot,
|
||||
metadata: { filePath: join(workspaceRoot, "result.json") },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
resolvePermissionApproval("always", "edit", {
|
||||
workspaceRoot,
|
||||
metadata: { filePath: join(workspaceRoot, "result.json") },
|
||||
}),
|
||||
).toMatchObject({ autoApprove: true, autoReject: false });
|
||||
for (const filePath of [".env", "data/private.json", "logs/latest.log"]) {
|
||||
expect(
|
||||
resolvePermissionApproval("auto", "edit", {
|
||||
workspaceRoot,
|
||||
metadata: { filePath: join(workspaceRoot, filePath) },
|
||||
}),
|
||||
).toMatchObject({ autoApprove: false, autoReject: false });
|
||||
expect(
|
||||
resolvePermissionApproval("always", "edit", {
|
||||
workspaceRoot,
|
||||
metadata: { filePath: join(workspaceRoot, filePath) },
|
||||
}),
|
||||
).toMatchObject({ autoApprove: false, autoReject: false });
|
||||
}
|
||||
expect(
|
||||
canAutoApprovePermission("glob", {
|
||||
workspaceRoot,
|
||||
@@ -173,17 +213,19 @@ describe("permission approval policy", () => {
|
||||
"(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: "已拒绝递归强制删除",
|
||||
});
|
||||
])("asks before recursive force removal in always mode: %s", (command) => {
|
||||
for (const approvalMode of ["auto", "always"] as const) {
|
||||
expect(
|
||||
resolvePermissionApproval(approvalMode, "bash", {
|
||||
metadata: { command },
|
||||
patterns: [command],
|
||||
}),
|
||||
).toMatchObject({
|
||||
autoApprove: false,
|
||||
autoReject: false,
|
||||
title: "等待权限确认",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["rm tmp.txt", "rm -f tmp.txt", "rm -r tmp-dir", "echo 'rm -rf tmp'"])(
|
||||
|
||||
@@ -584,6 +584,28 @@ describe("streamPromptResponse", () => {
|
||||
always: ["npm test"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "permission.asked",
|
||||
properties: {
|
||||
id: "perm-auto-external",
|
||||
sessionID: "runtime-session-1",
|
||||
permission: "external_directory",
|
||||
patterns: ["/app/opencode.json/*"],
|
||||
metadata: { filepath: "/app/opencode.json" },
|
||||
always: ["/app/opencode.json/*"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "permission.asked",
|
||||
properties: {
|
||||
id: "perm-auto-env",
|
||||
sessionID: "runtime-session-1",
|
||||
permission: "read",
|
||||
patterns: [".env"],
|
||||
metadata: { filepath: ".env" },
|
||||
always: [".env"],
|
||||
},
|
||||
},
|
||||
{ type: "session.idle", properties: { sessionID: "runtime-session-1" } },
|
||||
]),
|
||||
prompt: async () => undefined,
|
||||
@@ -606,9 +628,19 @@ describe("streamPromptResponse", () => {
|
||||
request_id: "perm-auto-bash",
|
||||
permission: "bash",
|
||||
});
|
||||
expect(events.filter((item) => item.event === "permission_request")[1]?.data).toMatchObject({
|
||||
request_id: "perm-auto-external",
|
||||
permission: "external_directory",
|
||||
target: "/app/opencode.json",
|
||||
});
|
||||
expect(events.filter((item) => item.event === "permission_request")[2]?.data).toMatchObject({
|
||||
request_id: "perm-auto-env",
|
||||
permission: "read",
|
||||
target: ".env",
|
||||
});
|
||||
});
|
||||
|
||||
it("approves every OpenCode ask once in always mode", async () => {
|
||||
it("approves routine asks but forwards external paths in always mode", async () => {
|
||||
const replies: Array<Record<string, unknown>> = [];
|
||||
const runtime = {
|
||||
subscribeEvents: async () =>
|
||||
@@ -624,6 +656,28 @@ describe("streamPromptResponse", () => {
|
||||
always: ["npm test"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "permission.asked",
|
||||
properties: {
|
||||
id: "perm-always-external",
|
||||
sessionID: "runtime-session-1",
|
||||
permission: "external_directory",
|
||||
patterns: ["/app/private/*"],
|
||||
metadata: { filepath: "/app/private/config.json" },
|
||||
always: ["/app/private/*"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "permission.asked",
|
||||
properties: {
|
||||
id: "perm-always-env",
|
||||
sessionID: "runtime-session-1",
|
||||
permission: "read",
|
||||
patterns: [".env"],
|
||||
metadata: { filepath: ".env" },
|
||||
always: [".env"],
|
||||
},
|
||||
},
|
||||
{ type: "session.idle", properties: { sessionID: "runtime-session-1" } },
|
||||
]),
|
||||
prompt: async () => undefined,
|
||||
@@ -650,10 +704,17 @@ describe("streamPromptResponse", () => {
|
||||
reply: "once",
|
||||
},
|
||||
]);
|
||||
expect(events.some((item) => item.event === "permission_request")).toBe(false);
|
||||
expect(events.find((item) => item.event === "permission_request")?.data).toMatchObject({
|
||||
request_id: "perm-always-external",
|
||||
permission: "external_directory",
|
||||
});
|
||||
expect(events.filter((item) => item.event === "permission_request")[1]?.data).toMatchObject({
|
||||
request_id: "perm-always-env",
|
||||
permission: "read",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects recursive force removal even in always mode", async () => {
|
||||
it("asks before recursive force removal even in always mode", async () => {
|
||||
const replies: Array<Record<string, unknown>> = [];
|
||||
const runtime = {
|
||||
subscribeEvents: async () =>
|
||||
@@ -687,19 +748,10 @@ describe("streamPromptResponse", () => {
|
||||
write: (event, data) => events.push({ event, data }),
|
||||
});
|
||||
|
||||
expect(replies).toEqual([
|
||||
{
|
||||
requestId: "perm-always-rm-rf",
|
||||
sessionId: "runtime-session-1",
|
||||
directory: "/tmp/conversation-workspace-1",
|
||||
reply: "reject",
|
||||
},
|
||||
]);
|
||||
expect(events.some((item) => item.event === "permission_request")).toBe(false);
|
||||
expect(events.find((item) => item.event === "permission_response")?.data).toEqual({
|
||||
session_id: "client-session-1",
|
||||
expect(replies).toEqual([]);
|
||||
expect(events.find((item) => item.event === "permission_request")?.data).toMatchObject({
|
||||
request_id: "perm-always-rm-rf",
|
||||
reply: "reject",
|
||||
permission: "bash",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -735,6 +787,7 @@ describe("streamPromptResponse", () => {
|
||||
sessionId: "runtime-session-1",
|
||||
clientSessionId: "client-session-1",
|
||||
message: "read /tmp",
|
||||
approvalMode: "auto",
|
||||
write: (event, data) => events.push({ event, data }),
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("internal OpenCode permissions", () => {
|
||||
expect(dockerfile.startsWith("FROM smanx/opencode:1.18.13@")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps protected paths denied in every approval mode", async () => {
|
||||
it("sends protected paths to approval while allowing mounted skills", async () => {
|
||||
const config = JSON.parse(await readFile("opencode.json", "utf8")) as {
|
||||
permission?: Record<string, string | Record<string, string>>;
|
||||
};
|
||||
@@ -34,27 +34,35 @@ describe("internal OpenCode permissions", () => {
|
||||
const read = permission.read as Record<string, string> | undefined;
|
||||
|
||||
expect(permission["*"]).toBe("ask");
|
||||
expect(permission.external_directory).toBe("deny");
|
||||
expect(Object.entries(permission.external_directory ?? {})).toEqual([
|
||||
["*", "ask"],
|
||||
["/app/.opencode/skills/*", "allow"],
|
||||
]);
|
||||
expect(permission.task).toBe("deny");
|
||||
expect(permission.question).toBe("allow");
|
||||
expect(permission.activity_update).toBe("allow");
|
||||
expect(permission.final_answer).toBe("allow");
|
||||
expect(permission.todowrite).toBe("allow");
|
||||
expect(read?.["*"]).toBe("allow");
|
||||
expect(read?.["data/**"]).toBe("deny");
|
||||
expect(read?.["**/logs/**"]).toBe("deny");
|
||||
expect(read?.[".env"]).toBe("ask");
|
||||
expect(read?.["data/**"]).toBe("ask");
|
||||
expect(read?.["**/logs/**"]).toBe("ask");
|
||||
expect(Object.values(read ?? {})).not.toContain("deny");
|
||||
expect(edit?.["*"]).toBe("ask");
|
||||
expect(edit?.["data/**"]).toBe("deny");
|
||||
expect(edit?.["**/logs/**"]).toBe("deny");
|
||||
expect(edit?.[".env"]).toBe("ask");
|
||||
expect(edit?.["data/**"]).toBe("ask");
|
||||
expect(edit?.["**/logs/**"]).toBe("ask");
|
||||
expect(Object.values(edit ?? {})).not.toContain("deny");
|
||||
expect(bash?.["*"]).toBe("ask");
|
||||
expect(bash?.["rm *"]).toBe("ask");
|
||||
expect(bash?.["rm -rf *"]).toBe("deny");
|
||||
expect(bash?.["rm -fr *"]).toBe("deny");
|
||||
expect(bash?.["rm -r -f *"]).toBe("deny");
|
||||
expect(bash?.["rm -f -r *"]).toBe("deny");
|
||||
expect(bash?.["rm --recursive --force *"]).toBe("deny");
|
||||
expect(bash?.["rm --force --recursive *"]).toBe("deny");
|
||||
expect(bash?.["*.env*"]).toBe("deny");
|
||||
expect(bash?.["rm -rf *"]).toBe("ask");
|
||||
expect(bash?.["rm -fr *"]).toBe("ask");
|
||||
expect(bash?.["rm -r -f *"]).toBe("ask");
|
||||
expect(bash?.["rm -f -r *"]).toBe("ask");
|
||||
expect(bash?.["rm --recursive --force *"]).toBe("ask");
|
||||
expect(bash?.["rm --force --recursive *"]).toBe("ask");
|
||||
expect(bash?.["*.env*"]).toBe("ask");
|
||||
expect(Object.values(bash ?? {})).not.toContain("deny");
|
||||
expect(bash?.["*data/*"]).toBeUndefined();
|
||||
expect(bash?.["*logs/*"]).toBeUndefined();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user