From 58b4407cda296e530acaa6b913f0e8e2ec852261 Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 27 Apr 2026 16:43:31 +0100 Subject: [PATCH] fix: reject stale plugin metadata inventory --- src/plugins/plugin-lookup-table.test.ts | 66 +++++++++++++++++++++++++ src/plugins/plugin-lookup-table.ts | 1 + src/plugins/plugin-metadata-snapshot.ts | 38 +++++++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/plugins/plugin-lookup-table.test.ts b/src/plugins/plugin-lookup-table.test.ts index bc630f8b645..150794fa24b 100644 --- a/src/plugins/plugin-lookup-table.test.ts +++ b/src/plugins/plugin-lookup-table.test.ts @@ -289,4 +289,70 @@ describe("loadPluginLookUpTable", () => { }), ); }); + + it("rebuilds when a provided metadata snapshot has stale plugin inventory", async () => { + const snapshotPlugins = [ + createManifestRecord({ + id: "telegram", + origin: "bundled", + channels: ["telegram"], + }), + ]; + const requestedPlugins = [ + createManifestRecord({ + id: "telegram", + origin: "bundled", + channels: ["telegram"], + }), + createManifestRecord({ + id: "discord", + origin: "bundled", + channels: ["discord"], + }), + ]; + const config = { + channels: { + telegram: { token: "configured" }, + }, + } as OpenClawConfig; + const policyHash = resolveInstalledPluginIndexPolicyHash(config); + const snapshotIndex = createIndex(snapshotPlugins, { policyHash }); + const requestedIndex = createIndex(requestedPlugins, { policyHash }); + const snapshotRegistry: PluginManifestRegistry = { + plugins: snapshotPlugins, + diagnostics: [], + }; + const requestedRegistry: PluginManifestRegistry = { + plugins: requestedPlugins, + diagnostics: [], + }; + loadPluginManifestRegistryForInstalledIndex + .mockReturnValueOnce(snapshotRegistry) + .mockReturnValueOnce(requestedRegistry); + const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); + const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); + + const metadataSnapshot = loadPluginMetadataSnapshot({ + config, + env: {}, + index: snapshotIndex, + }); + loadPluginManifestRegistryForInstalledIndex.mockClear(); + + const table = loadPluginLookUpTable({ + config, + env: {}, + index: requestedIndex, + metadataSnapshot, + }); + + expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce(); + expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledWith( + expect.objectContaining({ + index: requestedIndex, + config, + }), + ); + expect(table.manifestRegistry).toBe(requestedRegistry); + }); }); diff --git a/src/plugins/plugin-lookup-table.ts b/src/plugins/plugin-lookup-table.ts index 2d2ee283023..916885c9da7 100644 --- a/src/plugins/plugin-lookup-table.ts +++ b/src/plugins/plugin-lookup-table.ts @@ -60,6 +60,7 @@ export function loadPluginLookUpTable(params: LoadPluginLookUpTableParams): Plug snapshot: params.metadataSnapshot, config: requestedSnapshotConfig, workspaceDir: params.workspaceDir, + index: params.index, }) ? params.metadataSnapshot : loadPluginMetadataSnapshot({ diff --git a/src/plugins/plugin-metadata-snapshot.ts b/src/plugins/plugin-metadata-snapshot.ts index b05c217651a..a4ce694a053 100644 --- a/src/plugins/plugin-metadata-snapshot.ts +++ b/src/plugins/plugin-metadata-snapshot.ts @@ -51,14 +51,48 @@ export type LoadPluginMetadataSnapshotParams = { index?: PluginRegistrySnapshot; }; +function indexesMatch( + left: PluginRegistrySnapshot | undefined, + right: PluginRegistrySnapshot | undefined, +): boolean { + if (!left || !right) { + return true; + } + if ( + left.version !== right.version || + left.hostContractVersion !== right.hostContractVersion || + left.compatRegistryVersion !== right.compatRegistryVersion || + left.migrationVersion !== right.migrationVersion || + left.policyHash !== right.policyHash || + left.plugins.length !== right.plugins.length + ) { + return false; + } + for (let index = 0; index < left.plugins.length; index += 1) { + const leftPlugin = left.plugins[index]; + const rightPlugin = right.plugins[index]; + if ( + !rightPlugin || + leftPlugin.pluginId !== rightPlugin.pluginId || + leftPlugin.manifestHash !== rightPlugin.manifestHash || + leftPlugin.installRecordHash !== rightPlugin.installRecordHash + ) { + return false; + } + } + return true; +} + export function isPluginMetadataSnapshotCompatible(params: { - snapshot: Pick; + snapshot: Pick; config: OpenClawConfig; workspaceDir?: string; + index?: PluginRegistrySnapshot; }): boolean { return ( params.snapshot.policyHash === resolveInstalledPluginIndexPolicyHash(params.config) && - (params.snapshot.workspaceDir ?? "") === (params.workspaceDir ?? "") + (params.snapshot.workspaceDir ?? "") === (params.workspaceDir ?? "") && + indexesMatch(params.snapshot.index, params.index) ); }