From e5e6fe1d52d8224a3979359feee402e0bd836d5d Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 29 Apr 2026 07:43:52 +0100 Subject: [PATCH] fix: harden provider discovery metadata scope --- ...els-config.applies-config-env-vars.test.ts | 5 ++ src/agents/models-config.ts | 4 +- .../capability-provider-runtime.test.ts | 63 +++++++++++-------- src/plugins/capability-provider-runtime.ts | 3 - 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/agents/models-config.applies-config-env-vars.test.ts b/src/agents/models-config.applies-config-env-vars.test.ts index 544cf8d4688..bd036dcfb2d 100644 --- a/src/agents/models-config.applies-config-env-vars.test.ts +++ b/src/agents/models-config.applies-config-env-vars.test.ts @@ -127,6 +127,7 @@ describe("models-config", () => { it("threads startup provider discovery scope into implicit provider discovery", async () => { let observedProviderIds: readonly string[] | undefined; + let observedEntriesOnly: boolean | undefined; let observedTimeoutMs: number | undefined; await resolveProvidersForModelsJsonWithDeps( @@ -135,14 +136,17 @@ describe("models-config", () => { agentDir: "/tmp/openclaw-models-config-env-vars-test", env: {}, providerDiscoveryProviderIds: ["openai"], + providerDiscoveryEntriesOnly: true, providerDiscoveryTimeoutMs: 5000, }, { resolveImplicitProviders: async ({ providerDiscoveryProviderIds, + providerDiscoveryEntriesOnly, providerDiscoveryTimeoutMs, }) => { observedProviderIds = providerDiscoveryProviderIds; + observedEntriesOnly = providerDiscoveryEntriesOnly; observedTimeoutMs = providerDiscoveryTimeoutMs; return {}; }, @@ -150,6 +154,7 @@ describe("models-config", () => { ); expect(observedProviderIds).toEqual(["openai"]); + expect(observedEntriesOnly).toBe(true); expect(observedTimeoutMs).toBe(5000); }); diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index ad347594c1d..cfba621280a 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -194,7 +194,9 @@ export async function ensureOpenClawModelsJson( ...(options.providerDiscoveryTimeoutMs !== undefined ? { providerDiscoveryTimeoutMs: options.providerDiscoveryTimeoutMs } : {}), - ...(options.providerDiscoveryEntriesOnly === true ? { providerDiscoveryEntriesOnly: true } : {}), + ...(options.providerDiscoveryEntriesOnly === true + ? { providerDiscoveryEntriesOnly: true } + : {}), }); const cacheKey = modelsJsonReadyCacheKey(targetPath, fingerprint); const cached = MODELS_JSON_STATE.readyCache.get(cacheKey); diff --git a/src/plugins/capability-provider-runtime.test.ts b/src/plugins/capability-provider-runtime.test.ts index 7c16cd29299..33655de6562 100644 --- a/src/plugins/capability-provider-runtime.test.ts +++ b/src/plugins/capability-provider-runtime.test.ts @@ -103,7 +103,6 @@ function expectBundledCompatLoadPath(params: { config: params.enablementCompat, onlyPluginIds: ["openai"], activate: false, - onlyPluginIds: ["openai"], }); } @@ -123,13 +122,17 @@ function createCompatChainConfig() { return { cfg, allowlistCompat, enablementCompat }; } -function setBundledCapabilityFixture(contractKey: string) { +function setBundledCapabilityFixture( + contractKey: string, + pluginId = "openai", + providerId = pluginId, +) { mocks.loadPluginManifestRegistry.mockReturnValue({ plugins: [ { - id: "openai", + id: pluginId, origin: "bundled", - contracts: { [contractKey]: ["openai"] }, + contracts: { [contractKey]: [providerId] }, }, { id: "custom-plugin", @@ -231,7 +234,7 @@ describe("resolvePluginCapabilityProviders", () => { expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(); }); - it("uses active non-speech capability providers even when cfg is passed", () => { + it("uses active non-speech capability providers even when cfg has explicit plugin entries", () => { const active = createEmptyPluginRegistry(); active.mediaUnderstandingProviders.push({ pluginId: "deepgram", @@ -247,6 +250,7 @@ describe("resolvePluginCapabilityProviders", () => { const providers = resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders", cfg: { + plugins: { entries: { deepgram: { enabled: true } } }, tools: { media: { models: [{ provider: "deepgram" }], @@ -404,7 +408,6 @@ describe("resolvePluginCapabilityProviders", () => { }), onlyPluginIds: ["microsoft"], activate: false, - onlyPluginIds: ["microsoft"], }); }); @@ -605,16 +608,7 @@ describe("resolvePluginCapabilityProviders", () => { nativeDocumentInputs: ["pdf"], }, } as never); - mocks.loadPluginManifestRegistry.mockReturnValue({ - plugins: [ - { - id: "google", - origin: "bundled", - contracts: { mediaUnderstandingProviders: ["google"] }, - }, - ] as never, - diagnostics: [], - }); + setBundledCapabilityFixture("mediaUnderstandingProviders", "google", "google"); mocks.withBundledPluginEnablementCompat.mockReturnValue(compatConfig); mocks.withBundledPluginVitestCompat.mockReturnValue(compatConfig); mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) => @@ -632,7 +626,29 @@ describe("resolvePluginCapabilityProviders", () => { config: compatConfig, onlyPluginIds: ["google"], activate: false, - onlyPluginIds: ["openai"], + }); + }); + + it.each([ + "imageGenerationProviders", + "videoGenerationProviders", + "musicGenerationProviders", + ] as const)("uses an explicit empty plugin scope for %s when no bundled owner exists", (key) => { + const providers = resolvePluginCapabilityProviders({ + key, + cfg: {} as OpenClawConfig, + }); + + expectNoResolvedCapabilityProviders(providers as Array<{ id: string }>); + expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({ + config: {}, + env: process.env, + }); + expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(); + expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ + config: expect.anything(), + onlyPluginIds: [], + activate: false, }); }); @@ -667,9 +683,8 @@ describe("resolvePluginCapabilityProviders", () => { const snapshotLoadOptions = mocks.resolveRuntimePluginRegistry.mock.calls .map(([options]) => options) - .filter( - (options): options is { activate: boolean; onlyPluginIds?: string[] } => - Boolean(options && typeof options === "object" && "activate" in options), + .filter((options): options is { activate: boolean; onlyPluginIds?: string[] } => + Boolean(options && typeof options === "object" && "activate" in options), ); expect(snapshotLoadOptions.map((options) => options.onlyPluginIds)).toEqual([ ["minimax", "openai"], @@ -702,11 +717,10 @@ describe("resolvePluginCapabilityProviders", () => { const snapshotLoadOptions = mocks.resolveRuntimePluginRegistry.mock.calls .map(([options]) => options) - .filter( - (options): options is { activate: boolean; onlyPluginIds?: string[] } => - Boolean(options && typeof options === "object" && "activate" in options), + .filter((options): options is { activate: boolean; onlyPluginIds?: string[] } => + Boolean(options && typeof options === "object" && "activate" in options), ); - expect(snapshotLoadOptions.map((options) => options.onlyPluginIds)).toEqual([["openai"]]); + expect(snapshotLoadOptions.map((options) => options.onlyPluginIds)).toEqual([["openai"], []]); }); it("loads only the bundled owner plugin for a targeted provider lookup", () => { @@ -772,7 +786,6 @@ describe("resolvePluginCapabilityProviders", () => { config: enablementCompat, onlyPluginIds: ["google"], activate: false, - onlyPluginIds: ["google"], }); }); }); diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index bea4fa6e13f..8d30641b35b 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -262,9 +262,6 @@ export function resolvePluginCapabilityProviders entry.provider) as CapabilityProviderForKey[]; - } const compatConfig = resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg,