From ad8296e685c9d8b1f05119951a2e687759d9a94b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 04:25:16 -0700 Subject: [PATCH] fix(plugins): harden registry migration guards --- scripts/postinstall-bundled-plugins.mjs | 8 +++- src/plugins/installed-plugin-index.test.ts | 37 +++++++++++++++++++ src/plugins/installed-plugin-index.ts | 15 +++----- .../postinstall-bundled-plugins.test.ts | 19 +++++----- 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/scripts/postinstall-bundled-plugins.mjs b/scripts/postinstall-bundled-plugins.mjs index 9c3aa7584c3..8dc2ba5832d 100644 --- a/scripts/postinstall-bundled-plugins.mjs +++ b/scripts/postinstall-bundled-plugins.mjs @@ -32,6 +32,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const DEFAULT_EXTENSIONS_DIR = join(__dirname, "..", "dist", "extensions"); const DEFAULT_PACKAGE_ROOT = join(__dirname, ".."); const DISABLE_POSTINSTALL_ENV = "OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL"; +const DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION"; const EAGER_BUNDLED_PLUGIN_DEPS_ENV = "OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS"; const DIST_INVENTORY_PATH = "dist/postinstall-inventory.json"; const LEGACY_QA_CHANNEL_DIR = ["qa", "channel"].join("-"); @@ -663,6 +664,11 @@ async function importInstalledDistModule(params, distPath) { export async function runPluginRegistryPostinstallMigration(params = {}) { const log = params.log ?? console; const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT; + const env = params.env ?? process.env; + + if (env[DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV]?.trim()) { + return { status: "disabled", migrated: false, reason: "disabled-env" }; + } try { const migrationModule = await importInstalledDistModule( @@ -677,7 +683,7 @@ export async function runPluginRegistryPostinstallMigration(params = {}) { } const result = await migrationModule.migratePluginRegistryForInstall({ - env: params.env ?? process.env, + env, packageRoot, }); for (const warning of result.preflight?.deprecationWarnings ?? []) { diff --git a/src/plugins/installed-plugin-index.test.ts b/src/plugins/installed-plugin-index.test.ts index 1b802ee56c1..8490e9bdb59 100644 --- a/src/plugins/installed-plugin-index.test.ts +++ b/src/plugins/installed-plugin-index.test.ts @@ -281,6 +281,43 @@ describe("installed plugin index", () => { ).toEqual(["demo"]); }); + it("uses runtime plugin id normalization for legacy enablement aliases", () => { + const rootDir = makeTempDir(); + writeRuntimeEntry(rootDir); + writePluginManifest(rootDir, { + id: "openai", + configSchema: { type: "object" }, + providers: ["openai"], + }); + + const config = { + plugins: { + entries: { + "openai-codex": { + enabled: false, + }, + }, + }, + }; + const index = loadInstalledPluginIndex({ + candidates: [ + createPluginCandidate({ + rootDir, + idHint: "openai", + origin: "bundled", + }), + ], + config, + env: hermeticEnv(), + }); + + expect(index.plugins[0]).toMatchObject({ + pluginId: "openai", + enabled: false, + }); + expect(listEnabledInstalledPluginRecords(index, config)).toEqual([]); + }); + it("records the config install ledger separately from package install intent", () => { const fixture = createRichPluginFixture(); diff --git a/src/plugins/installed-plugin-index.ts b/src/plugins/installed-plugin-index.ts index 95611bcbd3b..f3b35c8725c 100644 --- a/src/plugins/installed-plugin-index.ts +++ b/src/plugins/installed-plugin-index.ts @@ -5,10 +5,7 @@ import type { OpenClawConfig } from "../config/types.js"; import type { PluginInstallRecord } from "../config/types.plugins.js"; import { resolveCompatibilityHostVersion } from "../version.js"; import { listPluginCompatRecords, type PluginCompatCode } from "./compat/registry.js"; -import { - normalizePluginsConfigWithResolver, - resolveEffectiveEnableState, -} from "./config-policy.js"; +import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js"; import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js"; import { describePluginInstallSource, @@ -353,7 +350,7 @@ function resolveCompatRegistryVersion(): string { } function resolvePolicyHash(config: OpenClawConfig | undefined): string { - const normalized = normalizePluginsConfigWithResolver(config?.plugins); + const normalized = normalizePluginsConfig(config?.plugins); const channelPolicy: Record = {}; const channels = config?.channels; if (channels && typeof channels === "object" && !Array.isArray(channels)) { @@ -404,7 +401,7 @@ function resolveRegistry(params: LoadInstalledPluginIndexParams): { }; } - const normalized = normalizePluginsConfigWithResolver(params.config?.plugins); + const normalized = normalizePluginsConfig(params.config?.plugins); const discovery = discoverOpenClawPlugins({ workspaceDir: params.workspaceDir, extraPaths: normalized.loadPaths, @@ -430,7 +427,7 @@ function buildInstalledPluginIndex( const env = params.env ?? process.env; const { candidates, registry } = resolveRegistry(params); const candidateByRootDir = buildCandidateLookup(candidates); - const normalizedConfig = normalizePluginsConfigWithResolver(params.config?.plugins); + const normalizedConfig = normalizePluginsConfig(params.config?.plugins); const diagnostics: PluginDiagnostic[] = [...registry.diagnostics]; const generatedAtMs = (params.now?.() ?? new Date()).getTime(); const plugins = registry.plugins.map((record): InstalledPluginIndexRecord => { @@ -528,7 +525,7 @@ export function listEnabledInstalledPluginRecords( if (!config) { return index.plugins.filter((plugin) => plugin.enabled); } - const normalizedConfig = normalizePluginsConfigWithResolver(config?.plugins); + const normalizedConfig = normalizePluginsConfig(config?.plugins); return index.plugins.filter( (plugin) => resolveEffectiveEnableState({ @@ -560,7 +557,7 @@ export function isInstalledPluginEnabled( if (!config) { return record.enabled; } - const normalizedConfig = normalizePluginsConfigWithResolver(config?.plugins); + const normalizedConfig = normalizePluginsConfig(config?.plugins); return resolveEffectiveEnableState({ id: record.pluginId, origin: record.origin, diff --git a/test/scripts/postinstall-bundled-plugins.test.ts b/test/scripts/postinstall-bundled-plugins.test.ts index 873b5edc2fa..ee0d08f9a7f 100644 --- a/test/scripts/postinstall-bundled-plugins.test.ts +++ b/test/scripts/postinstall-bundled-plugins.test.ts @@ -330,22 +330,23 @@ describe("bundled plugin postinstall", () => { }); it("honors plugin registry postinstall migration disable env", async () => { - const migratePluginRegistryForInstall = vi.fn(async () => ({ - status: "disabled", - migrated: false, - preflight: { - deprecationWarnings: [], - }, - })); + const importModule = vi.fn(async () => { + throw new Error("dist migration module should not import when migration is disabled"); + }); await expect( runPluginRegistryPostinstallMigration({ packageRoot: "/pkg", env: { OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION: "1" }, existsSync: vi.fn(() => true), - importModule: vi.fn(async () => ({ migratePluginRegistryForInstall })), + importModule, log: { log: vi.fn(), warn: vi.fn() }, }), - ).resolves.toMatchObject({ status: "disabled" }); + ).resolves.toMatchObject({ + status: "disabled", + migrated: false, + reason: "disabled-env", + }); + expect(importModule).not.toHaveBeenCalled(); }); it("prunes stale dist files from packaged installs", async () => {