refactor(plugins): reuse lifecycle-owned metadata snapshot (#114807)

This commit is contained in:
Peter Steinberger
2026-07-27 20:29:14 -04:00
committed by GitHub
parent 6125edfb76
commit 8a2852bdfa
5 changed files with 115 additions and 28 deletions
@@ -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. */
+8 -3
View File
@@ -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,
+2
View File
@@ -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,
+62
View File
@@ -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();
+38 -10
View File
@@ -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", () => {