From 0739cb19b72bdb731e70896df1a95d1d741e000a Mon Sep 17 00:00:00 2001 From: Doruk Ardahan <35905596+dorukardahan@users.noreply.github.com> Date: Sun, 3 May 2026 18:20:18 +0300 Subject: [PATCH] fix(tools): skip denied optional media factories --- CHANGELOG.md | 1 + .../openclaw-tools.media-factory-plan.test.ts | 29 ++++++++++ src/agents/openclaw-tools.ts | 55 ++++++++++++++----- ...tools.create-openclaw-coding-tools.test.ts | 15 +++++ src/agents/pi-tools.ts | 13 +++++ src/agents/tool-policy.ts | 19 +++++++ 6 files changed, 119 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c2dc5d3f5c..7b8c5ff6c8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Agents/tools: skip optional media and PDF tool factories when the effective tool denylist already blocks them, avoiding unnecessary hot-path setup for tools that will be filtered out before model use. - Gateway/performance: lazy-load early runtime discovery and shutdown-hook helpers, defer maintenance timers until after readiness, and trim duplicate plugin auto-enable work during Gateway startup. - QA/Mantis: add a `pnpm openclaw qa mantis discord-smoke` runner and manual GitHub workflow that verify the Mantis Discord bot can see the configured guild/channel, post a smoke message, add a reaction, and upload artifacts. - Gateway/performance: lazy-load the heavy cron runtime after the rest of Gateway startup, defer restart-sentinel refresh after readiness, and let the Gateway startup benchmark write per-run V8 CPU profiles with `--cpu-prof-dir`. diff --git a/src/agents/openclaw-tools.media-factory-plan.test.ts b/src/agents/openclaw-tools.media-factory-plan.test.ts index c58929c9f67..ad2916343f7 100644 --- a/src/agents/openclaw-tools.media-factory-plan.test.ts +++ b/src/agents/openclaw-tools.media-factory-plan.test.ts @@ -247,6 +247,35 @@ describe("optional media tool factory planning", () => { }); }); + it("skips tools that the resolved denylist blocks", () => { + const config: OpenClawConfig = {}; + installSnapshot(config, [ + createPlugin({ + id: "image-owner", + contracts: { imageGenerationProviders: ["image-owner"] }, + setupProviders: [{ id: "image-owner", envVars: ["IMAGE_OWNER_API_KEY"] }], + }), + createPlugin({ + id: "media-owner", + contracts: { mediaUnderstandingProviders: ["anthropic"] }, + setupProviders: [{ id: "anthropic", envVars: ["ANTHROPIC_API_KEY"] }], + }), + ]); + + expect( + __testing.resolveOptionalMediaToolFactoryPlan({ + config, + authStore: createAuthStore(["image-owner", "anthropic"]), + toolDenylist: ["image_generate", "pdf"], + }), + ).toEqual({ + imageGenerate: false, + videoGenerate: false, + musicGenerate: false, + pdf: false, + }); + }); + it("keeps auth-backed providers on the factory path", () => { const config: OpenClawConfig = {}; installSnapshot(config, [ diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index 748d0b9b72a..675d45c1423 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -91,6 +91,25 @@ function isToolAllowedByFactoryAllowlist(toolName: string, allowlist?: string[]) return expanded.has("*") || expanded.has(normalizeToolName(toolName)); } +function isToolDeniedByFactoryDenylist(toolName: string, denylist?: string[]): boolean { + if (!denylist || denylist.length === 0) { + return false; + } + const expanded = new Set(expandToolGroups(denylist)); + return expanded.has("*") || expanded.has(normalizeToolName(toolName)); +} + +function isToolAllowedByFactoryPolicy(params: { + toolName: string; + allowlist?: string[]; + denylist?: string[]; +}): boolean { + if (isToolDeniedByFactoryDenylist(params.toolName, params.denylist)) { + return false; + } + return isToolAllowedByFactoryAllowlist(params.toolName, params.allowlist); +} + function resolveImageToolFactoryAvailable(params: { config?: OpenClawConfig; agentDir?: string; @@ -159,21 +178,29 @@ function resolveOptionalMediaToolFactoryPlan(params: { workspaceDir?: string; authStore?: AuthProfileStore; toolAllowlist?: string[]; + toolDenylist?: string[]; }): OptionalMediaToolFactoryPlan { const defaults = params.config?.agents?.defaults; - const allowImageGenerate = isToolAllowedByFactoryAllowlist( - "image_generate", - params.toolAllowlist, - ); - const allowVideoGenerate = isToolAllowedByFactoryAllowlist( - "video_generate", - params.toolAllowlist, - ); - const allowMusicGenerate = isToolAllowedByFactoryAllowlist( - "music_generate", - params.toolAllowlist, - ); - const allowPdf = isToolAllowedByFactoryAllowlist("pdf", params.toolAllowlist); + const allowImageGenerate = isToolAllowedByFactoryPolicy({ + toolName: "image_generate", + allowlist: params.toolAllowlist, + denylist: params.toolDenylist, + }); + const allowVideoGenerate = isToolAllowedByFactoryPolicy({ + toolName: "video_generate", + allowlist: params.toolAllowlist, + denylist: params.toolDenylist, + }); + const allowMusicGenerate = isToolAllowedByFactoryPolicy({ + toolName: "music_generate", + allowlist: params.toolAllowlist, + denylist: params.toolDenylist, + }); + const allowPdf = isToolAllowedByFactoryPolicy({ + toolName: "pdf", + allowlist: params.toolAllowlist, + denylist: params.toolDenylist, + }); const explicitImageGeneration = hasExplicitToolModelConfig(defaults?.imageGenerationModel); const explicitVideoGeneration = hasExplicitToolModelConfig(defaults?.videoGenerationModel); const explicitMusicGeneration = hasExplicitToolModelConfig(defaults?.musicGenerationModel); @@ -256,6 +283,7 @@ export function createOpenClawTools( sandboxed?: boolean; config?: OpenClawConfig; pluginToolAllowlist?: string[]; + pluginToolDenylist?: string[]; /** Current channel ID for auto-threading. */ currentChannelId?: string; /** Current thread timestamp for auto-threading. */ @@ -348,6 +376,7 @@ export function createOpenClawTools( workspaceDir, authStore: options?.authProfileStore, toolAllowlist: options?.pluginToolAllowlist, + toolDenylist: options?.pluginToolDenylist, }); const imageToolAgentDir = options?.agentDir; const imageTool = resolveImageToolFactoryAvailable({ diff --git a/src/agents/pi-tools.create-openclaw-coding-tools.test.ts b/src/agents/pi-tools.create-openclaw-coding-tools.test.ts index 715d7945133..3fccb194352 100644 --- a/src/agents/pi-tools.create-openclaw-coding-tools.test.ts +++ b/src/agents/pi-tools.create-openclaw-coding-tools.test.ts @@ -180,6 +180,21 @@ describe("createOpenClawCodingTools", () => { ); }); + it("passes explicit denylist entries to OpenClaw tool factory planning", () => { + const createOpenClawToolsMock = vi.mocked(createOpenClawTools); + createOpenClawToolsMock.mockClear(); + + createOpenClawCodingTools({ + config: { tools: { deny: ["pdf"] } }, + }); + + expect(createOpenClawToolsMock).toHaveBeenCalledWith( + expect.objectContaining({ + pluginToolDenylist: expect.arrayContaining(["pdf"]), + }), + ); + }); + it("records core tool-prep stages for hot-path diagnostics", () => { const stages: string[] = []; diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 6dc9452f71c..3c760e7f6cc 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -69,6 +69,7 @@ import { import { applyOwnerOnlyToolPolicy, collectExplicitAllowlist, + collectExplicitDenylist, mergeAlsoAllowPolicy, normalizeToolName, resolveToolProfilePolicy, @@ -617,6 +618,17 @@ export function createOpenClawCodingTools(options?: { subagentPolicy, options?.runtimeToolAllowlist ? { allow: options.runtimeToolAllowlist } : undefined, ]); + const pluginToolDenylist = collectExplicitDenylist([ + profilePolicy, + providerProfilePolicy, + globalPolicy, + globalProviderPolicy, + agentPolicy, + agentProviderPolicy, + groupPolicy, + sandboxToolPolicy, + subagentPolicy, + ]); const pluginToolsOnly = includeCoreTools ? [] : resolveOpenClawPluginToolsForOptions({ @@ -705,6 +717,7 @@ export function createOpenClawCodingTools(options?: { sandboxed: !!sandbox, config: options?.config, pluginToolAllowlist, + pluginToolDenylist, currentChannelId: options?.currentChannelId, currentThreadTs: options?.currentThreadTs, currentMessageId: options?.currentMessageId, diff --git a/src/agents/tool-policy.ts b/src/agents/tool-policy.ts index eb3df6bdaef..4a06b62334d 100644 --- a/src/agents/tool-policy.ts +++ b/src/agents/tool-policy.ts @@ -112,6 +112,25 @@ export function collectExplicitAllowlist(policies: Array): string[] { + const entries: string[] = []; + for (const policy of policies) { + if (!policy?.deny) { + continue; + } + for (const value of policy.deny) { + if (typeof value !== "string") { + continue; + } + const trimmed = value.trim(); + if (trimmed) { + entries.push(trimmed); + } + } + } + return entries; +} + export function buildPluginToolGroups(params: { tools: T[]; toolMeta: (tool: T) => { pluginId: string } | undefined;