diff --git a/CHANGELOG.md b/CHANGELOG.md index deff5a5a361..5ea86670b58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Subagents: stop stale unended runs from counting as active or pending forever, while preserving restart-aborted recovery for recoverable child sessions. Fixes #71252. Thanks @hclsys. +- Gateway/tools: allow `POST /tools/invoke` to reach plugin-backed catalog tools such as `browser` when no core implementation exists, while still preferring built-in tools for real core names. Thanks @chat2way. - Browser/security: require `operator.admin` for the `browser.request` gateway method, matching the host/browser-node control authority exposed by that route. Thanks @RichardCao. - Reply media: allow sandboxed replies to deliver OpenClaw-managed `media/outbound` and `media/tool-*` attachments without treating them as sandbox escapes, while keeping alias-escape checks on the managed media root. Fixes #71138. Thanks @mayor686, @truffle-dev, and @neeravmakwana. - CLI/agent: keep `openclaw agent --json` stdout reserved for the JSON response by routing gateway, plugin, and embedded-fallback diagnostics to stderr before execution starts. Fixes #71319. diff --git a/src/gateway/tools-invoke-http.test.ts b/src/gateway/tools-invoke-http.test.ts index 4a56a92006a..45e6caf757c 100644 --- a/src/gateway/tools-invoke-http.test.ts +++ b/src/gateway/tools-invoke-http.test.ts @@ -127,6 +127,11 @@ vi.mock("../agents/openclaw-tools.js", () => { parameters: { type: "object", properties: {} }, execute: async () => ({ ok: true, result: "nodes" }), }, + { + name: "browser", + parameters: { type: "object", properties: {} }, + execute: async () => ({ ok: true, result: "browser" }), + }, { name: "owner_only_test", ownerOnly: true, @@ -181,7 +186,7 @@ vi.mock("../agents/openclaw-tools.js", () => { return { createOpenClawTools: (ctx: Record) => { lastCreateOpenClawToolsContext = ctx; - return tools; + return ctx.disablePluginTools ? tools.filter((tool) => tool.name !== "browser") : tools; }, }; }); @@ -878,4 +883,17 @@ describe("POST /tools/invoke", () => { expect(nodesRes.status).toBe(404); expect(nodesAdminRes.status).toBe(404); }); + + it("falls back to plugin-backed tools when a cataloged core tool has no core implementation", async () => { + setMainAllowedTools({ allow: ["browser"] }); + + const res = await invokeToolAuthed({ + tool: "browser", + sessionKey: "main", + }); + + const body = await expectOkInvokeResponse(res); + expect(body.result).toEqual({ ok: true, result: "browser" }); + expect(lastCreateOpenClawToolsContext?.disablePluginTools).toBe(false); + }); }); diff --git a/src/gateway/tools-invoke-http.ts b/src/gateway/tools-invoke-http.ts index e79964ad27a..5b10d5dd0c5 100644 --- a/src/gateway/tools-invoke-http.ts +++ b/src/gateway/tools-invoke-http.ts @@ -226,19 +226,25 @@ export async function handleToolsInvokeHttpRequest( // with the correct owner context and channel-action gates (e.g. Matrix set-profile) // work correctly for both owner and non-owner callers. const senderIsOwner = resolveOpenAiCompatibleHttpSenderIsOwner(req, requestAuth); - const { agentId, tools } = resolveGatewayScopedTools({ - cfg, - sessionKey, - messageProvider: messageChannel ?? undefined, - accountId, - agentTo, - agentThreadId, - allowGatewaySubagentBinding: true, - allowMediaInvokeCommands: true, - surface: "http", - disablePluginTools: isKnownCoreToolId(toolName), - senderIsOwner, - }); + const resolveTools = (disablePluginTools: boolean) => + resolveGatewayScopedTools({ + cfg, + sessionKey, + messageProvider: messageChannel ?? undefined, + accountId, + agentTo, + agentThreadId, + allowGatewaySubagentBinding: true, + allowMediaInvokeCommands: true, + surface: "http", + disablePluginTools, + senderIsOwner, + }); + const knownCoreTool = isKnownCoreToolId(toolName); + let { agentId, tools } = resolveTools(knownCoreTool); + if (knownCoreTool && !tools.some((candidate) => candidate.name === toolName)) { + ({ agentId, tools } = resolveTools(false)); + } const gatewayFiltered = applyOwnerOnlyToolPolicy(tools, senderIsOwner); const tool = gatewayFiltered.find((t) => t.name === toolName);