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, };