From 4ac9024de9cfcd58c80db45c782ac0750c9ed47b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 18 Mar 2026 01:46:50 -0700 Subject: [PATCH] Contracts: harden plugin registry loading --- .../contracts/registry.contract.test.ts | 6 + src/plugins/contracts/registry.ts | 250 ++++++++---------- 2 files changed, 120 insertions(+), 136 deletions(-) diff --git a/src/plugins/contracts/registry.contract.test.ts b/src/plugins/contracts/registry.contract.test.ts index 997aa560579..5c8d06785ce 100644 --- a/src/plugins/contracts/registry.contract.test.ts +++ b/src/plugins/contracts/registry.contract.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { loadPluginManifestRegistry } from "../manifest-registry.js"; import { resolvePluginWebSearchProviders } from "../web-search-providers.js"; import { + capabilityContractLoadError, imageGenerationProviderContractRegistry, mediaUnderstandingProviderContractRegistry, pluginRegistrationContractRegistry, @@ -85,6 +86,11 @@ function findRegistrationForPlugin(pluginId: string) { } describe("plugin contract registry", () => { + it("loads bundled non-provider capability registries without import-time failure", () => { + expect(capabilityContractLoadError).toBeUndefined(); + expect(pluginRegistrationContractRegistry.length).toBeGreaterThan(0); + }); + it("does not duplicate bundled provider ids", () => { const ids = providerContractRegistry.map((entry) => entry.provider.id); expect(ids).toEqual([...new Set(ids)]); diff --git a/src/plugins/contracts/registry.ts b/src/plugins/contracts/registry.ts index 142aa578b0f..acee90323b9 100644 --- a/src/plugins/contracts/registry.ts +++ b/src/plugins/contracts/registry.ts @@ -1,17 +1,8 @@ -import anthropicPlugin from "../../../extensions/anthropic/index.js"; -import bravePlugin from "../../../extensions/brave/index.js"; -import elevenLabsPlugin from "../../../extensions/elevenlabs/index.js"; -import firecrawlPlugin from "../../../extensions/firecrawl/index.js"; -import googlePlugin from "../../../extensions/google/index.js"; -import microsoftPlugin from "../../../extensions/microsoft/index.js"; -import minimaxPlugin from "../../../extensions/minimax/index.js"; -import mistralPlugin from "../../../extensions/mistral/index.js"; -import moonshotPlugin from "../../../extensions/moonshot/index.js"; -import openAIPlugin from "../../../extensions/openai/index.js"; -import perplexityPlugin from "../../../extensions/perplexity/index.js"; -import xaiPlugin from "../../../extensions/xai/index.js"; -import zaiPlugin from "../../../extensions/zai/index.js"; -import { createCapturedPluginRegistration } from "../captured-registration.js"; +import { createSubsystemLogger } from "../../logging/subsystem.js"; +import { withBundledPluginEnablementCompat } from "../bundled-compat.js"; +import { resolveBundledWebSearchPluginIds } from "../bundled-web-search.js"; +import { loadOpenClawPlugins } from "../loader.js"; +import { createPluginLoaderLogger } from "../logger.js"; import { resolvePluginProviders } from "../providers.js"; import type { ImageGenerationProviderPlugin, @@ -21,11 +12,6 @@ import type { WebSearchProviderPlugin, } from "../types.js"; -type RegistrablePlugin = { - id: string; - register: (api: ReturnType["api"]) => void; -}; - type CapabilityContractEntry = { pluginId: string; provider: T; @@ -52,52 +38,30 @@ type PluginRegistrationContractEntry = { toolNames: string[]; }; -const bundledWebSearchPlugins: Array = [ - { ...bravePlugin, credentialValue: "BSA-test" }, - { ...firecrawlPlugin, credentialValue: "fc-test" }, - { ...googlePlugin, credentialValue: "AIza-test" }, - { ...moonshotPlugin, credentialValue: "sk-test" }, - { ...perplexityPlugin, credentialValue: "pplx-test" }, - { ...xaiPlugin, credentialValue: "xai-test" }, -]; +const log = createSubsystemLogger("plugins"); -const bundledSpeechPlugins: RegistrablePlugin[] = [elevenLabsPlugin, microsoftPlugin, openAIPlugin]; +const BUNDLED_WEB_SEARCH_CREDENTIAL_VALUES: Readonly> = { + brave: "BSA-test", + firecrawl: "fc-test", + google: "AIza-test", + moonshot: "sk-test", + perplexity: "pplx-test", + xai: "xai-test", +}; -const bundledMediaUnderstandingPlugins: RegistrablePlugin[] = [ - anthropicPlugin, - googlePlugin, - minimaxPlugin, - mistralPlugin, - moonshotPlugin, - openAIPlugin, - zaiPlugin, -]; +const BUNDLED_SPEECH_PLUGIN_IDS = ["elevenlabs", "microsoft", "openai"] as const; +const BUNDLED_MEDIA_UNDERSTANDING_PLUGIN_IDS = [ + "anthropic", + "google", + "minimax", + "mistral", + "moonshot", + "openai", + "zai", +] as const; +const BUNDLED_IMAGE_GENERATION_PLUGIN_IDS = ["google", "openai"] as const; -const bundledImageGenerationPlugins: RegistrablePlugin[] = [googlePlugin, openAIPlugin]; - -function captureRegistrations(plugin: RegistrablePlugin) { - const captured = createCapturedPluginRegistration(); - plugin.register(captured.api); - return captured; -} - -function buildCapabilityContractRegistry(params: { - plugins: RegistrablePlugin[]; - select: (captured: ReturnType) => T[]; -}): CapabilityContractEntry[] { - return params.plugins.flatMap((plugin) => { - const captured = captureRegistrations(plugin); - return params.select(captured).map((provider) => ({ - pluginId: plugin.id, - provider, - })); - }); -} - -export const providerContractRegistry: ProviderContractEntry[] = buildCapabilityContractRegistry({ - plugins: [], - select: () => [], -}); +export const providerContractRegistry: ProviderContractEntry[] = []; export let providerContractLoadError: Error | undefined; @@ -143,6 +107,55 @@ export const providerContractCompatPluginIds = providerContractPluginIds.map((pl pluginId === "kimi-coding" ? "kimi" : pluginId, ); +const bundledCapabilityContractPluginIds = [ + ...new Set([ + ...providerContractCompatPluginIds, + ...resolveBundledWebSearchPluginIds({}), + ...BUNDLED_SPEECH_PLUGIN_IDS, + ...BUNDLED_MEDIA_UNDERSTANDING_PLUGIN_IDS, + ...BUNDLED_IMAGE_GENERATION_PLUGIN_IDS, + ]), +].toSorted((left, right) => left.localeCompare(right)); + +export let capabilityContractLoadError: Error | undefined; + +function loadBundledCapabilityRegistry() { + try { + capabilityContractLoadError = undefined; + return loadOpenClawPlugins({ + config: withBundledPluginEnablementCompat({ + config: { + plugins: { + enabled: true, + allow: bundledCapabilityContractPluginIds, + slots: { + memory: "none", + }, + }, + }, + pluginIds: bundledCapabilityContractPluginIds, + }), + cache: false, + activate: false, + logger: createPluginLoaderLogger(log), + }); + } catch (error) { + capabilityContractLoadError = error instanceof Error ? error : new Error(String(error)); + return loadOpenClawPlugins({ + config: { + plugins: { + enabled: false, + }, + }, + cache: false, + activate: false, + logger: createPluginLoaderLogger(log), + }); + } +} + +const loadedBundledCapabilityRegistry = loadBundledCapabilityRegistry(); + export function requireProviderContractProvider(providerId: string): ProviderPlugin { const provider = uniqueProviderContractProviders.find((entry) => entry.id === providerId); if (!provider) { @@ -183,85 +196,50 @@ export function resolveProviderContractProvidersForPluginIds( } export const webSearchProviderContractRegistry: WebSearchProviderContractEntry[] = - bundledWebSearchPlugins.flatMap((plugin) => { - const captured = captureRegistrations(plugin); - return captured.webSearchProviders.map((provider) => ({ - pluginId: plugin.id, - provider, - credentialValue: plugin.credentialValue, + loadedBundledCapabilityRegistry.webSearchProviders + .filter((entry) => entry.pluginId in BUNDLED_WEB_SEARCH_CREDENTIAL_VALUES) + .map((entry) => ({ + pluginId: entry.pluginId, + provider: entry.provider, + credentialValue: BUNDLED_WEB_SEARCH_CREDENTIAL_VALUES[entry.pluginId], })); - }); export const speechProviderContractRegistry: SpeechProviderContractEntry[] = - buildCapabilityContractRegistry({ - plugins: bundledSpeechPlugins, - select: (captured) => captured.speechProviders, - }); + loadedBundledCapabilityRegistry.speechProviders.map((entry) => ({ + pluginId: entry.pluginId, + provider: entry.provider, + })); export const mediaUnderstandingProviderContractRegistry: MediaUnderstandingProviderContractEntry[] = - buildCapabilityContractRegistry({ - plugins: bundledMediaUnderstandingPlugins, - select: (captured) => captured.mediaUnderstandingProviders, - }); + loadedBundledCapabilityRegistry.mediaUnderstandingProviders.map((entry) => ({ + pluginId: entry.pluginId, + provider: entry.provider, + })); export const imageGenerationProviderContractRegistry: ImageGenerationProviderContractEntry[] = - buildCapabilityContractRegistry({ - plugins: bundledImageGenerationPlugins, - select: (captured) => captured.imageGenerationProviders, - }); + loadedBundledCapabilityRegistry.imageGenerationProviders.map((entry) => ({ + pluginId: entry.pluginId, + provider: entry.provider, + })); -const bundledPluginRegistrationList = [ - ...new Map( - [ - ...bundledSpeechPlugins, - ...bundledMediaUnderstandingPlugins, - ...bundledImageGenerationPlugins, - ...bundledWebSearchPlugins, - ].map((plugin) => [plugin.id, plugin]), - ).values(), -]; - -export const pluginRegistrationContractRegistry: PluginRegistrationContractEntry[] = [ - ...new Map( - providerContractRegistry.map((entry) => [ - entry.pluginId, - { - pluginId: entry.pluginId, - providerIds: providerContractRegistry - .filter((candidate) => candidate.pluginId === entry.pluginId) - .map((candidate) => candidate.provider.id), - speechProviderIds: [] as string[], - mediaUnderstandingProviderIds: [] as string[], - imageGenerationProviderIds: [] as string[], - webSearchProviderIds: [] as string[], - toolNames: [] as string[], - }, - ]), - ).values(), -]; - -for (const plugin of bundledPluginRegistrationList) { - const captured = captureRegistrations(plugin); - const existing = pluginRegistrationContractRegistry.find((entry) => entry.pluginId === plugin.id); - const next = { - pluginId: plugin.id, - providerIds: captured.providers.map((provider) => provider.id), - speechProviderIds: captured.speechProviders.map((provider) => provider.id), - mediaUnderstandingProviderIds: captured.mediaUnderstandingProviders.map( - (provider) => provider.id, - ), - imageGenerationProviderIds: captured.imageGenerationProviders.map((provider) => provider.id), - webSearchProviderIds: captured.webSearchProviders.map((provider) => provider.id), - toolNames: captured.tools.map((tool) => tool.name), - }; - if (!existing) { - pluginRegistrationContractRegistry.push(next); - continue; - } - existing.providerIds = next.providerIds.length > 0 ? next.providerIds : existing.providerIds; - existing.speechProviderIds = next.speechProviderIds; - existing.mediaUnderstandingProviderIds = next.mediaUnderstandingProviderIds; - existing.imageGenerationProviderIds = next.imageGenerationProviderIds; - existing.webSearchProviderIds = next.webSearchProviderIds; - existing.toolNames = next.toolNames; -} +export const pluginRegistrationContractRegistry: PluginRegistrationContractEntry[] = + loadedBundledCapabilityRegistry.plugins + .filter( + (plugin) => + plugin.origin === "bundled" && + (plugin.providerIds.length > 0 || + plugin.speechProviderIds.length > 0 || + plugin.mediaUnderstandingProviderIds.length > 0 || + plugin.imageGenerationProviderIds.length > 0 || + plugin.webSearchProviderIds.length > 0 || + plugin.toolNames.length > 0), + ) + .map((plugin) => ({ + pluginId: plugin.id, + providerIds: plugin.providerIds, + speechProviderIds: plugin.speechProviderIds, + mediaUnderstandingProviderIds: plugin.mediaUnderstandingProviderIds, + imageGenerationProviderIds: plugin.imageGenerationProviderIds, + webSearchProviderIds: plugin.webSearchProviderIds, + toolNames: plugin.toolNames, + }));