From ab28cfa9d41d4a351f4ca0bbc024a4ae7ab376a0 Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 27 Apr 2026 16:25:09 +0100 Subject: [PATCH] fix: guard plugin metadata snapshot reuse --- src/gateway/server-plugins.test.ts | 1 + .../server-startup-config.recovery.test.ts | 1 + src/gateway/server-startup-plugins.test.ts | 1 + src/plugins/plugin-lookup-table.test.ts | 86 ++++++++++++++++--- src/plugins/plugin-lookup-table.ts | 20 +++-- src/plugins/plugin-metadata-snapshot.ts | 16 ++++ 6 files changed, 106 insertions(+), 19 deletions(-) diff --git a/src/gateway/server-plugins.test.ts b/src/gateway/server-plugins.test.ts index 41e97863b1f..179f8b83730 100644 --- a/src/gateway/server-plugins.test.ts +++ b/src/gateway/server-plugins.test.ts @@ -115,6 +115,7 @@ function createLookUpTableForTest(params: { }): PluginLookUpTable { return { key: "test", + policyHash: "test", index: { version: 1, hostContractVersion: "test", diff --git a/src/gateway/server-startup-config.recovery.test.ts b/src/gateway/server-startup-config.recovery.test.ts index e6d2545a994..9cea3a282f9 100644 --- a/src/gateway/server-startup-config.recovery.test.ts +++ b/src/gateway/server-startup-config.recovery.test.ts @@ -13,6 +13,7 @@ const applyPluginAutoEnable = vi.hoisted(() => const pluginManifestRegistry = vi.hoisted(() => ({ plugins: [], diagnostics: [] })); const pluginMetadataSnapshot = vi.hoisted( (): PluginMetadataSnapshot => ({ + policyHash: "policy", index: { version: 1, hostContractVersion: "test", diff --git a/src/gateway/server-startup-plugins.test.ts b/src/gateway/server-startup-plugins.test.ts index 15dfeaec207..fcc569bff3e 100644 --- a/src/gateway/server-startup-plugins.test.ts +++ b/src/gateway/server-startup-plugins.test.ts @@ -25,6 +25,7 @@ const resolveBundledRuntimeDependencyPackageInstallRoot = vi.hoisted(() => const pluginManifestRegistry = vi.hoisted(() => ({ plugins: [], diagnostics: [] })); const pluginMetadataSnapshot = vi.hoisted( (): PluginMetadataSnapshot => ({ + policyHash: "policy", index: { version: 1, hostContractVersion: "test", diff --git a/src/plugins/plugin-lookup-table.test.ts b/src/plugins/plugin-lookup-table.test.ts index 13e063d77d1..bc630f8b645 100644 --- a/src/plugins/plugin-lookup-table.test.ts +++ b/src/plugins/plugin-lookup-table.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js"; import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js"; import type { PluginRegistrySnapshot } from "./plugin-registry.js"; @@ -47,13 +48,16 @@ function createManifestRecord( }; } -function createIndex(plugins: readonly PluginManifestRecord[]): PluginRegistrySnapshot { +function createIndex( + plugins: readonly PluginManifestRecord[], + params: { policyHash?: string } = {}, +): PluginRegistrySnapshot { return { version: 1, hostContractVersion: "test", compatRegistryVersion: "test", migrationVersion: 1, - policyHash: "policy", + policyHash: params.policyHash ?? "policy", generatedAtMs: 1, installRecords: {}, diagnostics: [], @@ -194,6 +198,15 @@ describe("loadPluginLookUpTable", () => { }), ]; const index = createIndex(plugins); + const config = { + channels: { + telegram: { token: "configured" }, + }, + } as OpenClawConfig; + const compatibleIndex = { + ...index, + policyHash: resolveInstalledPluginIndexPolicyHash(config), + }; const manifestRegistry: PluginManifestRegistry = { plugins, diagnostics: [], @@ -203,22 +216,14 @@ describe("loadPluginLookUpTable", () => { const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); const metadataSnapshot = loadPluginMetadataSnapshot({ - config: { - channels: { - telegram: { token: "configured" }, - }, - } as OpenClawConfig, + config, env: {}, - index, + index: compatibleIndex, }); loadPluginManifestRegistryForInstalledIndex.mockClear(); const table = loadPluginLookUpTable({ - config: { - channels: { - telegram: { token: "configured" }, - }, - } as OpenClawConfig, + config, env: {}, metadataSnapshot, }); @@ -229,4 +234,59 @@ describe("loadPluginLookUpTable", () => { expect(table.metrics.indexPluginCount).toBe(1); expect(table.metrics.manifestPluginCount).toBe(1); }); + + it("rebuilds when a provided metadata snapshot has a stale plugin policy", async () => { + const plugins = [ + createManifestRecord({ + id: "telegram", + origin: "bundled", + channels: ["telegram"], + }), + ]; + const snapshotConfig = { + plugins: { + allow: ["telegram"], + }, + } as OpenClawConfig; + const requestedConfig = { + plugins: { + allow: ["other-plugin"], + }, + } as OpenClawConfig; + const snapshotIndex = createIndex(plugins, { + policyHash: resolveInstalledPluginIndexPolicyHash(snapshotConfig), + }); + const requestedIndex = createIndex(plugins, { + policyHash: resolveInstalledPluginIndexPolicyHash(requestedConfig), + }); + const manifestRegistry: PluginManifestRegistry = { + plugins, + diagnostics: [], + }; + loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); + const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); + const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); + + const metadataSnapshot = loadPluginMetadataSnapshot({ + config: snapshotConfig, + env: {}, + index: snapshotIndex, + }); + loadPluginManifestRegistryForInstalledIndex.mockClear(); + + loadPluginLookUpTable({ + config: requestedConfig, + env: {}, + index: requestedIndex, + metadataSnapshot, + }); + + expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce(); + expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledWith( + expect.objectContaining({ + index: requestedIndex, + config: requestedConfig, + }), + ); + }); }); diff --git a/src/plugins/plugin-lookup-table.ts b/src/plugins/plugin-lookup-table.ts index aaa4d98a85b..2d2ee283023 100644 --- a/src/plugins/plugin-lookup-table.ts +++ b/src/plugins/plugin-lookup-table.ts @@ -6,6 +6,7 @@ import { } from "./channel-plugin-ids.js"; import { hashJson } from "./installed-plugin-index-hash.js"; import { + isPluginMetadataSnapshotCompatible, loadPluginMetadataSnapshot, type PluginMetadataSnapshot, type PluginMetadataSnapshotOwnerMaps, @@ -52,14 +53,21 @@ export type LoadPluginLookUpTableParams = { }; export function loadPluginLookUpTable(params: LoadPluginLookUpTableParams): PluginLookUpTable { + const requestedSnapshotConfig = params.activationSourceConfig ?? params.config; const metadataSnapshot = - params.metadataSnapshot ?? - loadPluginMetadataSnapshot({ - config: params.config, + params.metadataSnapshot && + isPluginMetadataSnapshotCompatible({ + snapshot: params.metadataSnapshot, + config: requestedSnapshotConfig, workspaceDir: params.workspaceDir, - env: params.env, - ...(params.index ? { index: params.index } : {}), - }); + }) + ? params.metadataSnapshot + : loadPluginMetadataSnapshot({ + config: requestedSnapshotConfig, + workspaceDir: params.workspaceDir, + env: params.env, + ...(params.index ? { index: params.index } : {}), + }); const { index, manifestRegistry } = metadataSnapshot; const startupPlanStartedAt = performance.now(); const channelPluginIds = resolveChannelPluginIdsFromRegistry({ manifestRegistry }); diff --git a/src/plugins/plugin-metadata-snapshot.ts b/src/plugins/plugin-metadata-snapshot.ts index a9ab81f6f8d..b05c217651a 100644 --- a/src/plugins/plugin-metadata-snapshot.ts +++ b/src/plugins/plugin-metadata-snapshot.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js"; import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js"; import type { PluginDiagnostic } from "./manifest-types.js"; @@ -30,6 +31,8 @@ export type PluginMetadataSnapshotMetrics = { }; export type PluginMetadataSnapshot = { + policyHash: string; + workspaceDir?: string; index: PluginRegistrySnapshot; registryDiagnostics: readonly PluginRegistrySnapshotDiagnostic[]; manifestRegistry: PluginManifestRegistry; @@ -48,6 +51,17 @@ export type LoadPluginMetadataSnapshotParams = { index?: PluginRegistrySnapshot; }; +export function isPluginMetadataSnapshotCompatible(params: { + snapshot: Pick; + config: OpenClawConfig; + workspaceDir?: string; +}): boolean { + return ( + params.snapshot.policyHash === resolveInstalledPluginIndexPolicyHash(params.config) && + (params.snapshot.workspaceDir ?? "") === (params.workspaceDir ?? "") + ); +} + function appendOwner(owners: Map, ownedId: string, pluginId: string): void { const existing = owners.get(ownedId); if (existing) { @@ -149,6 +163,8 @@ export function loadPluginMetadataSnapshot( const totalMs = performance.now() - totalStartedAt; return { + policyHash: index.policyHash, + ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), index, registryDiagnostics: registryResult.diagnostics, manifestRegistry,