From 8ccb11cbfca7ff3cd03c6e857f3e883e0531b115 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Sun, 24 May 2026 13:44:03 -0700 Subject: [PATCH] perf(plugins,gateway): thread metadata snapshot + discovery through hot paths + plugin owner fixes (#84649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(plugins): thread metadata snapshot and discovery through hot paths With the snapshot memo now actually hitting, route the snapshot's manifestRegistry and discovery through the helper chains that already had fast paths for them. Eliminates redundant per-call rebuilds at two big amplifiers. - Provider resolve paths (resolvePluginProviders / isPluginProvidersLoadInFlight / resolveOwningPluginIdsForProvider / resolveExternalAuthProfilesWithPlugins) self-service a snapshot once at the public entry, then thread it as a separate required arg through resolvePluginProviderLoadBase, resolveExplicitProviderOwnerPluginIds, and the setup/runtime load state helpers. Inner reads change from 'params.pluginMetadataSnapshot?.x' to 'snapshot.x', no more enrichedParams clone. loadPluginManifestRegistryForInstalledIndex fires drop ~685 -> ~10 per cold start. - Bundled-channel / auto-enable chain accepts an optional PluginDiscoveryResult. discoverOpenClawPlugins is fired once during snapshot building (resolveInstalledPluginIndexRegistry already produced it internally; now bubbled up through loadInstalledPluginIndexWithDiscovery, PluginRegistrySnapshotResult, and onto PluginMetadataSnapshot.discovery). load-context reads metadataSnapshot.discovery and passes it through applyPluginAutoEnable, so the bundled-channel cascade (collectConfiguredChannelIds, listBundledChannelIdsWith*, listPotentialConfiguredChannelPresenceSignals) short-circuits instead of each leaf re-firing discovery. Persisted-cache path is unchanged: no discovery on the snapshot, downstream chain handles its own fallback (pre-PR behavior on that path). * test(plugins): isolate snapshot memo across tests that mock manifest registry The snapshot memo is now process-scoped and effective (~98% hit rate). Three test files were depending on cache misses (because the broken cache returned them) — each test would set up its own loadPluginManifestRegistry mock and expect a fresh derive. With the cache fixed, an earlier test's mocked registry now leaks into later tests in the same file. - io.write-config.test.ts: afterEach now clears the snapshot memo so the 'demo' plugin mocked in the first test does not survive into 'keeps shipped plugin install config records when index migration fails', which expects an empty registry to surface the 'plugin not found: demo' warning. - gateway/model-pricing-cache.ts: resetGatewayModelPricingCacheForTest also clears the memo. Tests in model-pricing-cache.test.ts assert loadPluginManifestRegistryForInstalledIndex was called; the memo hit otherwise skips the call. - providers.test.ts: vi.doMock loadPluginMetadataSnapshot to wrap the existing loadPluginManifestRegistryMock fixture. The plumbing commit added an auto-fetch fall-through in resolveOwningPluginIdsForProvider; without the mock, providers tests hit real disk reads and return empty registries (which is what surfaced as 9 unrelated-looking failures in the prior CI run). * fix(plugins): preserve setup.cliBackends owner matching in provider scan resolveOwningPluginIdsForProvider now also checks plugin.setup?.cliBackends. The pre-PR no-registry fallback used resolvePluginContributionOwners which includes both top-level cliBackends and setup.cliBackends; the PR's manifest scan replacement was missing the setup case. * fix(plugins): inherit active registry workspaceDir before loading metadata snapshot isPluginProvidersLoadInFlight and resolvePluginProviders now resolve env and workspaceDir once at the entry point (falling back to getActivePluginRegistryWorkspaceDir) and pass them into both loadPluginMetadataSnapshot and resolvePluginProviderLoadBase. Pre-fix the snapshot used params.workspaceDir raw while the load base inherited the active workspace, so workspace-scoped provider plugins could be absent from the snapshot manifest registry even though owner resolution expected them. Regression test asserts the snapshot mock receives the active workspaceDir when the caller omits it. * perf(gateway): thread discovery into applyPluginAutoEnable call sites Every gateway applyPluginAutoEnable call now passes the snapshot's PluginDiscoveryResult so the bundled-channel cascade (collectConfiguredChannelIds → listBundledChannelIdsWith* → listPotentialConfiguredChannelPresenceSignals) short-circuits instead of each leaf re-firing discovery. Startup-time sites pull discovery from the snapshot/lookup-table they already hold: - server-plugin-bootstrap.ts (pluginLookUpTable) - server-startup-plugins.ts (pluginMetadataSnapshot) - server-startup-config.ts (pluginMetadataSnapshot) - server-plugins.ts (pluginLookUpTable, both call sites) Per-RPC sites (server.impl getRuntimeConfig callback, server-methods/channels status + start handlers, server-methods/send) source discovery via getCurrentPluginMetadataSnapshot using the runtime config to validate compatibility. Falls through to the original slow path when the snapshot is absent or incompatible. --- src/channels/config-presence.ts | 16 ++- src/channels/plugins/bundled-ids.ts | 33 ++++- src/channels/plugins/configured-state.ts | 9 +- src/channels/plugins/package-state-probes.ts | 15 ++- src/channels/plugins/persisted-auth-state.ts | 9 +- src/config/plugin-auto-enable.apply.ts | 2 + src/config/plugin-auto-enable.detect.ts | 4 +- src/config/plugin-auto-enable.shared.ts | 16 ++- src/gateway/model-pricing-cache.ts | 6 +- src/gateway/server-methods/channels.ts | 23 +++- src/gateway/server-methods/send.ts | 12 +- src/gateway/server-plugin-bootstrap.ts | 1 + src/gateway/server-plugins.ts | 2 + src/gateway/server-startup-config.ts | 1 + src/gateway/server-startup-plugins.ts | 1 + src/gateway/server.impl.ts | 17 ++- src/plugins/activation-context.ts | 13 +- .../installed-plugin-index-registry.ts | 8 +- src/plugins/installed-plugin-index.ts | 38 ++++-- src/plugins/plugin-metadata-snapshot.ts | 1 + src/plugins/plugin-metadata-snapshot.types.ts | 2 + src/plugins/plugin-registry-snapshot.ts | 21 +-- src/plugins/provider-runtime.ts | 8 ++ src/plugins/providers.runtime.ts | 123 +++++++++++------- src/plugins/providers.test.ts | 47 +++++++ src/plugins/providers.ts | 60 ++++----- src/plugins/runtime/load-context.ts | 5 +- 27 files changed, 348 insertions(+), 145 deletions(-) diff --git a/src/channels/config-presence.ts b/src/channels/config-presence.ts index c556cb20802..0afe26687de 100644 --- a/src/channels/config-presence.ts +++ b/src/channels/config-presence.ts @@ -7,6 +7,7 @@ import { import { resolveStateDir } from "../config/paths.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { hasNonEmptyString } from "../infra/outbound/channel-target.js"; +import type { PluginDiscoveryResult } from "../plugins/discovery.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; import { isRecord } from "../utils.js"; import { listBundledChannelIds } from "./plugins/bundled-ids.js"; @@ -15,6 +16,7 @@ const IGNORED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]); type ChannelPresenceOptions = { channelIds?: readonly string[]; + discovery?: PluginDiscoveryResult; includePersistedAuthState?: boolean; persistedAuthStateProbe?: { listChannelIds: () => readonly string[]; @@ -71,6 +73,9 @@ function listPersistedAuthStateChannelIds(options: ChannelPresenceOptions): read if (override) { return override; } + if (options.discovery) { + return listBundledChannelIdsWithPersistedAuthState(options.discovery); + } if (persistedAuthStateChannelIds) { return persistedAuthStateChannelIds; } @@ -88,7 +93,12 @@ function hasPersistedAuthState(params: { if (override) { return override.hasState(params); } - return hasBundledChannelPersistedAuthState(params); + return hasBundledChannelPersistedAuthState({ + channelId: params.channelId, + cfg: params.cfg, + env: params.env, + discovery: params.options.discovery, + }); } export function listPotentialConfiguredChannelIds( @@ -121,7 +131,7 @@ export function listPotentialConfiguredChannelPresenceSignals( signals.push({ channelId, source }); }; const configuredChannelIds = new Set(); - const channelIds = options.channelIds ?? listBundledChannelIds(env); + const channelIds = options.channelIds ?? listBundledChannelIds(env, options.discovery); const channelEnvPrefixes = listChannelEnvPrefixes(channelIds); const channels = isRecord(cfg.channels) ? cfg.channels : null; if (channels) { @@ -165,7 +175,7 @@ function hasEnvConfiguredChannel( env: NodeJS.ProcessEnv, options: ChannelPresenceOptions = {}, ): boolean { - const channelIds = options.channelIds ?? listBundledChannelIds(env); + const channelIds = options.channelIds ?? listBundledChannelIds(env, options.discovery); const channelEnvPrefixes = listChannelEnvPrefixes(channelIds); for (const [key, value] of Object.entries(env)) { if (!hasNonEmptyString(value)) { diff --git a/src/channels/plugins/bundled-ids.ts b/src/channels/plugins/bundled-ids.ts index c23cd4b994b..f2a62301431 100644 --- a/src/channels/plugins/bundled-ids.ts +++ b/src/channels/plugins/bundled-ids.ts @@ -1,11 +1,17 @@ import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js"; +import type { PluginDiscoveryResult } from "../../plugins/discovery.js"; import { resolveBundledChannelRootScope } from "./bundled-root.js"; export function listBundledChannelPluginIdsForRoot( _packageRoot: string, env: NodeJS.ProcessEnv = process.env, + discovery?: PluginDiscoveryResult, ): string[] { - return listChannelCatalogEntries({ origin: "bundled", env }) + return listChannelCatalogEntries({ + origin: "bundled", + env, + discovery, + }) .map((entry) => entry.pluginId) .toSorted((left, right) => left.localeCompare(right)); } @@ -13,17 +19,32 @@ export function listBundledChannelPluginIdsForRoot( export function listBundledChannelIdsForRoot( _packageRoot: string, env: NodeJS.ProcessEnv = process.env, + discovery?: PluginDiscoveryResult, ): string[] { - return listChannelCatalogEntries({ origin: "bundled", env }) + return listChannelCatalogEntries({ + origin: "bundled", + env, + discovery, + }) .map((entry) => entry.channel.id) .filter((channelId): channelId is string => Boolean(channelId)) .toSorted((left, right) => left.localeCompare(right)); } -export function listBundledChannelPluginIds(env: NodeJS.ProcessEnv = process.env): string[] { - return listBundledChannelPluginIdsForRoot(resolveBundledChannelRootScope(env).cacheKey, env); +export function listBundledChannelPluginIds( + env: NodeJS.ProcessEnv = process.env, + discovery?: PluginDiscoveryResult, +): string[] { + return listBundledChannelPluginIdsForRoot( + resolveBundledChannelRootScope(env).cacheKey, + env, + discovery, + ); } -export function listBundledChannelIds(env: NodeJS.ProcessEnv = process.env): string[] { - return listBundledChannelIdsForRoot(resolveBundledChannelRootScope(env).cacheKey, env); +export function listBundledChannelIds( + env: NodeJS.ProcessEnv = process.env, + discovery?: PluginDiscoveryResult, +): string[] { + return listBundledChannelIdsForRoot(resolveBundledChannelRootScope(env).cacheKey, env, discovery); } diff --git a/src/channels/plugins/configured-state.ts b/src/channels/plugins/configured-state.ts index 3de929084a0..82f238fd539 100644 --- a/src/channels/plugins/configured-state.ts +++ b/src/channels/plugins/configured-state.ts @@ -1,22 +1,27 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import type { PluginDiscoveryResult } from "../../plugins/discovery.js"; import { hasBundledChannelPackageState, listBundledChannelIdsForPackageState, } from "./package-state-probes.js"; -export function listBundledChannelIdsWithConfiguredState(): string[] { - return listBundledChannelIdsForPackageState("configuredState"); +export function listBundledChannelIdsWithConfiguredState( + discovery?: PluginDiscoveryResult, +): string[] { + return listBundledChannelIdsForPackageState("configuredState", discovery); } export function hasBundledChannelConfiguredState(params: { channelId: string; cfg: OpenClawConfig; env?: NodeJS.ProcessEnv; + discovery?: PluginDiscoveryResult; }): boolean { return hasBundledChannelPackageState({ metadataKey: "configuredState", channelId: params.channelId, cfg: params.cfg, env: params.env, + discovery: params.discovery, }); } diff --git a/src/channels/plugins/package-state-probes.ts b/src/channels/plugins/package-state-probes.ts index 45c857c5b82..abb276276a7 100644 --- a/src/channels/plugins/package-state-probes.ts +++ b/src/channels/plugins/package-state-probes.ts @@ -8,6 +8,7 @@ import { listChannelCatalogEntries, type PluginChannelCatalogEntry, } from "../../plugins/channel-catalog-registry.js"; +import type { PluginDiscoveryResult } from "../../plugins/discovery.js"; import { getCachedPluginModuleLoader, type PluginModuleLoaderCache, @@ -174,10 +175,12 @@ function resolveChannelPackageStateMetadata( function listChannelPackageStateCatalog( metadataKey: ChannelPackageStateMetadataKey, + discovery?: PluginDiscoveryResult, ): PluginChannelCatalogEntry[] { - return listChannelCatalogEntries({ origin: "bundled" }).filter((entry) => - Boolean(resolveChannelPackageStateMetadata(entry, metadataKey)), - ); + return listChannelCatalogEntries({ + origin: "bundled", + discovery, + }).filter((entry) => Boolean(resolveChannelPackageStateMetadata(entry, metadataKey))); } function resolveChannelPackageStateChecker(params: { @@ -235,8 +238,9 @@ function resolvePackageStateChannelId(entry: PluginChannelCatalogEntry): string export function listBundledChannelIdsForPackageState( metadataKey: ChannelPackageStateMetadataKey, + discovery?: PluginDiscoveryResult, ): string[] { - return listChannelPackageStateCatalog(metadataKey) + return listChannelPackageStateCatalog(metadataKey, discovery) .map((entry) => resolvePackageStateChannelId(entry)) .filter((channelId): channelId is string => Boolean(channelId)) .toSorted((left, right) => left.localeCompare(right)); @@ -247,9 +251,10 @@ export function hasBundledChannelPackageState(params: { channelId: string; cfg: OpenClawConfig; env?: NodeJS.ProcessEnv; + discovery?: PluginDiscoveryResult; }): boolean { const requestedChannelId = normalizeOptionalString(params.channelId); - const entry = listChannelPackageStateCatalog(params.metadataKey).find( + const entry = listChannelPackageStateCatalog(params.metadataKey, params.discovery).find( (candidate) => resolvePackageStateChannelId(candidate) === requestedChannelId, ); if (!entry) { diff --git a/src/channels/plugins/persisted-auth-state.ts b/src/channels/plugins/persisted-auth-state.ts index 4413f0757a4..69b98ac0294 100644 --- a/src/channels/plugins/persisted-auth-state.ts +++ b/src/channels/plugins/persisted-auth-state.ts @@ -1,22 +1,27 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import type { PluginDiscoveryResult } from "../../plugins/discovery.js"; import { hasBundledChannelPackageState, listBundledChannelIdsForPackageState, } from "./package-state-probes.js"; -export function listBundledChannelIdsWithPersistedAuthState(): string[] { - return listBundledChannelIdsForPackageState("persistedAuthState"); +export function listBundledChannelIdsWithPersistedAuthState( + discovery?: PluginDiscoveryResult, +): string[] { + return listBundledChannelIdsForPackageState("persistedAuthState", discovery); } export function hasBundledChannelPersistedAuthState(params: { channelId: string; cfg: OpenClawConfig; env?: NodeJS.ProcessEnv; + discovery?: PluginDiscoveryResult; }): boolean { return hasBundledChannelPackageState({ metadataKey: "persistedAuthState", channelId: params.channelId, cfg: params.cfg, env: params.env, + discovery: params.discovery, }); } diff --git a/src/config/plugin-auto-enable.apply.ts b/src/config/plugin-auto-enable.apply.ts index 4de04385fc8..75bbc5743df 100644 --- a/src/config/plugin-auto-enable.apply.ts +++ b/src/config/plugin-auto-enable.apply.ts @@ -1,3 +1,4 @@ +import type { PluginDiscoveryResult } from "../plugins/discovery.js"; import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; import { detectPluginAutoEnableCandidates } from "./plugin-auto-enable.detect.js"; import { @@ -44,6 +45,7 @@ export function applyPluginAutoEnable(params: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv; manifestRegistry?: PluginManifestRegistry; + discovery?: PluginDiscoveryResult; }): PluginAutoEnableResult { const candidates = detectPluginAutoEnableCandidates(params); return materializePluginAutoEnableCandidates({ diff --git a/src/config/plugin-auto-enable.detect.ts b/src/config/plugin-auto-enable.detect.ts index 7d06705fd5a..12a14352e70 100644 --- a/src/config/plugin-auto-enable.detect.ts +++ b/src/config/plugin-auto-enable.detect.ts @@ -1,3 +1,4 @@ +import type { PluginDiscoveryResult } from "../plugins/discovery.js"; import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; import { resolveConfiguredPluginAutoEnableCandidates, @@ -11,10 +12,11 @@ export function detectPluginAutoEnableCandidates(params: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv; manifestRegistry?: PluginManifestRegistry; + discovery?: PluginDiscoveryResult; }): PluginAutoEnableCandidate[] { const env = params.env ?? process.env; const config = params.config ?? ({} as OpenClawConfig); - const readiness = resolvePluginAutoEnableReadiness(config, env); + const readiness = resolvePluginAutoEnableReadiness(config, env, params.discovery); if (!readiness.mayNeedAutoEnable) { return []; } diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index bfcfbd121d3..03cb7a5d9bb 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -11,6 +11,7 @@ import { import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js"; import { normalizePluginsConfig } from "../plugins/config-state.js"; import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; +import type { PluginDiscoveryResult } from "../plugins/discovery.js"; import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plugin-index-policy.js"; import { type PluginManifestRecord, @@ -265,10 +266,15 @@ function collectPluginIdsForConfiguredChannel( return [claims[0]?.plugin.id ?? builtInId ?? normalizedChannelId]; } -function collectConfiguredChannelIds(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string[] { - const configuredStateChannelIds = new Set(listBundledChannelIdsWithConfiguredState()); +function collectConfiguredChannelIds( + cfg: OpenClawConfig, + env: NodeJS.ProcessEnv, + discovery?: PluginDiscoveryResult, +): string[] { + const configuredStateChannelIds = new Set(listBundledChannelIdsWithConfiguredState(discovery)); return listPotentialConfiguredChannelPresenceSignals(cfg, env, { includePersistedAuthState: false, + discovery, }) .map((signal) => ({ source: signal.source, @@ -281,6 +287,7 @@ function collectConfiguredChannelIds(cfg: OpenClawConfig, env: NodeJS.ProcessEnv channelId, source, configuredStateChannelIds, + discovery, }), ) .map(({ channelId }) => channelId); @@ -292,6 +299,7 @@ function isAutoEnableConfiguredChannelSignal(params: { channelId: string; source: ChannelPresenceSignalSource; configuredStateChannelIds: ReadonlySet; + discovery?: PluginDiscoveryResult; }): boolean { if ( params.source === "env" && @@ -300,6 +308,7 @@ function isAutoEnableConfiguredChannelSignal(params: { channelId: params.channelId, cfg: params.cfg, env: params.env, + discovery: params.discovery, }) ) { return false; @@ -535,6 +544,7 @@ export function configMayNeedPluginAutoEnable( export function resolvePluginAutoEnableReadiness( cfg: OpenClawConfig, env: NodeJS.ProcessEnv, + discovery?: PluginDiscoveryResult, ): { mayNeedAutoEnable: boolean; configuredChannelIds: string[] } { if (arePluginsGloballyDisabled(cfg)) { return { mayNeedAutoEnable: false, configuredChannelIds: [] }; @@ -545,7 +555,7 @@ export function resolvePluginAutoEnableReadiness( if (hasConfiguredPluginConfigEntry(cfg)) { return { mayNeedAutoEnable: true, configuredChannelIds: [] }; } - const configuredChannelIds = collectConfiguredChannelIds(cfg, env); + const configuredChannelIds = collectConfiguredChannelIds(cfg, env, discovery); if (configuredChannelIds.length > 0) { return { mayNeedAutoEnable: true, configuredChannelIds }; } diff --git a/src/gateway/model-pricing-cache.ts b/src/gateway/model-pricing-cache.ts index b388736659c..12e629af7bc 100644 --- a/src/gateway/model-pricing-cache.ts +++ b/src/gateway/model-pricing-cache.ts @@ -19,7 +19,10 @@ import type { PluginManifestModelPricingProvider, PluginManifestModelPricingSource, } from "../plugins/manifest.js"; -import { resolvePluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; +import { + clearLoadPluginMetadataSnapshotMemo, + resolvePluginMetadataSnapshot, +} from "../plugins/plugin-metadata-snapshot.js"; import type { PluginMetadataRegistryView } from "../plugins/plugin-metadata-snapshot.types.js"; import type { PluginRegistrySnapshot } from "../plugins/plugin-registry.js"; import { normalizeOptionalString, resolvePrimaryStringValue } from "../shared/string-coerce.js"; @@ -1396,6 +1399,7 @@ export function startGatewayModelPricingRefresh( export function resetGatewayModelPricingCacheForTest(): void { clearGatewayModelPricingCacheState(); + clearLoadPluginMetadataSnapshotMemo(); clearRefreshTimer(); inFlightRefresh = null; } diff --git a/src/gateway/server-methods/channels.ts b/src/gateway/server-methods/channels.ts index 94875c75a88..4c7fd454945 100644 --- a/src/gateway/server-methods/channels.ts +++ b/src/gateway/server-methods/channels.ts @@ -13,6 +13,7 @@ import { readConfigFileSnapshot } from "../../config/config.js"; import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { getChannelActivity } from "../../infra/channel-activity.js"; +import { getCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import { defaultRuntime } from "../../runtime.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; @@ -301,9 +302,16 @@ export const channelsHandlers: GatewayRequestHandlers = { const rawChannel = (params as { channel?: unknown }).channel; const requestedChannel = typeof rawChannel === "string" ? normalizeChannelId(rawChannel) : undefined; - const cfg = applyPluginAutoEnable({ - config: context.getRuntimeConfig(), + const runtimeConfig = context.getRuntimeConfig(); + const currentSnapshot = getCurrentPluginMetadataSnapshot({ + config: runtimeConfig, env: process.env, + }); + const cfg = applyPluginAutoEnable({ + config: runtimeConfig, + env: process.env, + manifestRegistry: currentSnapshot?.manifestRegistry, + discovery: currentSnapshot?.discovery, }).config; const runtime = context.getRuntimeSnapshot(); const plugins = listChannelPlugins(); @@ -579,9 +587,16 @@ export const channelsHandlers: GatewayRequestHandlers = { return; } try { - const cfg = applyPluginAutoEnable({ - config: context.getRuntimeConfig(), + const runtimeConfig = context.getRuntimeConfig(); + const currentSnapshot = getCurrentPluginMetadataSnapshot({ + config: runtimeConfig, env: process.env, + }); + const cfg = applyPluginAutoEnable({ + config: runtimeConfig, + env: process.env, + manifestRegistry: currentSnapshot?.manifestRegistry, + discovery: currentSnapshot?.discovery, }).config; const payload = await startChannelAccount({ channelId, diff --git a/src/gateway/server-methods/send.ts b/src/gateway/server-methods/send.ts index bced1b4a096..0377456bf90 100644 --- a/src/gateway/server-methods/send.ts +++ b/src/gateway/server-methods/send.ts @@ -21,6 +21,7 @@ import { maybeResolveIdLikeTarget } from "../../infra/outbound/target-resolver.j import { resolveOutboundTarget } from "../../infra/outbound/targets.js"; import { extractToolPayload } from "../../infra/outbound/tool-payload.js"; import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js"; +import { getCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js"; import { normalizePollInput } from "../../polls.js"; import { parseThreadSessionSuffix } from "../../sessions/session-key-utils.js"; import { @@ -131,9 +132,16 @@ async function resolveRequestedChannel(params: { error: errorShape(ErrorCodes.INVALID_REQUEST, params.unsupportedMessage(channelInput)), }; } - const cfg = applyPluginAutoEnable({ - config: params.context.getRuntimeConfig(), + const runtimeConfig = params.context.getRuntimeConfig(); + const currentSnapshot = getCurrentPluginMetadataSnapshot({ + config: runtimeConfig, env: process.env, + }); + const cfg = applyPluginAutoEnable({ + config: runtimeConfig, + env: process.env, + manifestRegistry: currentSnapshot?.manifestRegistry, + discovery: currentSnapshot?.discovery, }).config; let channel = normalizedChannel; if (!channel) { diff --git a/src/gateway/server-plugin-bootstrap.ts b/src/gateway/server-plugin-bootstrap.ts index a00c09fbb55..93c339a3594 100644 --- a/src/gateway/server-plugin-bootstrap.ts +++ b/src/gateway/server-plugin-bootstrap.ts @@ -83,6 +83,7 @@ export function prepareGatewayPluginLoad(params: GatewayPluginBootstrapParams) { ...(params.pluginLookUpTable?.manifestRegistry ? { manifestRegistry: params.pluginLookUpTable.manifestRegistry } : {}), + discovery: params.pluginLookUpTable?.discovery, }); const resolvedConfig = activationSourceConfig === params.cfg diff --git a/src/gateway/server-plugins.ts b/src/gateway/server-plugins.ts index 757233d1bf8..25360d46650 100644 --- a/src/gateway/server-plugins.ts +++ b/src/gateway/server-plugins.ts @@ -636,6 +636,7 @@ export function loadGatewayPlugins(params: { ...(params.pluginLookUpTable?.manifestRegistry ? { manifestRegistry: params.pluginLookUpTable.manifestRegistry } : {}), + discovery: params.pluginLookUpTable?.discovery, }) : undefined; const autoEnableMs = performance.now() - started; @@ -659,6 +660,7 @@ export function loadGatewayPlugins(params: { ...(params.pluginLookUpTable?.manifestRegistry ? { manifestRegistry: params.pluginLookUpTable.manifestRegistry } : {}), + discovery: params.pluginLookUpTable?.discovery, }); const resolvedConfigMs = performance.now() - started; const resolvedConfig = autoEnabled.config; diff --git a/src/gateway/server-startup-config.ts b/src/gateway/server-startup-config.ts index d424e5c6500..93e521ab448 100644 --- a/src/gateway/server-startup-config.ts +++ b/src/gateway/server-startup-config.ts @@ -125,6 +125,7 @@ export async function loadGatewayStartupConfigSnapshot(params: { ...(pluginMetadataSnapshot?.manifestRegistry ? { manifestRegistry: pluginMetadataSnapshot.manifestRegistry } : {}), + discovery: pluginMetadataSnapshot?.discovery, }), ); if (autoEnable.changes.length === 0) { diff --git a/src/gateway/server-startup-plugins.ts b/src/gateway/server-startup-plugins.ts index fb856f99aa3..9c76221a79e 100644 --- a/src/gateway/server-startup-plugins.ts +++ b/src/gateway/server-startup-plugins.ts @@ -87,6 +87,7 @@ export async function prepareGatewayPluginBootstrap(params: { ...(params.pluginMetadataSnapshot?.manifestRegistry ? { manifestRegistry: params.pluginMetadataSnapshot.manifestRegistry } : {}), + discovery: params.pluginMetadataSnapshot?.discovery, }).config, }); const pluginsGloballyDisabled = gatewayPluginConfig.plugins?.enabled === false; diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 78efc4bfeda..b8fdcaed9cd 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -41,6 +41,7 @@ import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/di import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js"; import { clearCurrentPluginMetadataSnapshot, + getCurrentPluginMetadataSnapshot, setCurrentPluginMetadataSnapshot, } from "../plugins/current-plugin-metadata-snapshot.js"; import type { PluginHookGatewayCronService } from "../plugins/hook-types.js"; @@ -836,11 +837,19 @@ export async function startGatewayServer( }); const { createChannelManager } = await import("./server-channels.js"); const channelManager = createChannelManager({ - getRuntimeConfig: () => - applyPluginAutoEnable({ - config: getRuntimeConfig(), + getRuntimeConfig: () => { + const runtimeConfig = getRuntimeConfig(); + const currentSnapshot = getCurrentPluginMetadataSnapshot({ + config: runtimeConfig, env: process.env, - }).config, + }); + return applyPluginAutoEnable({ + config: runtimeConfig, + env: process.env, + manifestRegistry: currentSnapshot?.manifestRegistry, + discovery: currentSnapshot?.discovery, + }).config; + }, channelLogs, channelRuntimeEnvs, resolveChannelRuntime: getChannelRuntime, diff --git a/src/plugins/activation-context.ts b/src/plugins/activation-context.ts index 68dd7f3fe5e..faf3c12a363 100644 --- a/src/plugins/activation-context.ts +++ b/src/plugins/activation-context.ts @@ -12,6 +12,7 @@ import { type PluginActivationConfigSource, } from "./config-state.js"; import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; +import type { PluginDiscoveryResult } from "./discovery.js"; export type PluginActivationCompatConfig = { allowlistPluginIds?: readonly string[]; @@ -68,6 +69,7 @@ type BundledPluginCompatibleActivationParams = { env?: NodeJS.ProcessEnv; onlyPluginIds?: readonly string[]; }) => string[]; + discovery?: PluginDiscoveryResult; }; export function withActivatedPluginIds(params: { @@ -175,6 +177,7 @@ function applyPluginAutoEnableForActivation(params: { config: OpenClawConfig; env: NodeJS.ProcessEnv; workspaceDir?: string; + discovery?: PluginDiscoveryResult; }) { const currentSnapshot = getCurrentPluginMetadataSnapshot({ config: params.config, @@ -196,7 +199,8 @@ function applyPluginAutoEnableForActivation(params: { return applyPluginAutoEnable({ config: params.config, env: params.env, - ...(currentManifestRegistry ? { manifestRegistry: currentManifestRegistry } : {}), + manifestRegistry: currentManifestRegistry, + discovery: params.discovery, }); } @@ -207,6 +211,7 @@ export function resolvePluginActivationSnapshot(params: { env?: NodeJS.ProcessEnv; workspaceDir?: string; applyAutoEnable?: boolean; + discovery?: PluginDiscoveryResult; }): PluginActivationSnapshot { const env = params.env ?? process.env; const rawConfig = params.rawConfig ?? params.resolvedConfig; @@ -218,6 +223,7 @@ export function resolvePluginActivationSnapshot(params: { config: rawConfig, env, workspaceDir: params.workspaceDir, + discovery: params.discovery, }); resolvedConfig = autoEnabled.config; autoEnabledReasons = autoEnabled.autoEnabledReasons; @@ -243,6 +249,7 @@ export function resolvePluginActivationInputs(params: { workspaceDir?: string; compat?: PluginActivationCompatConfig; applyAutoEnable?: boolean; + discovery?: PluginDiscoveryResult; }): PluginActivationInputs { const env = params.env ?? process.env; const snapshot = resolvePluginActivationSnapshot({ @@ -252,6 +259,7 @@ export function resolvePluginActivationInputs(params: { env, workspaceDir: params.workspaceDir, applyAutoEnable: params.applyAutoEnable, + discovery: params.discovery, }); const config = applyPluginCompatibilityOverrides({ config: snapshot.config, @@ -279,6 +287,7 @@ export function resolveBundledPluginCompatibleActivationInputs( env: params.env, workspaceDir: params.workspaceDir, applyAutoEnable: params.applyAutoEnable, + discovery: params.discovery, }); const allowlistCompatEnabled = params.compatMode.allowlist === true; const shouldResolveCompatPluginIds = shouldResolveBundledCompatPluginIds({ @@ -304,6 +313,7 @@ export function resolveBundledPluginCompatibleActivationInputs( allowlistCompatEnabled, compatPluginIds, }), + discovery: params.discovery, }); return { @@ -325,6 +335,7 @@ export function resolveBundledPluginCompatibleLoadValues( config: rawConfig, env, workspaceDir: params.workspaceDir, + discovery: params.discovery, }); resolvedConfig = autoEnabled.config; autoEnabledReasons = autoEnabled.autoEnabledReasons; diff --git a/src/plugins/installed-plugin-index-registry.ts b/src/plugins/installed-plugin-index-registry.ts index d61053545e5..eda6e96d123 100644 --- a/src/plugins/installed-plugin-index-registry.ts +++ b/src/plugins/installed-plugin-index-registry.ts @@ -1,5 +1,9 @@ import { normalizePluginsConfig } from "./config-state.js"; -import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js"; +import { + discoverOpenClawPlugins, + type PluginCandidate, + type PluginDiscoveryResult, +} from "./discovery.js"; import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-record-reader.js"; import type { LoadInstalledPluginIndexParams } from "./installed-plugin-index-types.js"; import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js"; @@ -7,6 +11,7 @@ import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manif export function resolveInstalledPluginIndexRegistry(params: LoadInstalledPluginIndexParams): { registry: PluginManifestRegistry; candidates: readonly PluginCandidate[]; + discovery?: PluginDiscoveryResult; } { if (params.candidates) { return { @@ -35,6 +40,7 @@ export function resolveInstalledPluginIndexRegistry(params: LoadInstalledPluginI }); return { candidates: discovery.candidates, + discovery, registry: loadPluginManifestRegistry({ config: params.config, workspaceDir: params.workspaceDir, diff --git a/src/plugins/installed-plugin-index.ts b/src/plugins/installed-plugin-index.ts index 0ed43460bea..b2021da22a2 100644 --- a/src/plugins/installed-plugin-index.ts +++ b/src/plugins/installed-plugin-index.ts @@ -2,6 +2,7 @@ import type { OpenClawConfig } from "../config/types.js"; import { resolveCompatibilityHostVersion } from "../version.js"; import { normalizePluginsConfig, resolveEffectivePluginActivationState } from "./config-state.js"; import { isPluginEnabledByDefaultForPlatform } from "./default-enablement.js"; +import type { PluginDiscoveryResult } from "./discovery.js"; import { normalizeInstallRecordMap } from "./installed-plugin-index-install-records.js"; import { resolveCompatRegistryVersion, @@ -42,9 +43,9 @@ export { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index- function buildInstalledPluginIndex( params: LoadInstalledPluginIndexParams & { refreshReason?: InstalledPluginIndexRefreshReason }, -): InstalledPluginIndex { +): { index: InstalledPluginIndex; discovery: PluginDiscoveryResult | undefined } { const env = params.env ?? process.env; - const { candidates, registry } = resolveInstalledPluginIndexRegistry(params); + const { candidates, registry, discovery } = resolveInstalledPluginIndexRegistry(params); const registryDiagnostics = registry.diagnostics ?? []; const diagnostics = [...registryDiagnostics]; const generatedAtMs = (params.now?.() ?? new Date()).getTime(); @@ -65,30 +66,39 @@ function buildInstalledPluginIndex( }); return { - version: INSTALLED_PLUGIN_INDEX_VERSION, - warning: INSTALLED_PLUGIN_INDEX_WARNING, - hostContractVersion: resolveCompatibilityHostVersion(env), - compatRegistryVersion: resolveCompatRegistryVersion(), - migrationVersion: INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION, - policyHash: resolveInstalledPluginIndexPolicyHash(params.config), - generatedAtMs, - ...(params.refreshReason ? { refreshReason: params.refreshReason } : {}), - installRecords, - plugins, - diagnostics, + index: { + version: INSTALLED_PLUGIN_INDEX_VERSION, + warning: INSTALLED_PLUGIN_INDEX_WARNING, + hostContractVersion: resolveCompatibilityHostVersion(env), + compatRegistryVersion: resolveCompatRegistryVersion(), + migrationVersion: INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION, + policyHash: resolveInstalledPluginIndexPolicyHash(params.config), + generatedAtMs, + ...(params.refreshReason ? { refreshReason: params.refreshReason } : {}), + installRecords, + plugins, + diagnostics, + }, + discovery, }; } export function loadInstalledPluginIndex( params: LoadInstalledPluginIndexParams = {}, ): InstalledPluginIndex { + return buildInstalledPluginIndex(params).index; +} + +export function loadInstalledPluginIndexWithDiscovery( + params: LoadInstalledPluginIndexParams = {}, +): { index: InstalledPluginIndex; discovery: PluginDiscoveryResult | undefined } { return buildInstalledPluginIndex(params); } export function refreshInstalledPluginIndex( params: RefreshInstalledPluginIndexParams, ): InstalledPluginIndex { - return buildInstalledPluginIndex({ ...params, refreshReason: params.reason }); + return buildInstalledPluginIndex({ ...params, refreshReason: params.reason }).index; } export function listInstalledPluginRecords( diff --git a/src/plugins/plugin-metadata-snapshot.ts b/src/plugins/plugin-metadata-snapshot.ts index 06c093416bf..e0daece4d44 100644 --- a/src/plugins/plugin-metadata-snapshot.ts +++ b/src/plugins/plugin-metadata-snapshot.ts @@ -681,6 +681,7 @@ function loadPluginMetadataSnapshotImpl(params: LoadPluginMetadataSnapshotParams indexPluginCount: index.plugins.length, manifestPluginCount: manifestRegistry.plugins.length, }, + discovery: registryResult.discovery, }, }; } diff --git a/src/plugins/plugin-metadata-snapshot.types.ts b/src/plugins/plugin-metadata-snapshot.types.ts index c6b064c209f..924eaff062d 100644 --- a/src/plugins/plugin-metadata-snapshot.types.ts +++ b/src/plugins/plugin-metadata-snapshot.types.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; +import type { PluginDiscoveryResult } from "./discovery.js"; import type { InstalledPluginIndex } from "./installed-plugin-index-types.js"; import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js"; import type { PluginDiagnostic } from "./manifest-types.js"; @@ -48,6 +49,7 @@ export type PluginMetadataSnapshot = { normalizePluginId: (pluginId: string) => string; owners: PluginMetadataSnapshotOwnerMaps; metrics: PluginMetadataSnapshotMetrics; + discovery?: PluginDiscoveryResult; }; export type PluginMetadataRegistryView = Pick; diff --git a/src/plugins/plugin-registry-snapshot.ts b/src/plugins/plugin-registry-snapshot.ts index 63440da270a..e07385f5a49 100644 --- a/src/plugins/plugin-registry-snapshot.ts +++ b/src/plugins/plugin-registry-snapshot.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { resolveUserPath } from "../utils.js"; import { resolveBundledPluginsDir } from "./bundled-dir.js"; import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; +import type { PluginDiscoveryResult } from "./discovery.js"; import { fileSignatureMatches } from "./installed-plugin-index-hash.js"; import { hasOptionalMissingPluginManifestFile } from "./installed-plugin-index-manifest.js"; import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-record-reader.js"; @@ -19,7 +20,7 @@ import { extractPluginInstallRecordsFromInstalledPluginIndex, isInstalledPluginEnabled, listInstalledPluginRecords, - loadInstalledPluginIndex, + loadInstalledPluginIndexWithDiscovery, resolveInstalledPluginIndexPolicyHash, type InstalledPluginIndex, type InstalledPluginIndexRecord, @@ -48,6 +49,7 @@ export type PluginRegistrySnapshotResult = { snapshot: PluginRegistrySnapshot; source: PluginRegistrySnapshotSource; diagnostics: readonly PluginRegistrySnapshotDiagnostic[]; + discovery?: PluginDiscoveryResult; }; export const DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV = "OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY"; @@ -347,11 +349,12 @@ export function loadPluginRegistrySnapshotWithMetadata( "Persisted plugin registry is missing recoverable managed npm plugins; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", }); } else { - return { + const persistedResult: PluginRegistrySnapshotResult = { snapshot: persistedIndex, source: "persisted", diagnostics, }; + return persistedResult; } } else if (persistedReadsEnabled) { diagnostics.push({ @@ -370,15 +373,17 @@ export function loadPluginRegistrySnapshotWithMetadata( }); } + const derived = loadInstalledPluginIndexWithDiscovery({ + ...params, + installRecords: persistedInstallRecordReadsEnabled + ? params.installRecords + : (params.installRecords ?? {}), + }); return { - snapshot: loadInstalledPluginIndex({ - ...params, - ...(persistedInstallRecordReadsEnabled - ? {} - : { installRecords: params.installRecords ?? {} }), - }), + snapshot: derived.index, source: "derived", diagnostics, + discovery: derived.discovery, }; } diff --git a/src/plugins/provider-runtime.ts b/src/plugins/provider-runtime.ts index 0cb3ea9397f..de52b0ee4cf 100644 --- a/src/plugins/provider-runtime.ts +++ b/src/plugins/provider-runtime.ts @@ -12,6 +12,7 @@ import { createSubsystemLogger } from "../logging/subsystem.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { sanitizeForLog } from "../terminal/ansi.js"; import { normalizeProviderModelIdWithManifest } from "./manifest-model-id-normalization.js"; +import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; import { resolvePluginDiscoveryProvidersRuntime } from "./provider-discovery.runtime.js"; import { prepareProviderExtraParams, @@ -925,10 +926,16 @@ export function resolveExternalAuthProfilesWithPlugins(params: { }): ProviderExternalAuthProfile[] { const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(); const env = params.env ?? process.env; + const { manifestRegistry } = loadPluginMetadataSnapshot({ + config: params.config ?? {}, + workspaceDir, + env, + }); const externalAuthPluginIds = resolveExternalAuthProfileProviderPluginIds({ config: params.config, workspaceDir, env, + manifestRegistry, }); const declaredPluginIds = new Set(externalAuthPluginIds); const fallbackPluginIds = resolveExternalAuthProfileCompatFallbackPluginIds({ @@ -936,6 +943,7 @@ export function resolveExternalAuthProfilesWithPlugins(params: { workspaceDir, env, declaredPluginIds, + manifestRegistry, }); const pluginIds = [...new Set([...externalAuthPluginIds, ...fallbackPluginIds])].toSorted( (left, right) => left.localeCompare(right), diff --git a/src/plugins/providers.runtime.ts b/src/plugins/providers.runtime.ts index 207ffc73fc6..54aa63989f1 100644 --- a/src/plugins/providers.runtime.ts +++ b/src/plugins/providers.runtime.ts @@ -9,6 +9,7 @@ import { loadOpenClawPlugins, type PluginLoadOptions, } from "./loader.js"; +import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; import type { PluginMetadataRegistryView } from "./plugin-metadata-snapshot.types.js"; import { hasExplicitPluginIdScope } from "./plugin-scope.js"; import { resolveProviderConfigApiOwnerHint } from "./provider-config-owner.js"; @@ -33,13 +34,15 @@ function dedupeSortedPluginIds(values: Iterable): string[] { return [...new Set(values)].toSorted((left, right) => left.localeCompare(right)); } -function resolveExplicitProviderOwnerPluginIds(params: { - providerRefs: readonly string[]; - config?: PluginLoadOptions["config"]; - workspaceDir?: string; - env?: PluginLoadOptions["env"]; - pluginMetadataSnapshot?: PluginMetadataRegistryView; -}): string[] { +function resolveExplicitProviderOwnerPluginIds( + params: { + providerRefs: readonly string[]; + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; + }, + snapshot: PluginMetadataRegistryView, +): string[] { return dedupeSortedPluginIds( params.providerRefs.flatMap((provider) => { const plannedPluginIds = resolveManifestActivationPluginIds({ @@ -50,7 +53,7 @@ function resolveExplicitProviderOwnerPluginIds(params: { config: params.config, workspaceDir: params.workspaceDir, env: params.env, - manifestRecords: params.pluginMetadataSnapshot?.manifestRegistry.plugins, + manifestRecords: snapshot.manifestRegistry.plugins, }); if (plannedPluginIds.length > 0) { return plannedPluginIds; @@ -68,7 +71,7 @@ function resolveExplicitProviderOwnerPluginIds(params: { config: params.config, workspaceDir: params.workspaceDir, env: params.env, - manifestRecords: params.pluginMetadataSnapshot?.manifestRegistry.plugins, + manifestRecords: snapshot.manifestRegistry.plugins, }); if (apiOwnerPluginIds.length > 0) { return apiOwnerPluginIds; @@ -78,7 +81,7 @@ function resolveExplicitProviderOwnerPluginIds(params: { config: params.config, workspaceDir: params.workspaceDir, env: params.env, - manifestRegistry: params.pluginMetadataSnapshot?.manifestRegistry, + manifestRegistry: snapshot.manifestRegistry, }); if (legacyApiOwnerPluginIds?.length) { return legacyApiOwnerPluginIds; @@ -92,7 +95,7 @@ function resolveExplicitProviderOwnerPluginIds(params: { config: params.config, workspaceDir: params.workspaceDir, env: params.env, - manifestRegistry: params.pluginMetadataSnapshot?.manifestRegistry, + manifestRegistry: snapshot.manifestRegistry, }) ?? [] ); }), @@ -109,25 +112,29 @@ function mergeExplicitOwnerPluginIds( return dedupeSortedPluginIds([...providerPluginIds, ...explicitOwnerPluginIds]); } -function resolvePluginProviderLoadBase(params: { - config?: PluginLoadOptions["config"]; - workspaceDir?: string; - env?: PluginLoadOptions["env"]; - onlyPluginIds?: string[]; - providerRefs?: readonly string[]; - modelRefs?: readonly string[]; - pluginMetadataSnapshot?: PluginMetadataRegistryView; -}) { +function resolvePluginProviderLoadBase( + params: { + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; + onlyPluginIds?: string[]; + providerRefs?: readonly string[]; + modelRefs?: readonly string[]; + }, + snapshot: PluginMetadataRegistryView, +) { const env = params.env ?? process.env; const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir(); const providerOwnedPluginIds = params.providerRefs?.length - ? resolveExplicitProviderOwnerPluginIds({ - providerRefs: params.providerRefs, - config: params.config, - workspaceDir, - env, - pluginMetadataSnapshot: params.pluginMetadataSnapshot, - }) + ? resolveExplicitProviderOwnerPluginIds( + { + providerRefs: params.providerRefs, + config: params.config, + workspaceDir, + env, + }, + snapshot, + ) : []; const modelOwnedPluginIds = params.modelRefs?.length ? resolveOwningPluginIdsForModelRefs({ @@ -135,7 +142,7 @@ function resolvePluginProviderLoadBase(params: { config: params.config, workspaceDir, env, - manifestRegistry: params.pluginMetadataSnapshot?.manifestRegistry, + manifestRegistry: snapshot.manifestRegistry, }) : []; const requestedPluginIds = @@ -168,6 +175,7 @@ function resolvePluginProviderLoadBase(params: { function resolveSetupProviderPluginLoadState( params: Parameters[0], base: ReturnType, + snapshot: PluginMetadataRegistryView, ) { const providerPluginIds = resolveDiscoveredProviderPluginIds({ config: params.config, @@ -175,8 +183,8 @@ function resolveSetupProviderPluginLoadState( env: base.env, onlyPluginIds: base.requestedPluginIds, includeUntrustedWorkspacePlugins: params.includeUntrustedWorkspacePlugins, - registry: params.pluginMetadataSnapshot?.index, - manifestRegistry: params.pluginMetadataSnapshot?.manifestRegistry, + registry: snapshot.index, + manifestRegistry: snapshot.manifestRegistry, }); const explicitOwnerPluginIds = resolveDiscoverableProviderOwnerPluginIds({ pluginIds: base.explicitOwnerPluginIds, @@ -184,8 +192,8 @@ function resolveSetupProviderPluginLoadState( workspaceDir: base.workspaceDir, env: base.env, includeUntrustedWorkspacePlugins: params.includeUntrustedWorkspacePlugins, - registry: params.pluginMetadataSnapshot?.index, - manifestRegistry: params.pluginMetadataSnapshot?.manifestRegistry, + registry: snapshot.index, + manifestRegistry: snapshot.manifestRegistry, }); const setupPluginIds = mergeExplicitOwnerPluginIds(providerPluginIds, explicitOwnerPluginIds); if (setupPluginIds.length === 0) { @@ -203,9 +211,8 @@ function resolveSetupProviderPluginLoadState( workspaceDir: base.workspaceDir, env: base.env, logger: createPluginRuntimeLoaderLogger(), - installRecords: params.pluginMetadataSnapshot - ? extractPluginInstallRecordsFromInstalledPluginIndex(params.pluginMetadataSnapshot.index) - : undefined, + manifestRegistry: snapshot.manifestRegistry, + installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(snapshot.index), }, { onlyPluginIds: setupPluginIds, @@ -220,6 +227,7 @@ function resolveSetupProviderPluginLoadState( function resolveRuntimeProviderPluginLoadState( params: Parameters[0], base: ReturnType, + snapshot: PluginMetadataRegistryView, ) { const explicitOwnerPluginIds = resolveActivatableProviderOwnerPluginIds({ pluginIds: base.explicitOwnerPluginIds, @@ -227,8 +235,8 @@ function resolveRuntimeProviderPluginLoadState( workspaceDir: base.workspaceDir, env: base.env, includeUntrustedWorkspacePlugins: params.includeUntrustedWorkspacePlugins, - registry: params.pluginMetadataSnapshot?.index, - manifestRegistry: params.pluginMetadataSnapshot?.manifestRegistry, + registry: snapshot.index, + manifestRegistry: snapshot.manifestRegistry, }); const runtimeRequestedPluginIds = base.requestedPluginIds !== undefined @@ -252,7 +260,7 @@ function resolveRuntimeProviderPluginLoadState( resolveCompatPluginIds: (compatParams) => resolveBundledProviderCompatPluginIds({ ...compatParams, - manifestRegistry: params.pluginMetadataSnapshot?.manifestRegistry, + manifestRegistry: snapshot.manifestRegistry, }), }); const config = params.bundledProviderVitestCompat @@ -268,8 +276,8 @@ function resolveRuntimeProviderPluginLoadState( workspaceDir: base.workspaceDir, env: base.env, onlyPluginIds: runtimeRequestedPluginIds, - registry: params.pluginMetadataSnapshot?.index, - manifestRegistry: params.pluginMetadataSnapshot?.manifestRegistry, + registry: snapshot.index, + manifestRegistry: snapshot.manifestRegistry, }), explicitOwnerPluginIds, ); @@ -281,9 +289,8 @@ function resolveRuntimeProviderPluginLoadState( workspaceDir: base.workspaceDir, env: base.env, logger: createPluginRuntimeLoaderLogger(), - installRecords: params.pluginMetadataSnapshot - ? extractPluginInstallRecordsFromInstalledPluginIndex(params.pluginMetadataSnapshot.index) - : undefined, + manifestRegistry: snapshot.manifestRegistry, + installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(snapshot.index), }, { onlyPluginIds: providerPluginIds, @@ -298,11 +305,20 @@ function resolveRuntimeProviderPluginLoadState( export function isPluginProvidersLoadInFlight( params: Parameters[0], ): boolean { - const base = resolvePluginProviderLoadBase(params); + const env = params.env ?? process.env; + const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir(); + const snapshot = + params.pluginMetadataSnapshot ?? + loadPluginMetadataSnapshot({ + config: params.config ?? {}, + workspaceDir, + env, + }); + const base = resolvePluginProviderLoadBase({ ...params, workspaceDir, env }, snapshot); const loadState = params.mode === "setup" - ? resolveSetupProviderPluginLoadState(params, base) - : resolveRuntimeProviderPluginLoadState(params, base); + ? resolveSetupProviderPluginLoadState(params, base, snapshot) + : resolveRuntimeProviderPluginLoadState(params, base, snapshot); if (!loadState) { return false; } @@ -327,9 +343,18 @@ export function resolvePluginProviders(params: { includeUntrustedWorkspacePlugins?: boolean; pluginMetadataSnapshot?: PluginMetadataRegistryView; }): ProviderPlugin[] { - const base = resolvePluginProviderLoadBase(params); + const env = params.env ?? process.env; + const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir(); + const snapshot = + params.pluginMetadataSnapshot ?? + loadPluginMetadataSnapshot({ + config: params.config ?? {}, + workspaceDir, + env, + }); + const base = resolvePluginProviderLoadBase({ ...params, workspaceDir, env }, snapshot); if (params.mode === "setup") { - const loadState = resolveSetupProviderPluginLoadState(params, base); + const loadState = resolveSetupProviderPluginLoadState(params, base, snapshot); if (!loadState) { return []; } @@ -338,7 +363,7 @@ export function resolvePluginProviders(params: { Object.assign({}, entry.provider, { pluginId: entry.pluginId }), ); } - const loadState = resolveRuntimeProviderPluginLoadState(params, base); + const loadState = resolveRuntimeProviderPluginLoadState(params, base, snapshot); const registry = loadState.loadOptions.onlyPluginIds?.length === 0 ? undefined diff --git a/src/plugins/providers.test.ts b/src/plugins/providers.test.ts index 0574541591a..f259c8fef1a 100644 --- a/src/plugins/providers.test.ts +++ b/src/plugins/providers.test.ts @@ -16,6 +16,8 @@ type LoadOpenClawPlugins = typeof import("./loader.js").loadOpenClawPlugins; type IsPluginRegistryLoadInFlight = typeof import("./loader.js").isPluginRegistryLoadInFlight; type LoadPluginManifestRegistry = typeof import("./manifest-registry.js").loadPluginManifestRegistry; +type LoadPluginMetadataSnapshot = + typeof import("./plugin-metadata-snapshot.js").loadPluginMetadataSnapshot; type ApplyPluginAutoEnable = typeof import("../config/plugin-auto-enable.js").applyPluginAutoEnable; type SetActivePluginRegistry = typeof import("./runtime.js").setActivePluginRegistry; @@ -25,6 +27,7 @@ const resolveCompatibleRuntimePluginRegistryMock = vi.fn(); const isPluginRegistryLoadInFlightMock = vi.fn((_) => false); const loadPluginManifestRegistryMock = vi.fn(); +const loadPluginMetadataSnapshotMock = vi.fn(); const applyPluginAutoEnableMock = vi.fn(); let resolveOwningPluginIdsForProvider: typeof import("./providers.js").resolveOwningPluginIdsForProvider; @@ -482,6 +485,15 @@ describe("resolvePluginProviders", () => { loadPluginManifestRegistry: (...args: Parameters) => loadPluginManifestRegistryMock(...args), })); + vi.doMock("./plugin-metadata-snapshot.js", () => ({ + loadPluginMetadataSnapshot: (params: Parameters[0]) => { + loadPluginMetadataSnapshotMock(params); + return { + manifestRegistry: loadPluginManifestRegistryMock(), + index: createProviderRegistrySnapshotFixture(), + }; + }, + })); vi.doMock("./plugin-registry.js", async () => { const actual = await vi.importActual("./plugin-registry.js"); @@ -521,6 +533,18 @@ describe("resolvePluginProviders", () => { expectOwningPluginIds("codex-cli"); }); + it("maps setup-only cli backend ids to owning plugin ids via manifests", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "setup-only-backend-owner", + providerIds: [], + setup: { cliBackends: ["setup-only-cli"] }, + }), + ]); + + expectOwningPluginIds("setup-only-cli", ["setup-only-backend-owner"]); + }); + it("reflects provider ownership manifest changes on the next lookup", () => { setManifestPlugins([ createManifestProviderPlugin({ @@ -548,6 +572,7 @@ describe("resolvePluginProviders", () => { loadOpenClawPluginsMock.mockReset(); isPluginRegistryLoadInFlightMock.mockReset(); isPluginRegistryLoadInFlightMock.mockReturnValue(false); + loadPluginMetadataSnapshotMock.mockReset(); const provider: ProviderPlugin = { id: "demo-provider", label: "Demo Provider", @@ -1154,6 +1179,28 @@ describe("resolvePluginProviders", () => { activate: false, }); }); + + it("inherits workspaceDir from the active registry when loading the metadata snapshot", () => { + setActivePluginRegistry( + createEmptyPluginRegistry(), + undefined, + "default", + "/workspace/runtime", + ); + + resolvePluginProviders({ + config: { + plugins: { + allow: ["google"], + }, + }, + onlyPluginIds: ["google"], + }); + + expect(loadPluginMetadataSnapshotMock).toHaveBeenCalled(); + const snapshotCall = loadPluginMetadataSnapshotMock.mock.calls.at(-1)?.[0]; + expect(snapshotCall?.workspaceDir).toBe("/workspace/runtime"); + }); it("activates owning plugins for explicit provider refs", () => { setOwningProviderManifestPlugins(); diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index d9cdc5d2b11..36b8bd6e568 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -10,11 +10,10 @@ import { } from "./manifest-owner-policy.js"; import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js"; +import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; import { loadPluginRegistrySnapshot, normalizePluginsConfigWithRegistry, - resolvePluginContributionOwners, - resolveProviderOwners, type PluginRegistryRecord, type PluginRegistrySnapshot, } from "./plugin-registry.js"; @@ -512,43 +511,30 @@ export function resolveOwningPluginIdsForProvider(params: { return undefined; } - 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; - } - - const env = params.env ?? process.env; - const pluginIds = [ - ...resolveProviderOwners({ - config: params.config, + const manifestRegistry = + params.manifestRegistry ?? + loadPluginMetadataSnapshot({ + config: params.config ?? {}, workspaceDir: params.workspaceDir, - env, - providerId: normalizedProvider, - includeDisabled: true, - }), - ...resolvePluginContributionOwners({ - config: params.config, - workspaceDir: params.workspaceDir, - env, - contribution: "cliBackends", - matches: (backendId) => normalizeProviderId(backendId) === normalizedProvider, - includeDisabled: true, - }), - ]; + env: params.env ?? process.env, + }).manifestRegistry; - const deduped = dedupeSortedPluginIds(pluginIds); - return deduped.length > 0 ? deduped : undefined; + const pluginIds = manifestRegistry.plugins + .filter( + (plugin) => + plugin.providers.some( + (providerId) => normalizeProviderId(providerId) === normalizedProvider, + ) || + plugin.cliBackends.some( + (backendId) => normalizeProviderId(backendId) === normalizedProvider, + ) || + (plugin.setup?.cliBackends ?? []).some( + (backendId) => normalizeProviderId(backendId) === normalizedProvider, + ), + ) + .map((plugin) => plugin.id); + + return pluginIds.length > 0 ? pluginIds : undefined; } export function resolveOwningPluginIdsForModelRef(params: { diff --git a/src/plugins/runtime/load-context.ts b/src/plugins/runtime/load-context.ts index 14bafd876bb..0167c25c3bd 100644 --- a/src/plugins/runtime/load-context.ts +++ b/src/plugins/runtime/load-context.ts @@ -87,6 +87,7 @@ export function resolvePluginRuntimeLoadContext( config: rawConfig, env, manifestRegistry, + discovery: metadataSnapshot?.discovery, }); const config = autoEnabled.config; const workspaceDir = @@ -111,7 +112,7 @@ export function resolvePluginRuntimeLoadContext( workspaceDir, env, logger: options?.logger ?? createPluginRuntimeLoaderLogger(), - ...(manifestRegistry ? { manifestRegistry } : {}), + manifestRegistry, installRecords, }; } @@ -134,7 +135,7 @@ export function buildPluginRuntimeLoadOptionsFromValues( workspaceDir: values.workspaceDir, env: values.env, logger: values.logger, - ...(values.manifestRegistry ? { manifestRegistry: values.manifestRegistry } : {}), + manifestRegistry: values.manifestRegistry, installRecords: values.installRecords, ...overrides, };