diff --git a/CHANGELOG.md b/CHANGELOG.md index 95cbb2b8260..af0e0005976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai - Plugins/CLI: add `openclaw plugins registry` for explicit persisted-registry inspection and `--refresh` repair without making normal startup rescan plugin locations. Thanks @vincentkoc. - Plugins/CLI: make `openclaw plugins list` read the cold persisted registry snapshot by default, leaving module-aware diagnostics to `plugins doctor` and `plugins inspect`. Thanks @vincentkoc. - Plugins/startup: move gateway startup plugin planning onto the versioned cold registry index, with postinstall repair for older registry files that predate startup metadata. Thanks @vincentkoc. +- Plugins/startup: normalize startup and provider plugin enablement through registry aliases so boot paths do not need the legacy manifest alias scan. Thanks @vincentkoc. - Providers/plugins: resolve provider ownership, provider discovery scopes, and catalog-hook provider ids from the cold plugin registry instead of rescanning manifests on those paths. Thanks @vincentkoc. - Plugins/chat commands: refresh the persisted plugin registry after `/plugins enable` and `/plugins disable`, matching the CLI mutation path. Thanks @vincentkoc. - Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna. diff --git a/src/plugins/gateway-startup-plugin-ids.ts b/src/plugins/gateway-startup-plugin-ids.ts index a3fe80362ce..cf8257b2164 100644 --- a/src/plugins/gateway-startup-plugin-ids.ts +++ b/src/plugins/gateway-startup-plugin-ids.ts @@ -9,14 +9,13 @@ import { } from "../memory-host-sdk/dreaming.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; import { hasExplicitChannelConfig } from "./channel-presence-policy.js"; -import { - createPluginActivationSource, - normalizePluginId, - normalizePluginsConfig, - resolveEffectivePluginActivationState, -} from "./config-state.js"; +import { resolveEffectivePluginActivationState } from "./config-state.js"; import type { InstalledPluginIndexRecord } from "./installed-plugin-index.js"; -import { loadPluginRegistrySnapshot } from "./plugin-registry.js"; +import { + createPluginRegistryIdNormalizer, + loadPluginRegistrySnapshot, + normalizePluginsConfigWithRegistry, +} from "./plugin-registry.js"; function listDisabledChannelIds(config: OpenClawConfig): Set { const channels = config.channels; @@ -64,7 +63,10 @@ function resolveGatewayStartupDreamingPluginIds(config: OpenClawConfig): Set string, +): string | undefined { const configuredSlot = config.plugins?.slots?.memory?.trim(); if (!configuredSlot || configuredSlot.toLowerCase() === "none") { return undefined; @@ -101,8 +103,11 @@ function hasConfiguredStartupChannel(params: { function canStartConfiguredChannelPlugin(params: { plugin: InstalledPluginIndexRecord; config: OpenClawConfig; - pluginsConfig: ReturnType; - activationSource: ReturnType; + pluginsConfig: ReturnType; + activationSource: { + plugins: ReturnType; + rootConfig?: OpenClawConfig; + }; }): boolean { if (!params.pluginsConfig.enabled) { return false; @@ -166,15 +171,16 @@ export function resolveConfiguredDeferredChannelPluginIds(params: { if (configuredChannelIds.size === 0) { return []; } - const pluginsConfig = normalizePluginsConfig(params.config.plugins); - const activationSource = createPluginActivationSource({ - config: params.config, - }); const index = loadPluginRegistrySnapshot({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, }); + const pluginsConfig = normalizePluginsConfigWithRegistry(params.config.plugins, index); + const activationSource = { + plugins: pluginsConfig, + rootConfig: params.config, + }; return index.plugins .filter( (plugin) => @@ -197,28 +203,28 @@ export function resolveGatewayStartupPluginIds(params: { env: NodeJS.ProcessEnv; }): string[] { const configuredChannelIds = new Set(listPotentialEnabledChannelIds(params.config, params.env)); - const pluginsConfig = normalizePluginsConfig(params.config.plugins); - // Startup must classify allowlist exceptions against the raw config snapshot, - // not the auto-enabled effective snapshot, or configured-only channels can be - // misclassified as explicit enablement. - const activationSource = createPluginActivationSource({ - config: params.activationSourceConfig ?? params.config, - }); - const requiredAgentHarnessRuntimes = new Set( - collectConfiguredAgentHarnessRuntimes( - params.activationSourceConfig ?? params.config, - params.env, - ), - ); - const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config); - const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId( - params.activationSourceConfig ?? params.config, - ); const index = loadPluginRegistrySnapshot({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, }); + const pluginsConfig = normalizePluginsConfigWithRegistry(params.config.plugins, index); + // Startup must classify allowlist exceptions against the raw config snapshot, + // not the auto-enabled effective snapshot, or configured-only channels can be + // misclassified as explicit enablement. + const activationSourceConfig = params.activationSourceConfig ?? params.config; + const activationSource = { + plugins: normalizePluginsConfigWithRegistry(activationSourceConfig.plugins, index), + rootConfig: activationSourceConfig, + }; + const requiredAgentHarnessRuntimes = new Set( + collectConfiguredAgentHarnessRuntimes(activationSourceConfig, params.env), + ); + const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config); + const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId( + activationSourceConfig, + createPluginRegistryIdNormalizer(index), + ); return index.plugins .filter((plugin) => { if (hasConfiguredStartupChannel({ plugin, configuredChannelIds })) { diff --git a/src/plugins/plugin-registry.test.ts b/src/plugins/plugin-registry.test.ts index 20e6d64e8a5..83ec41ec975 100644 --- a/src/plugins/plugin-registry.test.ts +++ b/src/plugins/plugin-registry.test.ts @@ -6,6 +6,7 @@ import { writePersistedInstalledPluginIndex } from "./installed-plugin-index-sto import type { InstalledPluginIndex } from "./installed-plugin-index.js"; import { DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV, + createPluginRegistryIdNormalizer, getPluginRecord, inspectPluginRegistry, isPluginEnabled, @@ -13,6 +14,7 @@ import { listPluginRecords, loadPluginRegistrySnapshot, loadPluginRegistrySnapshotWithMetadata, + normalizePluginsConfigWithRegistry, refreshPluginRegistry, resolveChannelOwners, resolveCliBackendOwners, @@ -196,6 +198,44 @@ describe("plugin registry facade", () => { ).toEqual(["demo"]); }); + it("normalizes plugin config ids through registry contribution aliases", () => { + const index = createIndex("openai"); + index.plugins[0] = { + ...index.plugins[0]!, + contributions: { + ...index.plugins[0]!.contributions, + providers: ["openai", "openai-codex"], + channels: ["openai-chat"], + }, + }; + + const normalizePluginId = createPluginRegistryIdNormalizer(index); + expect(normalizePluginId("OpenAI-Codex")).toBe("openai"); + expect(normalizePluginId("openai-chat")).toBe("openai"); + expect(normalizePluginId("unknown-plugin")).toBe("unknown-plugin"); + + expect( + normalizePluginsConfigWithRegistry( + { + allow: ["openai-chat"], + entries: { + "OpenAI-Codex": { + enabled: false, + }, + }, + }, + index, + ), + ).toMatchObject({ + allow: ["openai"], + entries: { + openai: { + enabled: false, + }, + }, + }); + }); + it("reads the persisted registry before deriving from discovered candidates", async () => { const stateDir = makeTempDir(); const rootDir = makeTempDir(); diff --git a/src/plugins/plugin-registry.ts b/src/plugins/plugin-registry.ts index 9dcdd28a4f4..7f30d778df3 100644 --- a/src/plugins/plugin-registry.ts +++ b/src/plugins/plugin-registry.ts @@ -1,4 +1,9 @@ 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 { readPersistedInstalledPluginIndexSync, type InstalledPluginIndexStoreInspection, @@ -83,6 +88,53 @@ 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(); +} + +export function createPluginRegistryIdNormalizer( + index: PluginRegistrySnapshot, +): (pluginId: string) => string { + const aliases = new Map(); + for (const plugin of [...index.plugins].toSorted((left, right) => + left.pluginId.localeCompare(right.pluginId), + )) { + const pluginId = normalizePluginRegistryAlias(plugin.pluginId); + if (!pluginId) { + continue; + } + aliases.set(normalizePluginRegistryAliasKey(pluginId), pluginId); + for (const alias of [ + ...plugin.contributions.providers, + ...plugin.contributions.channels, + ...plugin.contributions.setupProviders, + ...plugin.contributions.cliBackends, + ...plugin.contributions.modelCatalogProviders, + ]) { + 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"); diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 6175f0f3c0b..9fff7035b28 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -1,6 +1,6 @@ import { normalizeProviderId } from "../agents/provider-id.js"; import { withBundledPluginVitestCompat } from "./bundled-compat.js"; -import { normalizePluginsConfig, resolveEffectivePluginActivationState } from "./config-state.js"; +import { resolveEffectivePluginActivationState } from "./config-state.js"; import type { PluginLoadOptions } from "./loader.js"; import { isActivatedManifestOwner, @@ -13,6 +13,7 @@ import { } from "./manifest-registry.js"; import { loadPluginRegistrySnapshot, + normalizePluginsConfigWithRegistry, resolvePluginContributionOwners, resolveProviderOwners, type PluginRegistryRecord, @@ -25,7 +26,7 @@ type ProviderManifestLoadParams = { workspaceDir?: string; env?: PluginLoadOptions["env"]; }; -type NormalizedPluginsConfig = ReturnType; +type NormalizedPluginsConfig = ReturnType; type ProviderRegistryLoadParams = ProviderManifestLoadParams & { onlyPluginIds?: readonly string[]; }; @@ -80,7 +81,7 @@ function resolveProviderOwnerPluginIds( } const pluginIdSet = new Set(params.pluginIds); const registry = loadProviderRegistrySnapshot(params); - const normalizedConfig = normalizePluginsConfig(params.config?.plugins); + const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry); return listRegistryPluginIds( registry, (plugin) => pluginIdSet.has(plugin.pluginId) && params.isEligible(plugin, normalizedConfig), @@ -144,7 +145,7 @@ export function resolveEnabledProviderPluginIds(params: { onlyPluginIds?: readonly string[]; }): string[] { const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params); - const normalizedConfig = normalizePluginsConfig(params.config?.plugins); + const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry); return listRegistryPluginIds( registry, (plugin) => @@ -201,7 +202,7 @@ export function resolveExternalAuthProfileCompatFallbackPluginIds(params: { const declaredPluginIds = params.declaredPluginIds ?? new Set(resolveExternalAuthProfileProviderPluginIds(params)); const registry = loadProviderRegistrySnapshot(params); - const normalizedConfig = normalizePluginsConfig(params.config?.plugins); + const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry); return listRegistryPluginIds( registry, (plugin) => @@ -225,7 +226,7 @@ export function resolveDiscoveredProviderPluginIds(params: { }): string[] { const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params); const shouldFilterUntrustedWorkspacePlugins = params.includeUntrustedWorkspacePlugins === false; - const normalizedConfig = normalizePluginsConfig(params.config?.plugins); + const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry); return listRegistryPluginIds(registry, (plugin) => { if ( !( @@ -541,7 +542,7 @@ export function resolveNonBundledProviderPluginIds(params: { env?: PluginLoadOptions["env"]; }): string[] { const registry = loadProviderRegistrySnapshot(params); - const normalizedConfig = normalizePluginsConfig(params.config?.plugins); + const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry); return listRegistryPluginIds( registry, (plugin) => @@ -561,7 +562,7 @@ export function resolveCatalogHookProviderPluginIds(params: { env?: PluginLoadOptions["env"]; }): string[] { const registry = loadProviderRegistrySnapshot(params); - const normalizedConfig = normalizePluginsConfig(params.config?.plugins); + const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry); const enabledProviderPluginIds = listRegistryPluginIds( registry, (plugin) =>