From 8e1755928c6a219cbffc477273b5d27e0ce2ad5b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 26 Apr 2026 12:16:33 -0700 Subject: [PATCH] refactor(plugins): split plugin registry facade --- src/plugins/plugin-registry-contributions.ts | 385 +++++++++++++ src/plugins/plugin-registry-snapshot.ts | 163 ++++++ src/plugins/plugin-registry.ts | 544 +------------------ 3 files changed, 550 insertions(+), 542 deletions(-) create mode 100644 src/plugins/plugin-registry-contributions.ts create mode 100644 src/plugins/plugin-registry-snapshot.ts diff --git a/src/plugins/plugin-registry-contributions.ts b/src/plugins/plugin-registry-contributions.ts new file mode 100644 index 00000000000..af2938f168b --- /dev/null +++ b/src/plugins/plugin-registry-contributions.ts @@ -0,0 +1,385 @@ +import { normalizeProviderId } from "../agents/provider-id.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { + normalizePluginsConfigWithResolver, + type NormalizedPluginsConfig, +} from "./config-normalization-shared.js"; +import { isInstalledPluginEnabled } from "./installed-plugin-index.js"; +import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; +import type { + PluginManifestContractListKey, + PluginManifestRecord, + PluginManifestRegistry, +} from "./manifest-registry.js"; +import type { PluginOrigin } from "./plugin-origin.types.js"; +import { + loadPluginRegistrySnapshot, + type LoadPluginRegistryParams, + type PluginRegistrySnapshot, +} from "./plugin-registry-snapshot.js"; + +export type PluginRegistryContributionOptions = LoadPluginRegistryParams & { + includeDisabled?: boolean; +}; + +export type LoadPluginRegistryManifestParams = LoadPluginRegistryParams & { + includeDisabled?: boolean; + pluginIds?: readonly string[]; +}; + +export type PluginRegistryContributionKey = + | "providers" + | "channels" + | "channelConfigs" + | "setupProviders" + | "cliBackends" + | "modelCatalogProviders" + | "commandAliases" + | "contracts"; + +export type ResolvePluginContributionOwnersParams = PluginRegistryContributionOptions & { + contribution: PluginRegistryContributionKey; + matches: string | ((contributionId: string) => boolean); +}; + +export type ListPluginContributionIdsParams = PluginRegistryContributionOptions & { + contribution: PluginRegistryContributionKey; +}; + +export type ResolveProviderOwnersParams = PluginRegistryContributionOptions & { + providerId: string; +}; + +export type ResolveChannelOwnersParams = PluginRegistryContributionOptions & { + channelId: string; +}; + +export type ResolveCliBackendOwnersParams = PluginRegistryContributionOptions & { + cliBackendId: string; +}; + +export type ResolveSetupProviderOwnersParams = PluginRegistryContributionOptions & { + setupProviderId: string; +}; + +export type ResolveManifestContractPluginIdsParams = LoadPluginRegistryParams & { + contract: PluginManifestContractListKey; + origin?: PluginOrigin; + onlyPluginIds?: readonly string[]; +}; + +export type ResolveManifestContractOwnerPluginIdParams = LoadPluginRegistryParams & { + contract: PluginManifestContractListKey; + value: string | undefined; + origin?: PluginOrigin; +}; + +export type ResolveManifestContractPluginIdsByCompatibilityRuntimePathParams = + LoadPluginRegistryParams & { + contract: PluginManifestContractListKey; + path: string | undefined; + origin?: PluginOrigin; + }; + +function normalizeContributionId(value: string): string { + return value.trim(); +} + +function normalizePluginRegistryAlias(value: string): string { + return value.trim(); +} + +function normalizePluginRegistryAliasKey(value: string): string { + return normalizePluginRegistryAlias(value).toLowerCase(); +} + +function sortUnique(values: Iterable): string[] { + return [...new Set([...values].map((value) => value.trim()).filter(Boolean))].toSorted( + (left, right) => left.localeCompare(right), + ); +} + +function collectObjectKeys(value: Record | undefined): readonly string[] { + return value ? Object.keys(value) : []; +} + +function collectContractKeys(plugin: PluginManifestRecord): readonly string[] { + const contracts = plugin.contracts; + if (!contracts) { + return []; + } + return Object.entries(contracts).flatMap(([key, value]) => + Array.isArray(value) && value.length > 0 ? [key] : [], + ); +} + +function listManifestContractValues( + plugin: PluginManifestRecord, + contract: PluginManifestContractListKey, +): readonly string[] { + return plugin.contracts?.[contract] ?? []; +} + +function loadManifestContractRegistry( + params: LoadPluginRegistryParams & { + onlyPluginIds?: readonly string[]; + }, +): PluginManifestRegistry { + return loadPluginManifestRegistryForPluginRegistry({ + ...params, + pluginIds: params.onlyPluginIds, + includeDisabled: true, + }); +} + +function listManifestContributionIds( + plugin: PluginManifestRecord, + contribution: PluginRegistryContributionKey, +): readonly string[] { + switch (contribution) { + case "providers": + return plugin.providers; + case "channels": + return plugin.channels; + case "channelConfigs": + return collectObjectKeys(plugin.channelConfigs); + case "setupProviders": + return plugin.setup?.providers?.map((provider) => provider.id) ?? []; + case "cliBackends": + return [...plugin.cliBackends, ...(plugin.setup?.cliBackends ?? [])]; + case "modelCatalogProviders": + return collectObjectKeys(plugin.modelCatalog?.providers); + case "commandAliases": + return plugin.commandAliases?.map((alias) => alias.name) ?? []; + case "contracts": + return collectContractKeys(plugin); + } + return []; +} + +function resolveContributionPluginIds(params: { + index: PluginRegistrySnapshot; + includeDisabled?: boolean; + config?: OpenClawConfig; +}): readonly string[] { + if (params.includeDisabled) { + return params.index.plugins.map((plugin) => plugin.pluginId); + } + return params.index.plugins + .filter((plugin) => isInstalledPluginEnabled(params.index, plugin.pluginId, params.config)) + .map((plugin) => plugin.pluginId); +} + +function loadContributionManifestRegistry( + params: LoadPluginRegistryParams & { + index: PluginRegistrySnapshot; + includeDisabled?: boolean; + }, +): PluginManifestRegistry { + return loadPluginManifestRegistryForInstalledIndex({ + index: params.index, + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + pluginIds: resolveContributionPluginIds({ + index: params.index, + includeDisabled: params.includeDisabled, + config: params.config, + }), + includeDisabled: true, + }); +} + +export function loadPluginManifestRegistryForPluginRegistry( + params: LoadPluginRegistryManifestParams = {}, +): PluginManifestRegistry { + const index = loadPluginRegistrySnapshot(params); + return loadPluginManifestRegistryForInstalledIndex({ + index, + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + pluginIds: params.pluginIds, + includeDisabled: params.includeDisabled, + }); +} + +export function createPluginRegistryIdNormalizer( + index: PluginRegistrySnapshot, +): (pluginId: string) => string { + const aliases = new Map(); + for (const plugin of index.plugins) { + const pluginId = normalizePluginRegistryAlias(plugin.pluginId); + if (pluginId) { + aliases.set(normalizePluginRegistryAliasKey(pluginId), plugin.pluginId); + } + } + const registry = loadPluginManifestRegistryForInstalledIndex({ + index, + includeDisabled: true, + }); + for (const plugin of [...registry.plugins].toSorted((left, right) => + left.id.localeCompare(right.id), + )) { + const pluginId = normalizePluginRegistryAlias(plugin.id); + if (!pluginId) { + continue; + } + aliases.set(normalizePluginRegistryAliasKey(pluginId), plugin.id); + for (const alias of [ + plugin.id, + ...listManifestContributionIds(plugin, "providers"), + ...listManifestContributionIds(plugin, "channels"), + ...listManifestContributionIds(plugin, "setupProviders"), + ...listManifestContributionIds(plugin, "cliBackends"), + ...listManifestContributionIds(plugin, "modelCatalogProviders"), + ...(plugin.legacyPluginIds ?? []), + ]) { + const normalizedAlias = normalizePluginRegistryAlias(alias); + const normalizedAliasKey = normalizePluginRegistryAliasKey(alias); + if (normalizedAlias && !aliases.has(normalizedAliasKey)) { + aliases.set(normalizedAliasKey, pluginId); + } + } + } + return (pluginId: string) => { + const trimmed = normalizePluginRegistryAlias(pluginId); + return aliases.get(normalizePluginRegistryAliasKey(trimmed)) ?? trimmed; + }; +} + +export function normalizePluginsConfigWithRegistry( + config: OpenClawConfig["plugins"] | undefined, + index: PluginRegistrySnapshot, +): NormalizedPluginsConfig { + return normalizePluginsConfigWithResolver(config, createPluginRegistryIdNormalizer(index)); +} + +export function listPluginContributionIds( + params: ListPluginContributionIdsParams, +): readonly string[] { + const index = loadPluginRegistrySnapshot(params); + const registry = loadContributionManifestRegistry({ + ...params, + index, + }); + return sortUnique( + registry.plugins.flatMap((plugin) => listManifestContributionIds(plugin, params.contribution)), + ); +} + +export function resolvePluginContributionOwners( + params: ResolvePluginContributionOwnersParams, +): readonly string[] { + const matcher = + typeof params.matches === "string" + ? (contributionId: string) => contributionId === params.matches + : params.matches; + const index = loadPluginRegistrySnapshot(params); + const registry = loadContributionManifestRegistry({ + ...params, + index, + }); + return sortUnique( + registry.plugins.flatMap((plugin) => + listManifestContributionIds(plugin, params.contribution).some(matcher) ? [plugin.id] : [], + ), + ); +} + +export function resolveProviderOwners(params: ResolveProviderOwnersParams): readonly string[] { + const providerId = normalizeProviderId(params.providerId); + if (!providerId) { + return []; + } + return resolvePluginContributionOwners({ + ...params, + contribution: "providers", + matches: (contributionId) => normalizeProviderId(contributionId) === providerId, + }); +} + +export function resolveChannelOwners(params: ResolveChannelOwnersParams): readonly string[] { + const channelId = normalizeContributionId(params.channelId); + if (!channelId) { + return []; + } + return resolvePluginContributionOwners({ + ...params, + contribution: "channels", + matches: channelId, + }); +} + +export function resolveCliBackendOwners(params: ResolveCliBackendOwnersParams): readonly string[] { + const cliBackendId = normalizeContributionId(params.cliBackendId); + if (!cliBackendId) { + return []; + } + return resolvePluginContributionOwners({ + ...params, + contribution: "cliBackends", + matches: cliBackendId, + }); +} + +export function resolveSetupProviderOwners( + params: ResolveSetupProviderOwnersParams, +): readonly string[] { + const setupProviderId = normalizeContributionId(params.setupProviderId); + if (!setupProviderId) { + return []; + } + return resolvePluginContributionOwners({ + ...params, + contribution: "setupProviders", + matches: setupProviderId, + }); +} + +export function resolveManifestContractPluginIds( + params: ResolveManifestContractPluginIdsParams, +): string[] { + return loadManifestContractRegistry(params) + .plugins.filter( + (plugin) => + (!params.origin || plugin.origin === params.origin) && + listManifestContractValues(plugin, params.contract).length > 0, + ) + .map((plugin) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function resolveManifestContractPluginIdsByCompatibilityRuntimePath( + params: ResolveManifestContractPluginIdsByCompatibilityRuntimePathParams, +): string[] { + const normalizedPath = params.path?.trim(); + if (!normalizedPath) { + return []; + } + return loadManifestContractRegistry(params) + .plugins.filter( + (plugin) => + (!params.origin || plugin.origin === params.origin) && + listManifestContractValues(plugin, params.contract).length > 0 && + (plugin.configContracts?.compatibilityRuntimePaths ?? []).includes(normalizedPath), + ) + .map((plugin) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function resolveManifestContractOwnerPluginId( + params: ResolveManifestContractOwnerPluginIdParams, +): string | undefined { + const normalizedValue = normalizeContributionId(params.value ?? "").toLowerCase(); + if (!normalizedValue) { + return undefined; + } + return loadManifestContractRegistry(params).plugins.find( + (plugin) => + (!params.origin || plugin.origin === params.origin) && + listManifestContractValues(plugin, params.contract).some( + (candidate) => normalizeContributionId(candidate).toLowerCase() === normalizedValue, + ), + )?.id; +} diff --git a/src/plugins/plugin-registry-snapshot.ts b/src/plugins/plugin-registry-snapshot.ts new file mode 100644 index 00000000000..778cc1747ef --- /dev/null +++ b/src/plugins/plugin-registry-snapshot.ts @@ -0,0 +1,163 @@ +import { + inspectPersistedInstalledPluginIndex, + readPersistedInstalledPluginIndexSync, + refreshPersistedInstalledPluginIndex, + type InstalledPluginIndexStoreInspection, + type InstalledPluginIndexStoreOptions, +} from "./installed-plugin-index-store.js"; +import { + getInstalledPluginRecord, + extractPluginInstallRecordsFromInstalledPluginIndex, + isInstalledPluginEnabled, + listInstalledPluginRecords, + loadInstalledPluginIndex, + resolveInstalledPluginIndexPolicyHash, + type InstalledPluginIndex, + type InstalledPluginIndexRecord, + type LoadInstalledPluginIndexParams, + type RefreshInstalledPluginIndexParams, +} from "./installed-plugin-index.js"; + +export type PluginRegistrySnapshot = InstalledPluginIndex; +export type PluginRegistryRecord = InstalledPluginIndexRecord; +export type PluginRegistryInspection = InstalledPluginIndexStoreInspection; +export type PluginRegistrySnapshotSource = "provided" | "persisted" | "derived"; +export type PluginRegistrySnapshotDiagnosticCode = + | "persisted-registry-disabled" + | "persisted-registry-missing" + | "persisted-registry-stale-policy"; + +export type PluginRegistrySnapshotDiagnostic = { + level: "info" | "warn"; + code: PluginRegistrySnapshotDiagnosticCode; + message: string; +}; + +export type PluginRegistrySnapshotResult = { + snapshot: PluginRegistrySnapshot; + source: PluginRegistrySnapshotSource; + diagnostics: readonly PluginRegistrySnapshotDiagnostic[]; +}; + +export const DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV = "OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY"; + +function formatDeprecatedPersistedRegistryDisableWarning(): string { + return `${DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV} is a deprecated break-glass compatibility switch; use \`openclaw plugins registry --refresh\` or \`openclaw doctor --fix\` to repair registry state.`; +} + +export type LoadPluginRegistryParams = LoadInstalledPluginIndexParams & + InstalledPluginIndexStoreOptions & { + index?: PluginRegistrySnapshot; + preferPersisted?: boolean; + }; + +export type GetPluginRecordParams = LoadPluginRegistryParams & { + pluginId: string; +}; + +function hasEnvFlag(env: NodeJS.ProcessEnv, name: string): boolean { + const value = env[name]?.trim().toLowerCase(); + return Boolean(value && value !== "0" && value !== "false" && value !== "no"); +} + +export function loadPluginRegistrySnapshotWithMetadata( + params: LoadPluginRegistryParams = {}, +): PluginRegistrySnapshotResult { + if (params.index) { + return { + snapshot: params.index, + source: "provided", + diagnostics: [], + }; + } + + const env = params.env ?? process.env; + const diagnostics: PluginRegistrySnapshotDiagnostic[] = []; + const disabledByCaller = params.preferPersisted === false; + const disabledByEnv = hasEnvFlag(env, DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV); + const persistedReadsEnabled = !disabledByCaller && !disabledByEnv; + let persistedIndex: InstalledPluginIndex | null = null; + if (persistedReadsEnabled) { + persistedIndex = readPersistedInstalledPluginIndexSync(params); + if (persistedIndex) { + if ( + params.config && + persistedIndex.policyHash !== resolveInstalledPluginIndexPolicyHash(params.config) + ) { + diagnostics.push({ + level: "warn", + code: "persisted-registry-stale-policy", + message: + "Persisted plugin registry policy does not match current config; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", + }); + } else { + return { + snapshot: persistedIndex, + source: "persisted", + diagnostics, + }; + } + } else { + diagnostics.push({ + level: "info", + code: "persisted-registry-missing", + message: "Persisted plugin registry is missing or invalid; using derived plugin index.", + }); + } + } else { + diagnostics.push({ + level: "warn", + code: "persisted-registry-disabled", + message: disabledByEnv + ? `${formatDeprecatedPersistedRegistryDisableWarning()} Using legacy derived plugin index.` + : "Persisted plugin registry reads are disabled by the caller; using derived plugin index.", + }); + } + + return { + snapshot: loadInstalledPluginIndex({ + ...params, + installRecords: + params.installRecords ?? + extractPluginInstallRecordsFromInstalledPluginIndex(persistedIndex), + }), + source: "derived", + diagnostics, + }; +} + +function resolveSnapshot(params: LoadPluginRegistryParams = {}): PluginRegistrySnapshot { + return loadPluginRegistrySnapshotWithMetadata(params).snapshot; +} + +export function loadPluginRegistrySnapshot( + params: LoadPluginRegistryParams = {}, +): PluginRegistrySnapshot { + return resolveSnapshot(params); +} + +export function listPluginRecords( + params: LoadPluginRegistryParams = {}, +): readonly PluginRegistryRecord[] { + return listInstalledPluginRecords(resolveSnapshot(params)); +} + +export function getPluginRecord(params: GetPluginRecordParams): PluginRegistryRecord | undefined { + return getInstalledPluginRecord(resolveSnapshot(params), params.pluginId); +} + +export function isPluginEnabled(params: GetPluginRecordParams): boolean { + return isInstalledPluginEnabled(resolveSnapshot(params), params.pluginId, params.config); +} + +export function inspectPluginRegistry( + params: LoadInstalledPluginIndexParams & InstalledPluginIndexStoreOptions = {}, +): Promise { + return inspectPersistedInstalledPluginIndex(params); +} + +export function refreshPluginRegistry( + params: RefreshInstalledPluginIndexParams & InstalledPluginIndexStoreOptions, +): Promise { + return refreshPersistedInstalledPluginIndex(params); +} diff --git a/src/plugins/plugin-registry.ts b/src/plugins/plugin-registry.ts index f674212ae93..c166f9bafde 100644 --- a/src/plugins/plugin-registry.ts +++ b/src/plugins/plugin-registry.ts @@ -1,542 +1,2 @@ -import { normalizeProviderId } from "../agents/provider-id.js"; -import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { - normalizePluginsConfigWithResolver, - type NormalizedPluginsConfig, -} from "./config-normalization-shared.js"; -import { - inspectPersistedInstalledPluginIndex, - readPersistedInstalledPluginIndexSync, - refreshPersistedInstalledPluginIndex, - type InstalledPluginIndexStoreInspection, - type InstalledPluginIndexStoreOptions, -} from "./installed-plugin-index-store.js"; -import { - getInstalledPluginRecord, - extractPluginInstallRecordsFromInstalledPluginIndex, - isInstalledPluginEnabled, - listInstalledPluginRecords, - loadInstalledPluginIndex, - resolveInstalledPluginIndexPolicyHash, - type InstalledPluginIndex, - type InstalledPluginIndexRecord, - type LoadInstalledPluginIndexParams, - type RefreshInstalledPluginIndexParams, -} from "./installed-plugin-index.js"; -import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; -import type { - PluginManifestContractListKey, - PluginManifestRecord, - PluginManifestRegistry, -} from "./manifest-registry.js"; -import type { PluginOrigin } from "./plugin-origin.types.js"; - -export type PluginRegistrySnapshot = InstalledPluginIndex; -export type PluginRegistryRecord = InstalledPluginIndexRecord; -export type PluginRegistryInspection = InstalledPluginIndexStoreInspection; -export type PluginRegistrySnapshotSource = "provided" | "persisted" | "derived"; -export type PluginRegistrySnapshotDiagnosticCode = - | "persisted-registry-disabled" - | "persisted-registry-missing" - | "persisted-registry-stale-policy"; - -export type PluginRegistrySnapshotDiagnostic = { - level: "info" | "warn"; - code: PluginRegistrySnapshotDiagnosticCode; - message: string; -}; - -export type PluginRegistrySnapshotResult = { - snapshot: PluginRegistrySnapshot; - source: PluginRegistrySnapshotSource; - diagnostics: readonly PluginRegistrySnapshotDiagnostic[]; -}; - -export const DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV = "OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY"; - -function formatDeprecatedPersistedRegistryDisableWarning(): string { - return `${DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV} is a deprecated break-glass compatibility switch; use \`openclaw plugins registry --refresh\` or \`openclaw doctor --fix\` to repair registry state.`; -} - -export type LoadPluginRegistryParams = LoadInstalledPluginIndexParams & - InstalledPluginIndexStoreOptions & { - index?: PluginRegistrySnapshot; - preferPersisted?: boolean; - }; - -export type PluginRegistryContributionOptions = LoadPluginRegistryParams & { - includeDisabled?: boolean; -}; - -export type LoadPluginRegistryManifestParams = LoadPluginRegistryParams & { - includeDisabled?: boolean; - pluginIds?: readonly string[]; -}; - -export type GetPluginRecordParams = LoadPluginRegistryParams & { - pluginId: string; -}; - -export type PluginRegistryContributionKey = - | "providers" - | "channels" - | "channelConfigs" - | "setupProviders" - | "cliBackends" - | "modelCatalogProviders" - | "commandAliases" - | "contracts"; - -export type ResolvePluginContributionOwnersParams = PluginRegistryContributionOptions & { - contribution: PluginRegistryContributionKey; - matches: string | ((contributionId: string) => boolean); -}; - -export type ListPluginContributionIdsParams = PluginRegistryContributionOptions & { - contribution: PluginRegistryContributionKey; -}; - -export type ResolveProviderOwnersParams = PluginRegistryContributionOptions & { - providerId: string; -}; - -export type ResolveChannelOwnersParams = PluginRegistryContributionOptions & { - channelId: string; -}; - -export type ResolveCliBackendOwnersParams = PluginRegistryContributionOptions & { - cliBackendId: string; -}; - -export type ResolveSetupProviderOwnersParams = PluginRegistryContributionOptions & { - setupProviderId: string; -}; - -export type ResolveManifestContractPluginIdsParams = LoadPluginRegistryParams & { - contract: PluginManifestContractListKey; - origin?: PluginOrigin; - onlyPluginIds?: readonly string[]; -}; - -export type ResolveManifestContractOwnerPluginIdParams = LoadPluginRegistryParams & { - contract: PluginManifestContractListKey; - value: string | undefined; - origin?: PluginOrigin; -}; - -export type ResolveManifestContractPluginIdsByCompatibilityRuntimePathParams = - LoadPluginRegistryParams & { - contract: PluginManifestContractListKey; - path: string | undefined; - origin?: PluginOrigin; - }; - -function normalizeContributionId(value: string): string { - return value.trim(); -} - -function normalizePluginRegistryAlias(value: string): string { - return value.trim(); -} - -function normalizePluginRegistryAliasKey(value: string): string { - return normalizePluginRegistryAlias(value).toLowerCase(); -} - -function sortUnique(values: Iterable): string[] { - return [...new Set([...values].map((value) => value.trim()).filter(Boolean))].toSorted( - (left, right) => left.localeCompare(right), - ); -} - -function collectObjectKeys(value: Record | undefined): readonly string[] { - return value ? Object.keys(value) : []; -} - -function collectContractKeys(plugin: PluginManifestRecord): readonly string[] { - const contracts = plugin.contracts; - if (!contracts) { - return []; - } - return Object.entries(contracts).flatMap(([key, value]) => - Array.isArray(value) && value.length > 0 ? [key] : [], - ); -} - -function listManifestContractValues( - plugin: PluginManifestRecord, - contract: PluginManifestContractListKey, -): readonly string[] { - return plugin.contracts?.[contract] ?? []; -} - -function loadManifestContractRegistry( - params: LoadPluginRegistryParams & { - onlyPluginIds?: readonly string[]; - }, -): PluginManifestRegistry { - return loadPluginManifestRegistryForPluginRegistry({ - ...params, - pluginIds: params.onlyPluginIds, - includeDisabled: true, - }); -} - -function listManifestContributionIds( - plugin: PluginManifestRecord, - contribution: PluginRegistryContributionKey, -): readonly string[] { - switch (contribution) { - case "providers": - return plugin.providers; - case "channels": - return plugin.channels; - case "channelConfigs": - return collectObjectKeys(plugin.channelConfigs); - case "setupProviders": - return plugin.setup?.providers?.map((provider) => provider.id) ?? []; - case "cliBackends": - return [...plugin.cliBackends, ...(plugin.setup?.cliBackends ?? [])]; - case "modelCatalogProviders": - return collectObjectKeys(plugin.modelCatalog?.providers); - case "commandAliases": - return plugin.commandAliases?.map((alias) => alias.name) ?? []; - case "contracts": - return collectContractKeys(plugin); - } - return []; -} - -function resolveContributionPluginIds(params: { - index: PluginRegistrySnapshot; - includeDisabled?: boolean; - config?: OpenClawConfig; -}): readonly string[] { - if (params.includeDisabled) { - return params.index.plugins.map((plugin) => plugin.pluginId); - } - return params.index.plugins - .filter((plugin) => isInstalledPluginEnabled(params.index, plugin.pluginId, params.config)) - .map((plugin) => plugin.pluginId); -} - -function loadContributionManifestRegistry( - params: LoadPluginRegistryParams & { - index: PluginRegistrySnapshot; - includeDisabled?: boolean; - }, -): PluginManifestRegistry { - return loadPluginManifestRegistryForInstalledIndex({ - index: params.index, - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - pluginIds: resolveContributionPluginIds({ - index: params.index, - includeDisabled: params.includeDisabled, - config: params.config, - }), - includeDisabled: true, - }); -} - -export function loadPluginManifestRegistryForPluginRegistry( - params: LoadPluginRegistryManifestParams = {}, -): PluginManifestRegistry { - const index = resolveSnapshot(params); - return loadPluginManifestRegistryForInstalledIndex({ - index, - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - pluginIds: params.pluginIds, - includeDisabled: params.includeDisabled, - }); -} - -export function createPluginRegistryIdNormalizer( - index: PluginRegistrySnapshot, -): (pluginId: string) => string { - const aliases = new Map(); - for (const plugin of index.plugins) { - const pluginId = normalizePluginRegistryAlias(plugin.pluginId); - if (pluginId) { - aliases.set(normalizePluginRegistryAliasKey(pluginId), plugin.pluginId); - } - } - const registry = loadPluginManifestRegistryForInstalledIndex({ - index, - includeDisabled: true, - }); - for (const plugin of [...registry.plugins].toSorted((left, right) => - left.id.localeCompare(right.id), - )) { - const pluginId = normalizePluginRegistryAlias(plugin.id); - if (!pluginId) { - continue; - } - aliases.set(normalizePluginRegistryAliasKey(pluginId), plugin.id); - for (const alias of [ - plugin.id, - ...listManifestContributionIds(plugin, "providers"), - ...listManifestContributionIds(plugin, "channels"), - ...listManifestContributionIds(plugin, "setupProviders"), - ...listManifestContributionIds(plugin, "cliBackends"), - ...listManifestContributionIds(plugin, "modelCatalogProviders"), - ...(plugin.legacyPluginIds ?? []), - ]) { - const normalizedAlias = normalizePluginRegistryAlias(alias); - const normalizedAliasKey = normalizePluginRegistryAliasKey(alias); - if (normalizedAlias && !aliases.has(normalizedAliasKey)) { - aliases.set(normalizedAliasKey, pluginId); - } - } - } - return (pluginId: string) => { - const trimmed = normalizePluginRegistryAlias(pluginId); - return aliases.get(normalizePluginRegistryAliasKey(trimmed)) ?? trimmed; - }; -} - -export function normalizePluginsConfigWithRegistry( - config: OpenClawConfig["plugins"] | undefined, - index: PluginRegistrySnapshot, -): NormalizedPluginsConfig { - return normalizePluginsConfigWithResolver(config, createPluginRegistryIdNormalizer(index)); -} - -function hasEnvFlag(env: NodeJS.ProcessEnv, name: string): boolean { - const value = env[name]?.trim().toLowerCase(); - return Boolean(value && value !== "0" && value !== "false" && value !== "no"); -} - -export function loadPluginRegistrySnapshotWithMetadata( - params: LoadPluginRegistryParams = {}, -): PluginRegistrySnapshotResult { - if (params.index) { - return { - snapshot: params.index, - source: "provided", - diagnostics: [], - }; - } - - const env = params.env ?? process.env; - const diagnostics: PluginRegistrySnapshotDiagnostic[] = []; - const disabledByCaller = params.preferPersisted === false; - const disabledByEnv = hasEnvFlag(env, DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV); - const persistedReadsEnabled = !disabledByCaller && !disabledByEnv; - let persistedIndex: InstalledPluginIndex | null = null; - if (persistedReadsEnabled) { - persistedIndex = readPersistedInstalledPluginIndexSync(params); - if (persistedIndex) { - if ( - params.config && - persistedIndex.policyHash !== resolveInstalledPluginIndexPolicyHash(params.config) - ) { - diagnostics.push({ - level: "warn", - code: "persisted-registry-stale-policy", - message: - "Persisted plugin registry policy does not match current config; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", - }); - } else { - return { - snapshot: persistedIndex, - source: "persisted", - diagnostics, - }; - } - } else { - diagnostics.push({ - level: "info", - code: "persisted-registry-missing", - message: "Persisted plugin registry is missing or invalid; using derived plugin index.", - }); - } - } else { - diagnostics.push({ - level: "warn", - code: "persisted-registry-disabled", - message: disabledByEnv - ? `${formatDeprecatedPersistedRegistryDisableWarning()} Using legacy derived plugin index.` - : "Persisted plugin registry reads are disabled by the caller; using derived plugin index.", - }); - } - - return { - snapshot: loadInstalledPluginIndex({ - ...params, - installRecords: - params.installRecords ?? - extractPluginInstallRecordsFromInstalledPluginIndex(persistedIndex), - }), - source: "derived", - diagnostics, - }; -} - -function resolveSnapshot(params: LoadPluginRegistryParams = {}): PluginRegistrySnapshot { - return loadPluginRegistrySnapshotWithMetadata(params).snapshot; -} - -export function loadPluginRegistrySnapshot( - params: LoadPluginRegistryParams = {}, -): PluginRegistrySnapshot { - return resolveSnapshot(params); -} - -export function listPluginRecords( - params: LoadPluginRegistryParams = {}, -): readonly PluginRegistryRecord[] { - return listInstalledPluginRecords(resolveSnapshot(params)); -} - -export function getPluginRecord(params: GetPluginRecordParams): PluginRegistryRecord | undefined { - return getInstalledPluginRecord(resolveSnapshot(params), params.pluginId); -} - -export function isPluginEnabled(params: GetPluginRecordParams): boolean { - return isInstalledPluginEnabled(resolveSnapshot(params), params.pluginId, params.config); -} - -export function listPluginContributionIds( - params: ListPluginContributionIdsParams, -): readonly string[] { - const index = resolveSnapshot(params); - const registry = loadContributionManifestRegistry({ - ...params, - index, - }); - return sortUnique( - registry.plugins.flatMap((plugin) => listManifestContributionIds(plugin, params.contribution)), - ); -} - -export function resolvePluginContributionOwners( - params: ResolvePluginContributionOwnersParams, -): readonly string[] { - const matcher = - typeof params.matches === "string" - ? (contributionId: string) => contributionId === params.matches - : params.matches; - const index = resolveSnapshot(params); - const registry = loadContributionManifestRegistry({ - ...params, - index, - }); - return sortUnique( - registry.plugins.flatMap((plugin) => - listManifestContributionIds(plugin, params.contribution).some(matcher) ? [plugin.id] : [], - ), - ); -} - -export function resolveProviderOwners(params: ResolveProviderOwnersParams): readonly string[] { - const providerId = normalizeProviderId(params.providerId); - if (!providerId) { - return []; - } - return resolvePluginContributionOwners({ - ...params, - contribution: "providers", - matches: (contributionId) => normalizeProviderId(contributionId) === providerId, - }); -} - -export function resolveChannelOwners(params: ResolveChannelOwnersParams): readonly string[] { - const channelId = normalizeContributionId(params.channelId); - if (!channelId) { - return []; - } - return resolvePluginContributionOwners({ - ...params, - contribution: "channels", - matches: channelId, - }); -} - -export function resolveCliBackendOwners(params: ResolveCliBackendOwnersParams): readonly string[] { - const cliBackendId = normalizeContributionId(params.cliBackendId); - if (!cliBackendId) { - return []; - } - return resolvePluginContributionOwners({ - ...params, - contribution: "cliBackends", - matches: cliBackendId, - }); -} - -export function resolveSetupProviderOwners( - params: ResolveSetupProviderOwnersParams, -): readonly string[] { - const setupProviderId = normalizeContributionId(params.setupProviderId); - if (!setupProviderId) { - return []; - } - return resolvePluginContributionOwners({ - ...params, - contribution: "setupProviders", - matches: setupProviderId, - }); -} - -export function resolveManifestContractPluginIds( - params: ResolveManifestContractPluginIdsParams, -): string[] { - return loadManifestContractRegistry(params) - .plugins.filter( - (plugin) => - (!params.origin || plugin.origin === params.origin) && - listManifestContractValues(plugin, params.contract).length > 0, - ) - .map((plugin) => plugin.id) - .toSorted((left, right) => left.localeCompare(right)); -} - -export function resolveManifestContractPluginIdsByCompatibilityRuntimePath( - params: ResolveManifestContractPluginIdsByCompatibilityRuntimePathParams, -): string[] { - const normalizedPath = params.path?.trim(); - if (!normalizedPath) { - return []; - } - return loadManifestContractRegistry(params) - .plugins.filter( - (plugin) => - (!params.origin || plugin.origin === params.origin) && - listManifestContractValues(plugin, params.contract).length > 0 && - (plugin.configContracts?.compatibilityRuntimePaths ?? []).includes(normalizedPath), - ) - .map((plugin) => plugin.id) - .toSorted((left, right) => left.localeCompare(right)); -} - -export function resolveManifestContractOwnerPluginId( - params: ResolveManifestContractOwnerPluginIdParams, -): string | undefined { - const normalizedValue = normalizeContributionId(params.value ?? "").toLowerCase(); - if (!normalizedValue) { - return undefined; - } - return loadManifestContractRegistry(params).plugins.find( - (plugin) => - (!params.origin || plugin.origin === params.origin) && - listManifestContractValues(plugin, params.contract).some( - (candidate) => normalizeContributionId(candidate).toLowerCase() === normalizedValue, - ), - )?.id; -} - -export function inspectPluginRegistry( - params: LoadInstalledPluginIndexParams & InstalledPluginIndexStoreOptions = {}, -): Promise { - return inspectPersistedInstalledPluginIndex(params); -} - -export function refreshPluginRegistry( - params: RefreshInstalledPluginIndexParams & InstalledPluginIndexStoreOptions, -): Promise { - return refreshPersistedInstalledPluginIndex(params); -} +export * from "./plugin-registry-contributions.js"; +export * from "./plugin-registry-snapshot.js";