From 3ec5558f53d8cb3105686786aa923e65c5e86894 Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 23 Apr 2026 06:35:15 +0100 Subject: [PATCH] fix: preserve external auth hook compatibility --- src/plugins/provider-runtime.test.ts | 30 +++++++++++++++++++ src/plugins/provider-runtime.ts | 45 ++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/plugins/provider-runtime.test.ts b/src/plugins/provider-runtime.test.ts index 12f6f2cea65..9f90541aa63 100644 --- a/src/plugins/provider-runtime.test.ts +++ b/src/plugins/provider-runtime.test.ts @@ -22,12 +22,20 @@ type IsPluginProvidersLoadInFlight = typeof import("./providers.runtime.js").isPluginProvidersLoadInFlight; type ResolveCatalogHookProviderPluginIds = typeof import("./providers.js").resolveCatalogHookProviderPluginIds; +type ResolveExternalAuthProfileCompatFallbackPluginIds = + typeof import("./providers.js").resolveExternalAuthProfileCompatFallbackPluginIds; +type ResolveExternalAuthProfileProviderPluginIds = + typeof import("./providers.js").resolveExternalAuthProfileProviderPluginIds; const resolvePluginProvidersMock = vi.fn((_) => [] as ProviderPlugin[]); const isPluginProvidersLoadInFlightMock = vi.fn((_) => false); const resolveCatalogHookProviderPluginIdsMock = vi.fn( (_) => [] as string[], ); +const resolveExternalAuthProfileCompatFallbackPluginIdsMock = + vi.fn((_) => [] as string[]); +const resolveExternalAuthProfileProviderPluginIdsMock = + vi.fn((_) => [] as string[]); let augmentModelCatalogWithProviderPlugins: typeof import("./provider-runtime.js").augmentModelCatalogWithProviderPlugins; let buildProviderAuthDoctorHintWithPlugin: typeof import("./provider-runtime.js").buildProviderAuthDoctorHintWithPlugin; @@ -238,6 +246,10 @@ describe("provider-runtime", () => { vi.doMock("./providers.js", () => ({ resolveCatalogHookProviderPluginIds: (params: unknown) => resolveCatalogHookProviderPluginIdsMock(params as never), + resolveExternalAuthProfileCompatFallbackPluginIds: (params: unknown) => + resolveExternalAuthProfileCompatFallbackPluginIdsMock(params as never), + resolveExternalAuthProfileProviderPluginIds: (params: unknown) => + resolveExternalAuthProfileProviderPluginIdsMock(params as never), })); vi.doMock("./providers.runtime.js", () => ({ resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never), @@ -301,6 +313,10 @@ describe("provider-runtime", () => { isPluginProvidersLoadInFlightMock.mockReturnValue(false); resolveCatalogHookProviderPluginIdsMock.mockReset(); resolveCatalogHookProviderPluginIdsMock.mockReturnValue([]); + resolveExternalAuthProfileCompatFallbackPluginIdsMock.mockReset(); + resolveExternalAuthProfileCompatFallbackPluginIdsMock.mockReturnValue([]); + resolveExternalAuthProfileProviderPluginIdsMock.mockReset(); + resolveExternalAuthProfileProviderPluginIdsMock.mockReturnValue([]); }); it("matches providers by alias for runtime hook lookup", () => { @@ -355,6 +371,19 @@ describe("provider-runtime", () => { ); }); + it("skips provider runtime loading when no plugin declares external auth hooks", () => { + expect( + resolveExternalAuthProfilesWithPlugins({ + env: process.env, + context: { + env: process.env, + store: { version: 1, profiles: {} }, + }, + }), + ).toEqual([]); + expect(resolvePluginProvidersMock).not.toHaveBeenCalled(); + }); + it("returns provider-prepared runtime auth for the matched provider", async () => { const prepareRuntimeAuth = vi.fn(async () => ({ apiKey: "runtime-token", @@ -679,6 +708,7 @@ describe("provider-runtime", () => { it("dispatches runtime hooks for the matched provider", async () => { resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["openai"]); + resolveExternalAuthProfileProviderPluginIdsMock.mockReturnValue(["demo"]); const prepareDynamicModel = vi.fn(async () => undefined); const createStreamFn = vi.fn(() => vi.fn()); const createEmbeddingProvider = vi.fn(async () => ({ diff --git a/src/plugins/provider-runtime.ts b/src/plugins/provider-runtime.ts index 64d5e33681f..5a7d4d4429e 100644 --- a/src/plugins/provider-runtime.ts +++ b/src/plugins/provider-runtime.ts @@ -7,6 +7,7 @@ import { import type { ProviderSystemPromptContribution } from "../agents/system-prompt-contribution.js"; import type { ModelProviderConfig } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { __testing as providerHookRuntimeTesting, @@ -21,7 +22,11 @@ import { import { resolveBundledProviderPolicySurface } from "./provider-public-artifacts.js"; import type { ProviderRuntimeModel } from "./provider-runtime-model.types.js"; import type { ProviderThinkingProfile } from "./provider-thinking.types.js"; -import { resolveCatalogHookProviderPluginIds } from "./providers.js"; +import { + resolveCatalogHookProviderPluginIds, + resolveExternalAuthProfileCompatFallbackPluginIds, + resolveExternalAuthProfileProviderPluginIds, +} from "./providers.js"; import { getActivePluginRegistryWorkspaceDirFromState } from "./runtime-state.js"; import { resolveRuntimeTextTransforms } from "./text-transforms.runtime.js"; import type { @@ -70,6 +75,9 @@ import type { ProviderWebSocketSessionPolicy, PluginTextTransforms, } from "./types.js"; + +const log = createSubsystemLogger("plugins/provider-runtime"); +const warnedExternalAuthFallbackPluginIds = new Set(); export { clearProviderRuntimeHookCache, prepareProviderExtraParams, @@ -755,14 +763,47 @@ export function resolveExternalAuthProfilesWithPlugins(params: { env?: NodeJS.ProcessEnv; context: ProviderResolveExternalAuthProfilesContext; }): ProviderExternalAuthProfile[] { + const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(); + const env = params.env ?? process.env; + const externalAuthPluginIds = resolveExternalAuthProfileProviderPluginIds({ + config: params.config, + workspaceDir, + env, + }); + const fallbackPluginIds = resolveExternalAuthProfileCompatFallbackPluginIds({ + config: params.config, + workspaceDir, + env, + }); + const pluginIds = [...new Set([...externalAuthPluginIds, ...fallbackPluginIds])].toSorted( + (left, right) => left.localeCompare(right), + ); + if (pluginIds.length === 0) { + return []; + } + const declaredPluginIds = new Set(externalAuthPluginIds); const matches: ProviderExternalAuthProfile[] = []; - for (const plugin of resolveProviderPluginsForHooks(params)) { + for (const plugin of resolveProviderPluginsForHooks({ + ...params, + workspaceDir, + env, + onlyPluginIds: pluginIds, + })) { const profiles = plugin.resolveExternalAuthProfiles?.(params.context) ?? plugin.resolveExternalOAuthProfiles?.(params.context); if (!profiles || profiles.length === 0) { continue; } + if (!declaredPluginIds.has(plugin.id) && !warnedExternalAuthFallbackPluginIds.has(plugin.id)) { + warnedExternalAuthFallbackPluginIds.add(plugin.id); + // Deprecated compatibility path for plugins that predate the manifest + // contract. Remove this warning with the fallback resolver after the + // externalAuthProviders migration window closes. + log.warn( + `Provider plugin "${plugin.id}" uses external auth hooks without declaring contracts.externalAuthProviders. This compatibility fallback is deprecated and will be removed in a future release.`, + ); + } matches.push(...profiles); } return matches;