fix(gateway): invoke plugin-backed catalog tools

Co-authored-by: chat2way <chat2way@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-04-25 05:28:03 +01:00
parent 6602092a40
commit 98a99765af
3 changed files with 39 additions and 14 deletions

View File

@@ -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.

View File

@@ -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<string, unknown>) => {
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);
});
});

View File

@@ -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);