From 29988335fc8b475637413b7845e6aebdbbf41b2a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 05:45:37 -0700 Subject: [PATCH] feat(plugins): resolve provider owners from registry --- CHANGELOG.md | 1 + src/plugins/providers.test.ts | 7 ++ src/plugins/providers.ts | 227 +++++++++++++++++++++++----------- 3 files changed, 162 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5869353fe3e..6b83563edbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai - Plugins/CLI: add `openclaw plugins registry` for explicit persisted-registry inspection and `--refresh` repair without making normal startup rescan plugin locations. Thanks @vincentkoc. - Plugins/CLI: make `openclaw plugins list` read the cold persisted registry snapshot by default, leaving module-aware diagnostics to `plugins doctor` and `plugins inspect`. Thanks @vincentkoc. - Plugins/startup: move gateway startup plugin planning onto the versioned cold registry index, with postinstall repair for older registry files that predate startup metadata. Thanks @vincentkoc. +- Providers/plugins: resolve provider ownership, provider discovery scopes, and catalog-hook provider ids from the cold plugin registry instead of rescanning manifests on those paths. Thanks @vincentkoc. - Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna. - Diagnostics/OTEL: emit bounded exec-process diagnostics and export them as `openclaw.exec` spans without exposing command text, working directories, or container identifiers. (#71451) Thanks @vincentkoc and @jlapenna. - Diagnostics/OTEL: support `OPENCLAW_OTEL_PRELOADED=1` so the plugin can reuse an already-registered OpenTelemetry SDK while keeping OpenClaw diagnostic listeners wired. (#71450) Thanks @vincentkoc and @jlapenna. diff --git a/src/plugins/providers.test.ts b/src/plugins/providers.test.ts index 5918fef70a8..4f3200e8822 100644 --- a/src/plugins/providers.test.ts +++ b/src/plugins/providers.test.ts @@ -311,6 +311,13 @@ describe("resolvePluginProviders", () => { resolveManifestContractPluginIds: (...args: Parameters) => resolveManifestContractPluginIdsMock(...args), })); + vi.doMock("./installed-plugin-index-store.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readPersistedInstalledPluginIndexSync: () => null, + }; + }); ({ resolveActivatableProviderOwnerPluginIds, resolveOwningPluginIdsForProvider, diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 9528ba5dee9..6175f0f3c0b 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -8,10 +8,16 @@ import { } from "./manifest-owner-policy.js"; import { loadPluginManifestRegistry, - resolveManifestContractPluginIds, type PluginManifestRecord, type PluginManifestRegistry, } from "./manifest-registry.js"; +import { + loadPluginRegistrySnapshot, + resolvePluginContributionOwners, + resolveProviderOwners, + type PluginRegistryRecord, + type PluginRegistrySnapshot, +} from "./plugin-registry.js"; import { createPluginIdScopeSet } from "./plugin-scope.js"; type ProviderManifestLoadParams = { @@ -20,6 +26,9 @@ type ProviderManifestLoadParams = { env?: PluginLoadOptions["env"]; }; type NormalizedPluginsConfig = ReturnType; +type ProviderRegistryLoadParams = ProviderManifestLoadParams & { + onlyPluginIds?: readonly string[]; +}; function loadProviderManifestRegistry(params: ProviderManifestLoadParams): PluginManifestRegistry { return loadPluginManifestRegistry({ @@ -29,33 +38,39 @@ function loadProviderManifestRegistry(params: ProviderManifestLoadParams): Plugi }); } -function loadScopedProviderManifestRegistry( - params: ProviderManifestLoadParams & { onlyPluginIds?: readonly string[] }, -): { - registry: PluginManifestRegistry; +function loadProviderRegistrySnapshot(params: ProviderManifestLoadParams): PluginRegistrySnapshot { + return loadPluginRegistrySnapshot({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }); +} + +function loadScopedProviderRegistry(params: ProviderRegistryLoadParams): { + registry: PluginRegistrySnapshot; onlyPluginIdSet: ReturnType; } { return { - registry: loadProviderManifestRegistry(params), + registry: loadProviderRegistrySnapshot(params), onlyPluginIdSet: createPluginIdScopeSet(params.onlyPluginIds), }; } -function listManifestPluginIds( - registry: PluginManifestRegistry, - predicate: (plugin: PluginManifestRecord) => boolean, +function listRegistryPluginIds( + registry: PluginRegistrySnapshot, + predicate: (plugin: PluginRegistryRecord) => boolean, ): string[] { return registry.plugins .filter(predicate) - .map((plugin) => plugin.id) + .map((plugin) => plugin.pluginId) .toSorted((left, right) => left.localeCompare(right)); } function resolveProviderOwnerPluginIds( - params: ProviderManifestLoadParams & { + params: ProviderRegistryLoadParams & { pluginIds: readonly string[]; isEligible: ( - plugin: PluginManifestRecord, + plugin: PluginRegistryRecord, normalizedConfig: NormalizedPluginsConfig, ) => boolean; }, @@ -64,14 +79,40 @@ function resolveProviderOwnerPluginIds( return []; } const pluginIdSet = new Set(params.pluginIds); - const registry = loadProviderManifestRegistry(params); + const registry = loadProviderRegistrySnapshot(params); const normalizedConfig = normalizePluginsConfig(params.config?.plugins); - return listManifestPluginIds( + return listRegistryPluginIds( registry, - (plugin) => pluginIdSet.has(plugin.id) && params.isEligible(plugin, normalizedConfig), + (plugin) => pluginIdSet.has(plugin.pluginId) && params.isEligible(plugin, normalizedConfig), ); } +function recordHasProviderSurface(plugin: PluginRegistryRecord): boolean { + return plugin.contributions.providers.length > 0; +} + +function resolveEffectiveRegistryPluginActivation(params: { + plugin: PluginRegistryRecord; + normalizedConfig: NormalizedPluginsConfig; + rootConfig?: PluginLoadOptions["config"]; +}) { + return resolveEffectivePluginActivationState({ + id: params.plugin.pluginId, + origin: params.plugin.origin, + config: params.normalizedConfig, + rootConfig: params.rootConfig, + enabledByDefault: params.plugin.enabledByDefault, + }); +} + +function toManifestOwnerRecord(plugin: PluginRegistryRecord) { + return { + id: plugin.pluginId, + origin: plugin.origin, + enabledByDefault: plugin.enabledByDefault, + }; +} + export function withBundledProviderVitestCompat(params: { config: PluginLoadOptions["config"]; pluginIds: readonly string[]; @@ -86,13 +127,13 @@ export function resolveBundledProviderCompatPluginIds(params: { env?: PluginLoadOptions["env"]; onlyPluginIds?: readonly string[]; }): string[] { - const { registry, onlyPluginIdSet } = loadScopedProviderManifestRegistry(params); - return listManifestPluginIds( + const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params); + return listRegistryPluginIds( registry, (plugin) => plugin.origin === "bundled" && - plugin.providers.length > 0 && - (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)), + recordHasProviderSurface(plugin) && + (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.pluginId)), ); } @@ -102,19 +143,17 @@ export function resolveEnabledProviderPluginIds(params: { env?: PluginLoadOptions["env"]; onlyPluginIds?: readonly string[]; }): string[] { - const { registry, onlyPluginIdSet } = loadScopedProviderManifestRegistry(params); + const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params); const normalizedConfig = normalizePluginsConfig(params.config?.plugins); - return listManifestPluginIds( + return listRegistryPluginIds( registry, (plugin) => - plugin.providers.length > 0 && - (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) && - resolveEffectivePluginActivationState({ - id: plugin.id, - origin: plugin.origin, - config: normalizedConfig, + recordHasProviderSurface(plugin) && + (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.pluginId)) && + resolveEffectiveRegistryPluginActivation({ + plugin, + normalizedConfig, rootConfig: params.config, - enabledByDefault: plugin.enabledByDefault, }).activated, ); } @@ -124,11 +163,29 @@ export function resolveExternalAuthProfileProviderPluginIds(params: { workspaceDir?: string; env?: PluginLoadOptions["env"]; }): string[] { - return resolveManifestContractPluginIds({ + return resolveRegistryManifestContractPluginIds({ + ...params, contract: "externalAuthProviders", - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, + }); +} + +function resolveRegistryManifestContractPluginIds(params: { + contract: string; + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; + origin?: PluginRegistryRecord["origin"]; + onlyPluginIds?: readonly string[]; +}): string[] { + const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params); + return listRegistryPluginIds(registry, (plugin) => { + if (params.origin && plugin.origin !== params.origin) { + return false; + } + if (onlyPluginIdSet && !onlyPluginIdSet.has(plugin.pluginId)) { + return false; + } + return plugin.contributions.contracts.includes(params.contract); }); } @@ -143,14 +200,14 @@ export function resolveExternalAuthProfileCompatFallbackPluginIds(params: { // this with the warning path in provider-runtime after the migration window. const declaredPluginIds = params.declaredPluginIds ?? new Set(resolveExternalAuthProfileProviderPluginIds(params)); - const registry = loadProviderManifestRegistry(params); + const registry = loadProviderRegistrySnapshot(params); const normalizedConfig = normalizePluginsConfig(params.config?.plugins); - return listManifestPluginIds( + return listRegistryPluginIds( registry, (plugin) => plugin.origin !== "bundled" && - plugin.providers.length > 0 && - !declaredPluginIds.has(plugin.id) && + recordHasProviderSurface(plugin) && + !declaredPluginIds.has(plugin.pluginId) && isProviderPluginEligibleForRuntimeOwnerActivation({ plugin, normalizedConfig, @@ -166,11 +223,16 @@ export function resolveDiscoveredProviderPluginIds(params: { onlyPluginIds?: readonly string[]; includeUntrustedWorkspacePlugins?: boolean; }): string[] { - const { registry, onlyPluginIdSet } = loadScopedProviderManifestRegistry(params); + const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params); const shouldFilterUntrustedWorkspacePlugins = params.includeUntrustedWorkspacePlugins === false; const normalizedConfig = normalizePluginsConfig(params.config?.plugins); - return listManifestPluginIds(registry, (plugin) => { - if (!(plugin.providers.length > 0 && (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)))) { + return listRegistryPluginIds(registry, (plugin) => { + if ( + !( + recordHasProviderSurface(plugin) && + (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.pluginId)) + ) + ) { return false; } return isProviderPluginEligibleForSetupDiscovery({ @@ -183,7 +245,7 @@ export function resolveDiscoveredProviderPluginIds(params: { } function isProviderPluginEligibleForSetupDiscovery(params: { - plugin: PluginManifestRecord; + plugin: PluginRegistryRecord; shouldFilterUntrustedWorkspacePlugins: boolean; normalizedConfig: NormalizedPluginsConfig; rootConfig?: PluginLoadOptions["config"]; @@ -193,14 +255,14 @@ function isProviderPluginEligibleForSetupDiscovery(params: { } if ( !passesManifestOwnerBasePolicy({ - plugin: params.plugin, + plugin: toManifestOwnerRecord(params.plugin), normalizedConfig: params.normalizedConfig, }) ) { return false; } return isActivatedManifestOwner({ - plugin: params.plugin, + plugin: toManifestOwnerRecord(params.plugin), normalizedConfig: params.normalizedConfig, rootConfig: params.rootConfig, }); @@ -227,13 +289,13 @@ export function resolveDiscoverableProviderOwnerPluginIds(params: { } function isProviderPluginEligibleForRuntimeOwnerActivation(params: { - plugin: PluginManifestRecord; + plugin: PluginRegistryRecord; normalizedConfig: NormalizedPluginsConfig; rootConfig?: PluginLoadOptions["config"]; }): boolean { if ( !passesManifestOwnerBasePolicy({ - plugin: params.plugin, + plugin: toManifestOwnerRecord(params.plugin), normalizedConfig: params.normalizedConfig, }) ) { @@ -243,7 +305,7 @@ function isProviderPluginEligibleForRuntimeOwnerActivation(params: { return true; } return isActivatedManifestOwner({ - plugin: params.plugin, + plugin: toManifestOwnerRecord(params.plugin), normalizedConfig: params.normalizedConfig, rootConfig: params.rootConfig, }); @@ -376,20 +438,42 @@ export function resolveOwningPluginIdsForProvider(params: { return undefined; } - const registry = resolveManifestRegistry(params); - const pluginIds = registry.plugins - .filter( - (plugin) => - plugin.providers.some( - (providerId) => normalizeProviderId(providerId) === normalizedProvider, - ) || - plugin.cliBackends.some( - (backendId) => normalizeProviderId(backendId) === normalizedProvider, - ), - ) - .map((plugin) => plugin.id); + if (params.manifestRegistry) { + const pluginIds = params.manifestRegistry.plugins + .filter( + (plugin) => + plugin.providers.some( + (providerId) => normalizeProviderId(providerId) === normalizedProvider, + ) || + plugin.cliBackends.some( + (backendId) => normalizeProviderId(backendId) === normalizedProvider, + ), + ) + .map((plugin) => plugin.id); - return pluginIds.length > 0 ? pluginIds : undefined; + return pluginIds.length > 0 ? pluginIds : undefined; + } + + const pluginIds = [ + ...resolveProviderOwners({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + providerId: normalizedProvider, + includeDisabled: true, + }), + ...resolvePluginContributionOwners({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + contribution: "cliBackends", + matches: (backendId) => normalizeProviderId(backendId) === normalizedProvider, + includeDisabled: true, + }), + ]; + + const deduped = dedupeSortedPluginIds(pluginIds); + return deduped.length > 0 ? deduped : undefined; } export function resolveOwningPluginIdsForModelRef(params: { @@ -456,17 +540,16 @@ export function resolveNonBundledProviderPluginIds(params: { workspaceDir?: string; env?: PluginLoadOptions["env"]; }): string[] { - const registry = loadProviderManifestRegistry(params); + const registry = loadProviderRegistrySnapshot(params); const normalizedConfig = normalizePluginsConfig(params.config?.plugins); - return listManifestPluginIds( + return listRegistryPluginIds( registry, (plugin) => plugin.origin !== "bundled" && - plugin.providers.length > 0 && - resolveEffectivePluginActivationState({ - id: plugin.id, - origin: plugin.origin, - config: normalizedConfig, + recordHasProviderSurface(plugin) && + resolveEffectiveRegistryPluginActivation({ + plugin, + normalizedConfig, rootConfig: params.config, }).activated, ); @@ -477,18 +560,16 @@ export function resolveCatalogHookProviderPluginIds(params: { workspaceDir?: string; env?: PluginLoadOptions["env"]; }): string[] { - const registry = loadProviderManifestRegistry(params); + const registry = loadProviderRegistrySnapshot(params); const normalizedConfig = normalizePluginsConfig(params.config?.plugins); - const enabledProviderPluginIds = listManifestPluginIds( + const enabledProviderPluginIds = listRegistryPluginIds( registry, (plugin) => - plugin.providers.length > 0 && - resolveEffectivePluginActivationState({ - id: plugin.id, - origin: plugin.origin, - config: normalizedConfig, + recordHasProviderSurface(plugin) && + resolveEffectiveRegistryPluginActivation({ + plugin, + normalizedConfig, rootConfig: params.config, - enabledByDefault: plugin.enabledByDefault, }).activated, ); const bundledCompatPluginIds = resolveBundledProviderCompatPluginIds(params);