From 8a2852bdfa2e99622992cc4ae8c266495377a56f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Jul 2026 20:29:14 -0400 Subject: [PATCH] refactor(plugins): reuse lifecycle-owned metadata snapshot (#114807) --- .../facade-activation-check.runtime.ts | 20 ++---- src/plugins/loader-discovery.ts | 11 +++- src/plugins/loader-load-context.ts | 2 + src/plugins/loader.base.test-utils.ts | 62 +++++++++++++++++++ src/plugins/loader.runtime-registry.test.ts | 48 +++++++++++--- 5 files changed, 115 insertions(+), 28 deletions(-) diff --git a/src/plugin-sdk/facade-activation-check.runtime.ts b/src/plugin-sdk/facade-activation-check.runtime.ts index bcc77589a9e3..6e27c6195dea 100644 --- a/src/plugin-sdk/facade-activation-check.runtime.ts +++ b/src/plugin-sdk/facade-activation-check.runtime.ts @@ -17,12 +17,9 @@ import { normalizePluginsConfig, resolveEffectivePluginActivationState, } from "../plugins/config-state.js"; -import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; import { isPluginEnabledByDefaultForPlatform } from "../plugins/default-enablement.js"; -import { - loadPluginManifestRegistry, - type PluginManifestRecord, -} from "../plugins/manifest-registry.js"; +import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; +import { resolvePluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js"; import { ALWAYS_ALLOWED_RUNTIME_DIR_NAMES } from "./facade-activation-contract.js"; import { resolveRegistryPluginModuleLocationFromRecords } from "./facade-resolution-shared.js"; @@ -96,18 +93,11 @@ function getFacadeManifestRegistry(params: { }): readonly PluginManifestRecord[] { const envOption = params.env ? { env: params.env } : {}; const resolved = getFacadeBoundaryResolvedConfig(); - const current = getCurrentPluginMetadataSnapshot({ + return resolvePluginMetadataSnapshot({ config: resolved.config, ...envOption, - allowWorkspaceScopedSnapshot: true, - }); - if (current?.manifestRegistry) { - return current.manifestRegistry.plugins; - } - return loadPluginManifestRegistry({ - config: resolved.config, - ...envOption, - }).plugins; + allowWorkspaceScopedCurrent: true, + }).manifestRegistry.plugins; } /** Resolves the concrete plugin module location recorded in the manifest registry. */ diff --git a/src/plugins/loader-discovery.ts b/src/plugins/loader-discovery.ts index bb92c62d2218..bf7cb39896d0 100644 --- a/src/plugins/loader-discovery.ts +++ b/src/plugins/loader-discovery.ts @@ -40,9 +40,14 @@ export function resolvePluginLoadDiscovery(params: { suppliedManifestRegistry?: PluginManifestRegistry; }): ResolvedPluginLoadDiscovery { const { options, context } = params; - const discovery = params.suppliedManifestRegistry + // The load context has already verified workspace, environment, config, and + // plugin scope against the current lifecycle-owned metadata generation. + const suppliedManifestRegistry = + params.suppliedManifestRegistry ?? + (options.discovery === undefined ? context.metadataSnapshot?.manifestRegistry : undefined); + const discovery = suppliedManifestRegistry ? { - candidates: createPluginCandidatesFromManifestRegistry(params.suppliedManifestRegistry), + candidates: createPluginCandidatesFromManifestRegistry(suppliedManifestRegistry), diagnostics: [] as PluginDiagnostic[], } : (options.discovery ?? @@ -53,7 +58,7 @@ export function resolvePluginLoadDiscovery(params: { installRecords: context.installRecords, })); const manifestRegistry = - params.suppliedManifestRegistry ?? + suppliedManifestRegistry ?? loadPluginManifestRegistry({ config: context.cfg, workspaceDir: options.workspaceDir, diff --git a/src/plugins/loader-load-context.ts b/src/plugins/loader-load-context.ts index 3e7d5f03288d..2faed4b84c87 100644 --- a/src/plugins/loader-load-context.ts +++ b/src/plugins/loader-load-context.ts @@ -352,6 +352,7 @@ export function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { // scoped snapshots must match exactly to protect activation boundaries. const currentMetadataSnapshot = options.installRecords === undefined && + trustNormalized.loadPaths === normalized.loadPaths && !shouldResolveRawConfigEnvVars && (options.env === undefined || options.env === process.env) ? (getCurrentPluginMetadataSnapshot({ @@ -411,6 +412,7 @@ export function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { return { env, cfg, + metadataSnapshot: currentMetadataSnapshot, normalized: trustNormalized, activationSourceConfig, activationSource, diff --git a/src/plugins/loader.base.test-utils.ts b/src/plugins/loader.base.test-utils.ts index 785215d955da..24e926cb28a2 100644 --- a/src/plugins/loader.base.test-utils.ts +++ b/src/plugins/loader.base.test-utils.ts @@ -16,6 +16,7 @@ import { import { withEnv } from "../test-utils/env.js"; import { clearPluginCommands } from "./command-registry-state.js"; import { getPluginCommandSpecs } from "./command-specs.js"; +import { setCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; import { getGlobalHookRunner, resetGlobalHookRunner } from "./hook-runner-global.js"; import { writePersistedInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-records.js"; import { @@ -51,6 +52,7 @@ import { globalAfterAll1, } from "./loader.test-harness.js"; import { loadPluginManifestRegistry } from "./manifest-registry.js"; +import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; import { createEmptyPluginRegistry } from "./registry.js"; import { getActivePluginRegistry, @@ -441,6 +443,66 @@ describe("loadOpenClawPlugins", () => { expect(registry.plugins.find((entry) => entry.id === plugin.id)?.status).toBe("loaded"); }); + it("loads scoped plugins from the current metadata snapshot without rediscovering manifests", () => { + useNoBundledPlugins(); + const plugin = writePlugin({ + id: "snapshot-manifest", + body: `module.exports = { id: "snapshot-manifest", register() {} };`, + }); + const config = { + plugins: { + load: { paths: [plugin.file] }, + allow: [plugin.id], + }, + }; + const metadataSnapshot = loadPluginMetadataSnapshot({ config, env: process.env }); + setCurrentPluginMetadataSnapshot(metadataSnapshot, { config, env: process.env }); + fs.rmSync(path.join(plugin.dir, "openclaw.plugin.json")); + + const registry = loadOpenClawPlugins({ + cache: false, + config, + onlyPluginIds: [plugin.id], + }); + + expect(registry.plugins.find((entry) => entry.id === plugin.id)?.status).toBe("loaded"); + }); + + it("discovers plugin paths supplied only by the activation source", () => { + useNoBundledPlugins(); + const snapshotPlugin = writePlugin({ + id: "snapshot-base", + body: `module.exports = { id: "snapshot-base", register() {} };`, + }); + const sourcePlugin = writePlugin({ + id: "activation-source-only", + body: `module.exports = { id: "activation-source-only", register() {} };`, + }); + const config = { + plugins: { + load: { paths: [snapshotPlugin.file] }, + allow: [snapshotPlugin.id, sourcePlugin.id], + }, + }; + const metadataSnapshot = loadPluginMetadataSnapshot({ config, env: process.env }); + setCurrentPluginMetadataSnapshot(metadataSnapshot, { config, env: process.env }); + + const registry = loadOpenClawPlugins({ + activate: false, + cache: false, + config, + activationSourceConfig: { + plugins: { + load: { paths: [sourcePlugin.file] }, + allow: [sourcePlugin.id], + }, + }, + onlyPluginIds: [sourcePlugin.id], + }); + + expect(registry.plugins.find((entry) => entry.id === sourcePlugin.id)?.status).toBe("loaded"); + }); + it("loads installed plugin packages discovered from persisted install records", () => { useNoBundledPlugins(); const stateDir = makeTempDir(); diff --git a/src/plugins/loader.runtime-registry.test.ts b/src/plugins/loader.runtime-registry.test.ts index 621d25903651..ffd40c2492a7 100644 --- a/src/plugins/loader.runtime-registry.test.ts +++ b/src/plugins/loader.runtime-registry.test.ts @@ -71,14 +71,17 @@ function setLoaderMetadataSnapshot(params: { pluginIds?: readonly string[] } = { describe("resolvePluginLoadCacheContext", () => { it("reuses prepared install records from the compatible metadata generation", () => { - const { config, env, installRecords, workspaceDir } = setLoaderMetadataSnapshot(); + const { config, env, installRecords, snapshot, workspaceDir } = setLoaderMetadataSnapshot(); - expect(resolvePluginLoadCacheContext({ config, env, workspaceDir }).installRecords).toEqual( - installRecords, - ); - expect(resolvePluginLoadCacheContext({ config, workspaceDir }).installRecords).toEqual( - installRecords, - ); + for (const options of [ + { config, env, workspaceDir }, + { config, workspaceDir }, + ]) { + const context = resolvePluginLoadCacheContext(options); + + expect(context.installRecords).toEqual(installRecords); + expect(context.metadataSnapshot).toBe(snapshot); + } }); it("loads a custom profile's install records instead of reusing the process snapshot", () => { @@ -101,9 +104,34 @@ describe("resolvePluginLoadCacheContext", () => { expect(resolvePluginLoadCacheContext({ config, env, workspaceDir }).installRecords).toEqual( installRecords, ); - expect( - resolvePluginLoadCacheContext({ config, env: profileEnv, workspaceDir }).installRecords, - ).toEqual(profileInstallRecords); + const profileContext = resolvePluginLoadCacheContext({ + config, + env: profileEnv, + workspaceDir, + }); + + expect(profileContext.installRecords).toEqual(profileInstallRecords); + expect(profileContext.metadataSnapshot).toBeUndefined(); + }); + + it("does not reuse metadata when the activation source adds plugin load paths", () => { + const { config, env, workspaceDir } = setLoaderMetadataSnapshot(); + const activationSourceConfig: OpenClawConfig = { + plugins: { + ...config.plugins, + load: { paths: ["/plugins/activation-source-only"] }, + }, + }; + + const context = resolvePluginLoadCacheContext({ + activationSourceConfig, + config, + env, + workspaceDir, + }); + + expect(context.normalized.loadPaths).toContain("/plugins/activation-source-only"); + expect(context.metadataSnapshot).toBeUndefined(); }); it("reuses an exact matching scoped metadata generation", () => {