fix(plugins): normalize startup config from registry

This commit is contained in:
Vincent Koc
2026-04-25 05:53:33 -07:00
parent f14aa65bcc
commit 2f622acec6
5 changed files with 139 additions and 39 deletions

View File

@@ -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<string> {
const channels = config.channels;
@@ -64,7 +63,10 @@ function resolveGatewayStartupDreamingPluginIds(config: OpenClawConfig): Set<str
return new Set([DEFAULT_MEMORY_DREAMING_PLUGIN_ID, resolveMemoryDreamingPluginId(config)]);
}
function resolveExplicitMemorySlotStartupPluginId(config: OpenClawConfig): string | undefined {
function resolveExplicitMemorySlotStartupPluginId(
config: OpenClawConfig,
normalizePluginId: (pluginId: string) => 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<typeof normalizePluginsConfig>;
activationSource: ReturnType<typeof createPluginActivationSource>;
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
activationSource: {
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
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 })) {

View File

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

View File

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

View File

@@ -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<typeof normalizePluginsConfig>;
type NormalizedPluginsConfig = ReturnType<typeof normalizePluginsConfigWithRegistry>;
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) =>