mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix(plugins): normalize startup config from registry
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 })) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user