perf(gateway): cache current plugin metadata fingerprints

This commit is contained in:
Peter Steinberger
2026-05-27 12:59:12 +01:00
parent e2cebe88ca
commit 7efbaf7dba
2 changed files with 178 additions and 12 deletions

View File

@@ -177,6 +177,34 @@ describe("current plugin metadata snapshot", () => {
expect(getCurrentPluginMetadataSnapshot({ config, env: requestedEnv })).toBeUndefined();
});
it("rejects an exact cached config object after in-place policy changes", () => {
const config = { plugins: { allow: ["demo"] } };
const snapshot = createSnapshot({ config });
setCurrentPluginMetadataSnapshot(snapshot, { config });
expect(getCurrentPluginMetadataSnapshot({ config })).toBe(snapshot);
config.plugins.allow = ["other"];
expect(getCurrentPluginMetadataSnapshot({ config })).toBeUndefined();
});
it("rejects an exact cached env object after in-place root changes", () => {
const config = {};
const snapshot = createSnapshot({ config });
const env = {
HOME: "/home/snapshot",
OPENCLAW_HOME: undefined,
} as NodeJS.ProcessEnv;
setCurrentPluginMetadataSnapshot(snapshot, { config, env });
expect(getCurrentPluginMetadataSnapshot({ config, env })).toBe(snapshot);
env.HOME = "/home/requested";
expect(getCurrentPluginMetadataSnapshot({ config, env })).toBeUndefined();
});
it("keeps source-policy compatibility when storing an auto-enabled runtime config", () => {
const sourceConfig = { channels: { telegram: { botToken: "token" } } };
const autoEnabledConfig = {

View File

@@ -10,8 +10,20 @@ import {
type ResolvePluginControlPlaneContextParams,
} from "./plugin-control-plane-context.js";
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
import { resolvePluginCacheInputs } from "./roots.js";
type CurrentPluginMetadataSnapshotState = ReturnType<typeof getCurrentPluginMetadataSnapshotState>;
type CurrentPluginMetadataConfigCacheEntry = {
configFingerprint: string;
discoveryKey: string;
policyHash: string;
policyKey: string;
};
let currentPluginMetadataConfigIdentityCache = new WeakMap<
OpenClawConfig,
CurrentPluginMetadataConfigCacheEntry
>();
export function resolvePluginMetadataControlPlaneFingerprint(
config?: OpenClawConfig,
@@ -29,6 +41,65 @@ export function isReusableCurrentPluginMetadataSnapshot(
return true;
}
function resolveConfiguredPluginLoadPaths(
config: OpenClawConfig | undefined,
): readonly string[] | undefined {
const paths = config?.plugins?.load?.paths;
return Array.isArray(paths) ? paths : undefined;
}
function resolveCurrentPluginMetadataDiscoveryKey(params: {
config?: OpenClawConfig;
env: NodeJS.ProcessEnv;
workspaceDir?: string;
}): string {
return JSON.stringify(
resolvePluginCacheInputs({
env: params.env,
workspaceDir: params.workspaceDir,
loadPaths: [...(resolveConfiguredPluginLoadPaths(params.config) ?? [])],
}),
);
}
function resolveCurrentPluginMetadataPolicyKey(config: OpenClawConfig | undefined): string {
const plugins = config?.plugins;
const rawEntries = plugins?.entries;
const entries: Record<string, unknown> = {};
if (rawEntries && typeof rawEntries === "object" && !Array.isArray(rawEntries)) {
for (const [pluginId, entry] of Object.entries(rawEntries)) {
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
const enabled = (entry as Record<string, unknown>).enabled;
if (enabled !== undefined) {
entries[pluginId] = enabled;
}
}
}
}
const channelPolicy: Record<string, boolean> = {};
const channels = config?.channels;
if (channels && typeof channels === "object" && !Array.isArray(channels)) {
for (const [channelId, value] of Object.entries(channels)) {
if (value && typeof value === "object" && !Array.isArray(value)) {
const enabled = (value as Record<string, unknown>).enabled;
if (typeof enabled === "boolean") {
channelPolicy[channelId] = enabled;
}
}
}
}
return JSON.stringify({
plugins: {
enabled: plugins?.enabled,
allow: plugins?.allow,
deny: plugins?.deny,
slots: plugins?.slots,
entries,
},
channels: channelPolicy,
});
}
// Single-slot Gateway-owned handoff. Replace or clear it at lifecycle boundaries;
// never accumulate historical metadata snapshots here.
export function setCurrentPluginMetadataSnapshot(
@@ -40,6 +111,7 @@ export function setCurrentPluginMetadataSnapshot(
workspaceDir?: string;
} = {},
): void {
currentPluginMetadataConfigIdentityCache = new WeakMap();
const compatiblePolicyHashes = snapshot
? options.compatibleConfigs?.map((config) => resolveInstalledPluginIndexPolicyHash(config))
: undefined;
@@ -66,9 +138,52 @@ export function setCurrentPluginMetadataSnapshot(
compatiblePolicyHashes,
compatibleConfigFingerprints,
);
if (!snapshot) {
return;
}
const env = options.env ?? process.env;
const workspaceDir = options.workspaceDir ?? snapshot.workspaceDir;
if (options.config) {
const policyHash = resolveInstalledPluginIndexPolicyHash(options.config);
const policyKey = resolveCurrentPluginMetadataPolicyKey(options.config);
const discoveryKey = resolveCurrentPluginMetadataDiscoveryKey({
config: options.config,
env,
workspaceDir,
});
currentPluginMetadataConfigIdentityCache.set(options.config, {
policyHash,
policyKey,
discoveryKey,
configFingerprint: resolvePluginMetadataControlPlaneFingerprint(options.config, {
env,
index: snapshot.index,
policyHash,
workspaceDir,
}),
});
}
for (const [index, config] of (options.compatibleConfigs ?? []).entries()) {
const policyHash = compatiblePolicyHashes?.[index];
const configFingerprint = compatibleConfigFingerprints?.[index];
if (!policyHash || !configFingerprint) {
continue;
}
currentPluginMetadataConfigIdentityCache.set(config, {
policyHash,
policyKey: resolveCurrentPluginMetadataPolicyKey(config),
configFingerprint,
discoveryKey: resolveCurrentPluginMetadataDiscoveryKey({
config,
env,
workspaceDir,
}),
});
}
}
export function clearCurrentPluginMetadataSnapshot(): void {
currentPluginMetadataConfigIdentityCache = new WeakMap();
clearCurrentPluginMetadataSnapshotState();
}
@@ -79,6 +194,7 @@ export function captureCurrentPluginMetadataSnapshotState(): CurrentPluginMetada
export function restoreCurrentPluginMetadataSnapshotState(
state: CurrentPluginMetadataSnapshotState,
): void {
currentPluginMetadataConfigIdentityCache = new WeakMap();
setCurrentPluginMetadataSnapshotState(
state.snapshot,
state.configFingerprint,
@@ -106,25 +222,47 @@ export function getCurrentPluginMetadataSnapshot(
if (!snapshot) {
return undefined;
}
const requestedPolicyHash = params.config
? resolveInstalledPluginIndexPolicyHash(params.config)
: undefined;
const env = params.env ?? process.env;
const requestedWorkspaceDir =
params.workspaceDir ??
(params.allowWorkspaceScopedSnapshot === true ? snapshot.workspaceDir : undefined);
const cachedConfig = params.config && currentPluginMetadataConfigIdentityCache.get(params.config);
const requestedPolicyKey =
params.config && cachedConfig
? resolveCurrentPluginMetadataPolicyKey(params.config)
: undefined;
const requestedDiscoveryKey =
params.config && cachedConfig
? resolveCurrentPluginMetadataDiscoveryKey({
config: params.config,
env,
workspaceDir: requestedWorkspaceDir,
})
: undefined;
const canReuseCachedConfig =
cachedConfig !== undefined &&
cachedConfig.policyKey === requestedPolicyKey &&
cachedConfig.discoveryKey === requestedDiscoveryKey;
const requestedPolicyHash = canReuseCachedConfig
? cachedConfig.policyHash
: params.config
? resolveInstalledPluginIndexPolicyHash(params.config)
: undefined;
if (requestedPolicyHash && snapshot.policyHash !== requestedPolicyHash) {
const compatiblePolicies = new Set(compatiblePolicyHashes ?? []);
if (!compatiblePolicies.has(requestedPolicyHash)) {
return undefined;
}
}
const requestedWorkspaceDir =
params.workspaceDir ??
(params.allowWorkspaceScopedSnapshot === true ? snapshot.workspaceDir : undefined);
if (params.config) {
const requestedConfigFingerprint = resolvePluginMetadataControlPlaneFingerprint(params.config, {
env: params.env,
index: snapshot.index,
policyHash: requestedPolicyHash,
workspaceDir: requestedWorkspaceDir,
});
const requestedConfigFingerprint = canReuseCachedConfig
? cachedConfig.configFingerprint
: resolvePluginMetadataControlPlaneFingerprint(params.config, {
env,
index: snapshot.index,
policyHash: requestedPolicyHash,
workspaceDir: requestedWorkspaceDir,
});
const compatibleFingerprints = new Set(compatibleConfigFingerprints ?? []);
const fingerprintMatches =
configFingerprint === requestedConfigFingerprint ||