perf(plugins,gateway): thread metadata snapshot + discovery through hot paths + plugin owner fixes (#84649)

* 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.
This commit is contained in:
Dallin Romney
2026-05-24 13:44:03 -07:00
committed by GitHub
parent 8bf4f7d4a8
commit 8ccb11cbfc
27 changed files with 348 additions and 145 deletions

View File

@@ -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<string>();
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)) {

View File

@@ -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);
}

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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({

View File

@@ -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 [];
}

View File

@@ -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<string>;
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 };
}

View File

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

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;

View File

@@ -125,6 +125,7 @@ export async function loadGatewayStartupConfigSnapshot(params: {
...(pluginMetadataSnapshot?.manifestRegistry
? { manifestRegistry: pluginMetadataSnapshot.manifestRegistry }
: {}),
discovery: pluginMetadataSnapshot?.discovery,
}),
);
if (autoEnable.changes.length === 0) {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -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(

View File

@@ -681,6 +681,7 @@ function loadPluginMetadataSnapshotImpl(params: LoadPluginMetadataSnapshotParams
indexPluginCount: index.plugins.length,
manifestPluginCount: manifestRegistry.plugins.length,
},
discovery: registryResult.discovery,
},
};
}

View File

@@ -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<PluginMetadataSnapshot, "index" | "manifestRegistry">;

View File

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

View File

@@ -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),

View File

@@ -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>): 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<typeof resolvePluginProviders>[0],
base: ReturnType<typeof resolvePluginProviderLoadBase>,
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<typeof resolvePluginProviders>[0],
base: ReturnType<typeof resolvePluginProviderLoadBase>,
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<typeof resolvePluginProviders>[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

View File

@@ -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<ResolveCompatibleRuntim
const loadOpenClawPluginsMock = vi.fn<LoadOpenClawPlugins>();
const isPluginRegistryLoadInFlightMock = vi.fn<IsPluginRegistryLoadInFlight>((_) => false);
const loadPluginManifestRegistryMock = vi.fn<LoadPluginManifestRegistry>();
const loadPluginMetadataSnapshotMock = vi.fn<LoadPluginMetadataSnapshot>();
const applyPluginAutoEnableMock = vi.fn<ApplyPluginAutoEnable>();
let resolveOwningPluginIdsForProvider: typeof import("./providers.js").resolveOwningPluginIdsForProvider;
@@ -482,6 +485,15 @@ describe("resolvePluginProviders", () => {
loadPluginManifestRegistry: (...args: Parameters<LoadPluginManifestRegistry>) =>
loadPluginManifestRegistryMock(...args),
}));
vi.doMock("./plugin-metadata-snapshot.js", () => ({
loadPluginMetadataSnapshot: (params: Parameters<LoadPluginMetadataSnapshot>[0]) => {
loadPluginMetadataSnapshotMock(params);
return {
manifestRegistry: loadPluginManifestRegistryMock(),
index: createProviderRegistrySnapshotFixture(),
};
},
}));
vi.doMock("./plugin-registry.js", async () => {
const actual =
await vi.importActual<typeof import("./plugin-registry.js")>("./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();

View File

@@ -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: {

View File

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