fix: align manifest media availability with runtime

This commit is contained in:
Shakker
2026-05-01 22:04:21 +01:00
parent 88a8211fac
commit 7028f1b485
4 changed files with 200 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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