diff --git a/src/acp/client.test.ts b/src/acp/client.test.ts index 554823600..7b266b606 100644 --- a/src/acp/client.test.ts +++ b/src/acp/client.test.ts @@ -85,6 +85,18 @@ describe("resolvePermissionRequest", () => { expect(res).toEqual({ outcome: { outcome: "selected", optionId: "reject" } }); }); + it("prompts when tool name contains read/search substrings but isn't a safe kind", async () => { + const prompt = vi.fn(async () => false); + const res = await resolvePermissionRequest( + makePermissionRequest({ + toolCall: { toolCallId: "tool-t", title: "thread: reply", status: "pending" }, + }), + { prompt, log: () => {} }, + ); + expect(prompt).toHaveBeenCalledTimes(1); + expect(res).toEqual({ outcome: { outcome: "selected", optionId: "reject" } }); + }); + it("uses allow_always and reject_always when once options are absent", async () => { const options: RequestPermissionRequest["options"] = [ { kind: "allow_always", name: "Always allow", optionId: "allow-always" }, diff --git a/src/acp/client.ts b/src/acp/client.ts index c6ea3ed38..6d35a8089 100644 --- a/src/acp/client.ts +++ b/src/acp/client.ts @@ -96,11 +96,17 @@ function resolveToolKindForPermission( } const normalized = name.toLowerCase(); - // Prefer a conservative classifier: if in doubt, return "other" (prompt-required). - if (normalized === "read" || normalized.includes("read")) { + const hasToken = (token: string) => { + // Tool names tend to be snake_case. Avoid substring heuristics (ex: "thread" contains "read"). + const re = new RegExp(`(?:^|[._-])${token}(?:$|[._-])`); + return re.test(normalized); + }; + + // Prefer a conservative classifier: only classify safe kinds when confident. + if (normalized === "read" || hasToken("read")) { return "read"; } - if (normalized === "search" || normalized.includes("search") || normalized.includes("find")) { + if (normalized === "search" || hasToken("search") || hasToken("find")) { return "search"; } if (normalized.includes("fetch") || normalized.includes("http")) {