From 8f36231e583a5683df8a03cf90afd9a546a3441c Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 4 Jun 2026 02:42:09 +0200 Subject: [PATCH] fix(plugins): guard provider discovery credential metadata --- .../provider-discovery.runtime.test.ts | 52 +++++++++++++++++++ src/plugins/provider-discovery.runtime.ts | 22 ++++---- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/src/plugins/provider-discovery.runtime.test.ts b/src/plugins/provider-discovery.runtime.test.ts index b2ab5dba5843..e19e63e65f4a 100644 --- a/src/plugins/provider-discovery.runtime.test.ts +++ b/src/plugins/provider-discovery.runtime.test.ts @@ -170,6 +170,19 @@ function createManifestPluginWithoutDiscovery(params: { }; } +function createPoisonedCredentialMetadataPlugin(params: { + id: string; + poison: "setup" | "providerAuthEnvVars"; +}): PluginManifestRecord { + const plugin = createManifestPluginWithoutDiscovery({ id: params.id }); + return Object.defineProperty(plugin, params.poison, { + configurable: true, + get() { + throw new Error(`provider discovery ${params.poison} metadata exploded`); + }, + }); +} + function createProvider(params: { id: string; mode: "static" | "catalog" }): ProviderPlugin { const hook = { run: async () => ({ @@ -548,6 +561,45 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => { expect(params.onlyPluginIds).toEqual(["kilocode"]); }); + it("skips unreadable credential metadata while selecting full-load fallbacks", () => { + const codexEntryProvider = createProvider({ id: "codex", mode: "catalog" }); + const fullProviders = [createProvider({ id: "kilocode", mode: "catalog" })]; + mocks.resolveDiscoveredProviderPluginIds.mockReturnValue([ + "codex", + "broken-setup", + "broken-auth-vars", + "kilocode", + ]); + mocks.loadPluginMetadataSnapshot.mockReturnValue({ + index: { plugins: [] }, + manifestRegistry: { + plugins: [ + createManifestPlugin("codex"), + createPoisonedCredentialMetadataPlugin({ id: "broken-setup", poison: "setup" }), + createPoisonedCredentialMetadataPlugin({ + id: "broken-auth-vars", + poison: "providerAuthEnvVars", + }), + createManifestPluginWithoutDiscovery({ + id: "kilocode", + setupProviders: [{ id: "kilocode", envVars: ["KILOCODE_API_KEY"] }], + }), + ], + diagnostics: [], + }, + }); + mocks.loadSource.mockReturnValue(codexEntryProvider); + mocks.resolvePluginProviders.mockReturnValue(fullProviders); + + expect( + resolvePluginDiscoveryProvidersRuntime({ + env: { KILOCODE_API_KEY: "sk-test" } as NodeJS.ProcessEnv, + }), + ).toEqual([{ ...codexEntryProvider, pluginId: "codex" }, ...fullProviders]); + expect(mocks.resolvePluginProviders).toHaveBeenCalledTimes(1); + expect(requireResolvePluginProvidersParams().onlyPluginIds).toEqual(["kilocode"]); + }); + it("enables bundled provider Vitest compat when falling back from discovery entries", () => { const fullProviders = [createProvider({ id: "deepseek", mode: "catalog" })]; mocks.resolveDiscoveredProviderPluginIds.mockReturnValue([]); diff --git a/src/plugins/provider-discovery.runtime.ts b/src/plugins/provider-discovery.runtime.ts index ae55c4e66be0..f40739f1031d 100644 --- a/src/plugins/provider-discovery.runtime.ts +++ b/src/plugins/provider-discovery.runtime.ts @@ -1,10 +1,10 @@ import path from "node:path"; import type { NormalizedModelCatalogRow } from "@openclaw/model-catalog-core/model-catalog-types"; import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; +import { sortUniqueStrings } from "../../packages/normalization-core/src/string-normalization.js"; import type { ModelDefinitionConfig, ModelProviderConfig } from "../config/types.models.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { planManifestModelCatalogRows } from "../model-catalog/manifest-planner.js"; -import { sortUniqueStrings } from "../../packages/normalization-core/src/string-normalization.js"; import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js"; import type { PluginManifestRecord } from "./manifest-registry.js"; import { clearNativeRequireJavaScriptModuleCache } from "./native-module-require.js"; @@ -123,14 +123,18 @@ function hasProviderAuthEnvCredential( plugin: PluginManifestRecord, env: NodeJS.ProcessEnv, ): boolean { - const envVars = [ - ...(plugin.setup?.providers ?? []).flatMap((provider) => provider.envVars ?? []), - ...Object.values(plugin.providerAuthEnvVars ?? {}).flat(), - ]; - return envVars.some((name) => { - const value = env[name]?.trim(); - return value !== undefined && value !== ""; - }); + try { + const envVars = [ + ...(plugin.setup?.providers ?? []).flatMap((provider) => provider.envVars ?? []), + ...Object.values(plugin.providerAuthEnvVars ?? {}).flat(), + ]; + return envVars.some((name) => { + const value = env[name]?.trim(); + return value !== undefined && value !== ""; + }); + } catch { + return false; + } } function modelDefinitionCostFromManifestRow(