diff --git a/src/agents/openclaw-tools.media-factory-plan.test.ts b/src/agents/openclaw-tools.media-factory-plan.test.ts index 70602bb5f5c..72fc1847b42 100644 --- a/src/agents/openclaw-tools.media-factory-plan.test.ts +++ b/src/agents/openclaw-tools.media-factory-plan.test.ts @@ -393,6 +393,140 @@ describe("optional media tool factory planning", () => { }); }); + it("does not expose manifest-backed generation providers when plugins are globally disabled", () => { + const config: OpenClawConfig = { + plugins: { + enabled: false, + entries: { + comfy: { + config: { + mode: "local", + workflow: { "1": { inputs: {} } }, + promptNodeId: "1", + }, + }, + }, + }, + }; + const configSignals = [ + { + rootPath: "plugins.entries.comfy.config", + mode: { + path: "mode", + default: "local", + allowed: ["local"], + }, + requiredAny: ["workflow", "workflowPath"], + required: ["promptNodeId"], + }, + ]; + installSnapshot(config, [ + createPlugin({ + id: "comfy", + contracts: { + imageGenerationProviders: ["comfy"], + videoGenerationProviders: ["comfy"], + musicGenerationProviders: ["comfy"], + }, + imageGenerationProviderMetadata: { + comfy: { configSignals }, + }, + videoGenerationProviderMetadata: { + comfy: { configSignals }, + }, + musicGenerationProviderMetadata: { + comfy: { configSignals }, + }, + }), + ]); + + expect( + __testing.resolveOptionalMediaToolFactoryPlan({ + config, + authStore: createAuthStore(), + }), + ).toEqual({ + imageGenerate: false, + videoGenerate: false, + musicGenerate: false, + pdf: false, + }); + expect( + createOpenClawTools({ + config, + authProfileStore: createAuthStore(), + pluginToolAllowlist: ["image_generate", "video_generate", "music_generate"], + }).map((tool) => tool.name), + ).not.toEqual(expect.arrayContaining(["image_generate", "video_generate", "music_generate"])); + }); + + it("does not count unresolved SecretRef config signals as configured", () => { + vi.stubEnv("COMFY_TEST_API_KEY", ""); + const config: OpenClawConfig = { + plugins: { + entries: { + comfy: { + config: { + mode: "cloud", + apiKey: { source: "env", provider: "default", id: "COMFY_TEST_API_KEY" }, + workflow: { "1": { inputs: {} } }, + promptNodeId: "1", + }, + }, + }, + }, + }; + const configSignals = [ + { + rootPath: "plugins.entries.comfy.config", + mode: { + path: "mode", + allowed: ["cloud"], + }, + requiredAny: ["workflow", "workflowPath"], + required: ["promptNodeId", "apiKey"], + }, + ]; + installSnapshot(config, [ + createPlugin({ + id: "comfy", + contracts: { + imageGenerationProviders: ["comfy"], + videoGenerationProviders: ["comfy"], + musicGenerationProviders: ["comfy"], + }, + imageGenerationProviderMetadata: { + comfy: { configSignals }, + }, + videoGenerationProviderMetadata: { + comfy: { configSignals }, + }, + musicGenerationProviderMetadata: { + comfy: { configSignals }, + }, + }), + ]); + + expect( + __testing.resolveOptionalMediaToolFactoryPlan({ + config, + authStore: createAuthStore(), + }), + ).toEqual({ + imageGenerate: false, + videoGenerate: false, + musicGenerate: false, + pdf: false, + }); + expect( + createOpenClawTools({ + config, + authProfileStore: createAuthStore(), + pluginToolAllowlist: ["image_generate", "video_generate", "music_generate"], + }).map((tool) => tool.name), + ).not.toEqual(expect.arrayContaining(["image_generate", "video_generate", "music_generate"])); + }); + it.each([ { name: "legacy local provider config", diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index e24d833ea7f..e8573853a70 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -147,6 +147,14 @@ function resolveOptionalMediaToolFactoryPlan(params: { musicGenerate: allowMusicGenerate, pdf: allowPdf, }; + if (params.config?.plugins?.enabled === false) { + return { + imageGenerate: false, + videoGenerate: false, + musicGenerate: false, + pdf: false, + }; + } if (!params.authStore) { return fallbackPlan; } diff --git a/src/agents/tools/manifest-capability-availability.ts b/src/agents/tools/manifest-capability-availability.ts index deec0dc0093..01cd151b3ff 100644 --- a/src/agents/tools/manifest-capability-availability.ts +++ b/src/agents/tools/manifest-capability-availability.ts @@ -1,8 +1,10 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { coerceSecretRef, type SecretRef } from "../../config/types.secrets.js"; import { getCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js"; import { isManifestPluginAvailableForControlPlane } from "../../plugins/manifest-contract-eligibility.js"; import type { PluginManifestRecord } from "../../plugins/manifest-registry.js"; import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js"; +import { resolveDefaultSecretProviderAlias } from "../../secrets/ref-contract.js"; import { listProfilesForProvider } from "../auth-profiles.js"; import type { AuthProfileStore } from "../auth-profiles/types.js"; @@ -57,17 +59,44 @@ function readEffectiveConfig(params: { return isRecord(overlay) ? { ...root, ...overlay } : root; } -function hasConfiguredValue(value: unknown): boolean { - if (typeof value === "string") { - return value.trim().length > 0; +function canResolveEnvSecretRefInConfigPath(params: { + config?: OpenClawConfig; + ref: SecretRef; +}): boolean { + if (params.ref.source !== "env") { + return false; } - if (Array.isArray(value)) { - return value.length > 0; + const providerConfig = params.config?.secrets?.providers?.[params.ref.provider]; + if (!providerConfig) { + return params.ref.provider === resolveDefaultSecretProviderAlias(params.config ?? {}, "env"); } - if (isRecord(value)) { - return Object.keys(value).length > 0; + if (providerConfig.source !== "env") { + return false; } - return value !== undefined && value !== null; + const allowlist = providerConfig.allowlist; + return !allowlist || allowlist.includes(params.ref.id); +} + +function hasConfiguredValue(params: { config?: OpenClawConfig; value: unknown }): boolean { + const secretRef = coerceSecretRef(params.value, params.config?.secrets?.defaults); + if (secretRef) { + return ( + canResolveEnvSecretRefInConfigPath({ + config: params.config, + ref: secretRef, + }) && Boolean(process.env[secretRef.id]?.trim()) + ); + } + if (typeof params.value === "string") { + return params.value.trim().length > 0; + } + if (Array.isArray(params.value)) { + return params.value.length > 0; + } + if (isRecord(params.value)) { + return Object.keys(params.value).length > 0; + } + return params.value !== undefined && params.value !== null; } function configSignalPasses(params: { @@ -99,14 +128,24 @@ function configSignalPasses(params: { } } for (const requiredPath of params.signal.required ?? []) { - if (!hasConfiguredValue(readPath(effectiveConfig, requiredPath))) { + if ( + !hasConfiguredValue({ + config: params.config, + value: readPath(effectiveConfig, requiredPath), + }) + ) { return false; } } const requiredAny = params.signal.requiredAny ?? []; if ( requiredAny.length > 0 && - !requiredAny.some((path) => hasConfiguredValue(readPath(effectiveConfig, path))) + !requiredAny.some((path) => + hasConfiguredValue({ + config: params.config, + value: readPath(effectiveConfig, path), + }), + ) ) { return false; } @@ -210,6 +249,9 @@ export function hasSnapshotCapabilityAvailability(params: { config?: OpenClawConfig; authStore?: AuthProfileStore; }): boolean { + if (params.config?.plugins?.enabled === false) { + return false; + } for (const plugin of params.snapshot.plugins) { if ( !isManifestPluginAvailableForControlPlane({ @@ -266,6 +308,9 @@ export function hasSnapshotProviderEnvAvailability(params: { providerId: string; config?: OpenClawConfig; }): boolean { + if (params.config?.plugins?.enabled === false) { + return false; + } for (const plugin of params.snapshot.plugins) { if ( !isManifestPluginAvailableForControlPlane({ diff --git a/src/agents/tools/media-tool-shared.ts b/src/agents/tools/media-tool-shared.ts index ecd5a9acc80..26a414ea1f3 100644 --- a/src/agents/tools/media-tool-shared.ts +++ b/src/agents/tools/media-tool-shared.ts @@ -307,6 +307,9 @@ export function hasGenerationToolAvailability(params: { providers?: CapabilityProvider[] | (() => CapabilityProvider[]); providerKey: GenerationCapabilityProviderKey; }): boolean { + if (params.cfg?.plugins?.enabled === false) { + return false; + } if (hasToolModelConfig(coerceToolModelConfig(params.modelConfig))) { return true; }