ACP: fail closed on conflicting tool identity hints

This commit is contained in:
Vincent Koc
2026-03-14 20:27:37 -07:00
parent d1e4ee03ff
commit 7626903808
3 changed files with 38 additions and 1 deletions

View File

@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
- 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.
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
- ACP/approvals: use canonical tool identity for prompting decisions and fail closed when conflicting tool identity hints are present. Thanks @vincentkoc.
## 2026.3.13

View File

@@ -366,6 +366,27 @@ describe("resolvePermissionRequest", () => {
expect(prompt).not.toHaveBeenCalled();
});
it("prompts when raw input spoofs a safe tool name for a dangerous title", async () => {
const prompt = vi.fn(async () => false);
const res = await resolvePermissionRequest(
makePermissionRequest({
toolCall: {
toolCallId: "tool-exec-spoof",
title: "exec: cat /etc/passwd",
status: "pending",
rawInput: {
command: "cat /etc/passwd",
name: "search",
},
},
}),
{ prompt, log: () => {} },
);
expect(prompt).toHaveBeenCalledTimes(1);
expect(prompt).toHaveBeenCalledWith(undefined, "exec: cat /etc/passwd");
expect(res).toEqual({ outcome: { outcome: "selected", optionId: "reject" } });
});
it("prompts for read outside cwd scope", async () => {
const prompt = vi.fn(async () => false);
const res = await resolvePermissionRequest(

View File

@@ -104,7 +104,22 @@ function resolveToolNameForPermission(params: RequestPermissionRequest): string
const fromMeta = readFirstStringValue(toolMeta, ["toolName", "tool_name", "name"]);
const fromRawInput = readFirstStringValue(rawInput, ["tool", "toolName", "tool_name", "name"]);
const fromTitle = parseToolNameFromTitle(toolCall?.title);
return normalizeToolName(fromMeta ?? fromRawInput ?? fromTitle ?? "");
const metaName = fromMeta ? normalizeToolName(fromMeta) : undefined;
const rawInputName = fromRawInput ? normalizeToolName(fromRawInput) : undefined;
const titleName = fromTitle ? normalizeToolName(fromTitle) : undefined;
if ((fromMeta && !metaName) || (fromRawInput && !rawInputName)) {
return undefined;
}
if (metaName && titleName && metaName !== titleName) {
return undefined;
}
if (rawInputName && metaName && rawInputName !== metaName) {
return undefined;
}
if (rawInputName && titleName && rawInputName !== titleName) {
return undefined;
}
return metaName ?? titleName;
}
function extractPathFromToolTitle(