mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:31:06 +00:00
fix: preserve disabled plugin registry migration
This commit is contained in:
@@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Plugins/registry: preserve explicit disabled plugin records during registry migration without persisting every unused bundled plugin discovered on disk. Thanks @shakkernerd.
|
||||||
- Windows/native: keep CLI startup and bundled provider plugin loading off
|
- Windows/native: keep CLI startup and bundled provider plugin loading off
|
||||||
Windows ESM raw-path failure paths, fixing native onboarding/install smoke on
|
Windows ESM raw-path failure paths, fixing native onboarding/install smoke on
|
||||||
Node 24. Thanks @steipete.
|
Node 24. Thanks @steipete.
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ function hermeticEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCandidate(rootDir: string, id = "demo"): PluginCandidate {
|
function createCandidate(
|
||||||
|
rootDir: string,
|
||||||
|
id = "demo",
|
||||||
|
origin: PluginCandidate["origin"] = "global",
|
||||||
|
): PluginCandidate {
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(rootDir, "index.ts"),
|
path.join(rootDir, "index.ts"),
|
||||||
"throw new Error('runtime entry should not load while migrating plugin registry');\n",
|
"throw new Error('runtime entry should not load while migrating plugin registry');\n",
|
||||||
@@ -58,7 +62,7 @@ function createCandidate(rootDir: string, id = "demo"): PluginCandidate {
|
|||||||
idHint: id,
|
idHint: id,
|
||||||
source: path.join(rootDir, "index.ts"),
|
source: path.join(rootDir, "index.ts"),
|
||||||
rootDir,
|
rootDir,
|
||||||
origin: "global",
|
origin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,19 +131,22 @@ describe("plugin registry install migration", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("persists only plugins enabled by the central config policy", async () => {
|
it("persists migration-relevant plugin records without dropping explicit disabled state", async () => {
|
||||||
const stateDir = makeTempDir();
|
const stateDir = makeTempDir();
|
||||||
const enabledDir = path.join(stateDir, "plugins", "enabled-demo");
|
const enabledDir = path.join(stateDir, "plugins", "enabled-demo");
|
||||||
const disabledDir = path.join(stateDir, "plugins", "disabled-demo");
|
const disabledDir = path.join(stateDir, "plugins", "disabled-demo");
|
||||||
|
const unusedBundledDir = path.join(stateDir, "plugins", "unused-bundled");
|
||||||
fs.mkdirSync(enabledDir, { recursive: true });
|
fs.mkdirSync(enabledDir, { recursive: true });
|
||||||
fs.mkdirSync(disabledDir, { recursive: true });
|
fs.mkdirSync(disabledDir, { recursive: true });
|
||||||
|
fs.mkdirSync(unusedBundledDir, { recursive: true });
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
migratePluginRegistryForInstall({
|
migratePluginRegistryForInstall({
|
||||||
stateDir,
|
stateDir,
|
||||||
candidates: [
|
candidates: [
|
||||||
createCandidate(enabledDir, "enabled-demo"),
|
createCandidate(enabledDir, "enabled-demo"),
|
||||||
createCandidate(disabledDir, "disabled-demo"),
|
createCandidate(disabledDir, "disabled-demo", "bundled"),
|
||||||
|
createCandidate(unusedBundledDir, "unused-bundled", "bundled"),
|
||||||
],
|
],
|
||||||
readConfig: async () => ({
|
readConfig: async () => ({
|
||||||
plugins: {
|
plugins: {
|
||||||
@@ -155,15 +162,24 @@ describe("plugin registry install migration", () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
status: "migrated",
|
status: "migrated",
|
||||||
current: {
|
current: {
|
||||||
plugins: [expect.objectContaining({ pluginId: "enabled-demo" })],
|
plugins: [
|
||||||
|
expect.objectContaining({ pluginId: "enabled-demo", enabled: true }),
|
||||||
|
expect.objectContaining({ pluginId: "disabled-demo", enabled: false }),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
|
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
|
||||||
plugins: [expect.objectContaining({ pluginId: "enabled-demo" })],
|
plugins: [
|
||||||
|
expect.objectContaining({ pluginId: "enabled-demo", enabled: true }),
|
||||||
|
expect.objectContaining({ pluginId: "disabled-demo", enabled: false }),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
const persisted = await readPersistedInstalledPluginIndex({ stateDir });
|
const persisted = await readPersistedInstalledPluginIndex({ stateDir });
|
||||||
expect(persisted?.plugins.map((plugin) => plugin.pluginId)).toEqual(["enabled-demo"]);
|
expect(persisted?.plugins.map((plugin) => plugin.pluginId)).toEqual([
|
||||||
|
"enabled-demo",
|
||||||
|
"disabled-demo",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("supports dry-run preflight without reading config or writing the registry", async () => {
|
it("supports dry-run preflight without reading config or writing the registry", async () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import { normalizeProviderId } from "../../../agents/provider-id.js";
|
||||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||||
import {
|
import {
|
||||||
loadPluginInstallRecords,
|
loadPluginInstallRecords,
|
||||||
@@ -13,9 +14,9 @@ import {
|
|||||||
type InstalledPluginIndexStoreOptions,
|
type InstalledPluginIndexStoreOptions,
|
||||||
} from "../../../plugins/installed-plugin-index-store.js";
|
} from "../../../plugins/installed-plugin-index-store.js";
|
||||||
import {
|
import {
|
||||||
listEnabledInstalledPluginRecords,
|
|
||||||
loadInstalledPluginIndex,
|
loadInstalledPluginIndex,
|
||||||
type InstalledPluginIndex,
|
type InstalledPluginIndex,
|
||||||
|
type InstalledPluginIndexRecord,
|
||||||
type LoadInstalledPluginIndexParams,
|
type LoadInstalledPluginIndexParams,
|
||||||
} from "../../../plugins/installed-plugin-index.js";
|
} from "../../../plugins/installed-plugin-index.js";
|
||||||
|
|
||||||
@@ -111,6 +112,134 @@ async function readMigrationConfig(
|
|||||||
return await configModule.readBestEffortConfig();
|
return await configModule.readBestEffortConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRegistryReference(value: unknown): string | undefined {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed ? trimmed.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMigrationPluginIdNormalizer(
|
||||||
|
index: InstalledPluginIndex,
|
||||||
|
): (pluginId: string) => string {
|
||||||
|
const aliases = new Map<string, string>();
|
||||||
|
for (const plugin of index.plugins) {
|
||||||
|
const pluginId = normalizeRegistryReference(plugin.pluginId);
|
||||||
|
if (!pluginId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
aliases.set(pluginId, plugin.pluginId);
|
||||||
|
for (const alias of [
|
||||||
|
...plugin.contributions.providers,
|
||||||
|
...plugin.contributions.channels,
|
||||||
|
...plugin.contributions.setupProviders,
|
||||||
|
...plugin.contributions.cliBackends,
|
||||||
|
...plugin.contributions.modelCatalogProviders,
|
||||||
|
]) {
|
||||||
|
const normalizedAlias = normalizeRegistryReference(alias);
|
||||||
|
if (normalizedAlias && !aliases.has(normalizedAlias)) {
|
||||||
|
aliases.set(normalizedAlias, plugin.pluginId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (pluginId: string) => {
|
||||||
|
const normalized = normalizeRegistryReference(pluginId);
|
||||||
|
return normalized ? (aliases.get(normalized) ?? pluginId.trim()) : pluginId.trim();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPluginReference(
|
||||||
|
references: Set<string>,
|
||||||
|
normalizePluginId: (pluginId: string) => string,
|
||||||
|
value: unknown,
|
||||||
|
): void {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = normalizePluginId(value);
|
||||||
|
if (normalized) {
|
||||||
|
references.add(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listConfiguredChannelIds(config: OpenClawConfig): Set<string> {
|
||||||
|
const channels = config.channels;
|
||||||
|
if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
return new Set(
|
||||||
|
Object.keys(channels)
|
||||||
|
.map((channelId) => normalizeRegistryReference(channelId))
|
||||||
|
.filter((channelId): channelId is string => Boolean(channelId)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listConfiguredModelProviderIds(config: OpenClawConfig): Set<string> {
|
||||||
|
const providers = config.models?.providers;
|
||||||
|
if (!providers || typeof providers !== "object" || Array.isArray(providers)) {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
return new Set(
|
||||||
|
Object.keys(providers)
|
||||||
|
.map((providerId) => normalizeProviderId(providerId))
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listMigrationRelevantPluginRecords(params: {
|
||||||
|
index: InstalledPluginIndex;
|
||||||
|
config: OpenClawConfig;
|
||||||
|
installRecords: Record<string, unknown>;
|
||||||
|
}): readonly InstalledPluginIndexRecord[] {
|
||||||
|
const normalizePluginId = createMigrationPluginIdNormalizer(params.index);
|
||||||
|
const referencedPluginIds = new Set<string>();
|
||||||
|
const installedPluginIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const pluginId of Object.keys(params.installRecords)) {
|
||||||
|
addPluginReference(installedPluginIds, normalizePluginId, pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugins = params.config.plugins;
|
||||||
|
for (const pluginId of plugins?.allow ?? []) {
|
||||||
|
addPluginReference(referencedPluginIds, normalizePluginId, pluginId);
|
||||||
|
}
|
||||||
|
for (const pluginId of plugins?.deny ?? []) {
|
||||||
|
addPluginReference(referencedPluginIds, normalizePluginId, pluginId);
|
||||||
|
}
|
||||||
|
for (const pluginId of Object.keys(plugins?.entries ?? {})) {
|
||||||
|
addPluginReference(referencedPluginIds, normalizePluginId, pluginId);
|
||||||
|
}
|
||||||
|
for (const pluginId of Object.values(plugins?.slots ?? {})) {
|
||||||
|
if (normalizeRegistryReference(pluginId) === "none") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
addPluginReference(referencedPluginIds, normalizePluginId, pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuredChannelIds = listConfiguredChannelIds(params.config);
|
||||||
|
const configuredModelProviderIds = listConfiguredModelProviderIds(params.config);
|
||||||
|
|
||||||
|
return params.index.plugins.filter((plugin) => {
|
||||||
|
if (plugin.origin !== "bundled") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (installedPluginIds.has(plugin.pluginId) || referencedPluginIds.has(plugin.pluginId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
plugin.contributions.channels.some((channelId) =>
|
||||||
|
configuredChannelIds.has(normalizeRegistryReference(channelId) ?? ""),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return plugin.contributions.providers.some((providerId) =>
|
||||||
|
configuredModelProviderIds.has(normalizeProviderId(providerId)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function migratePluginRegistryForInstall(
|
export async function migratePluginRegistryForInstall(
|
||||||
params: PluginRegistryInstallMigrationParams = {},
|
params: PluginRegistryInstallMigrationParams = {},
|
||||||
): Promise<PluginRegistryInstallMigrationResult> {
|
): Promise<PluginRegistryInstallMigrationResult> {
|
||||||
@@ -139,7 +268,11 @@ export async function migratePluginRegistryForInstall(
|
|||||||
const current: InstalledPluginIndex = {
|
const current: InstalledPluginIndex = {
|
||||||
...candidateIndex,
|
...candidateIndex,
|
||||||
refreshReason: "migration",
|
refreshReason: "migration",
|
||||||
plugins: listEnabledInstalledPluginRecords(candidateIndex, config),
|
plugins: listMigrationRelevantPluginRecords({
|
||||||
|
index: candidateIndex,
|
||||||
|
config,
|
||||||
|
installRecords,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
if (Object.keys(installRecords).length > 0) {
|
if (Object.keys(installRecords).length > 0) {
|
||||||
await writePersistedPluginInstallLedger(installRecords, params);
|
await writePersistedPluginInstallLedger(installRecords, params);
|
||||||
|
|||||||
Reference in New Issue
Block a user