fix(plugins): harden registry migration guards

This commit is contained in:
Vincent Koc
2026-04-25 04:25:16 -07:00
parent f22a2f7e8b
commit ad8296e685
4 changed files with 60 additions and 19 deletions

View File

@@ -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 ?? []) {

View File

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

View File

@@ -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<string, boolean> = {};
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,

View File

@@ -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 () => {