fix(tools): skip denied optional media factories

This commit is contained in:
Doruk Ardahan
2026-05-03 18:20:18 +03:00
committed by Peter Steinberger
parent 7f6798094c
commit 0739cb19b7
6 changed files with 119 additions and 13 deletions

View File

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

View File

@@ -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, [

View File

@@ -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({

View File

@@ -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[] = [];

View File

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

View File

@@ -112,6 +112,25 @@ export function collectExplicitAllowlist(policies: Array<ToolPolicyLike | undefi
return entries;
}
export function collectExplicitDenylist(policies: Array<ToolPolicyLike | undefined>): 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<T extends { name: string }>(params: {
tools: T[];
toolMeta: (tool: T) => { pluginId: string } | undefined;