fix: reuse turn plugin metadata snapshot

This commit is contained in:
Shakker
2026-05-06 16:25:12 +01:00
parent c795a1a8ef
commit 3dffef651b
7 changed files with 175 additions and 27 deletions

View File

@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
- Voice Call/realtime: bound the paced Twilio audio queue and close overloaded realtime streams before provider audio can pile up behind the websocket backpressure guard. Thanks @vincentkoc.
- Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi.
- Gateway/performance: defer non-readiness sidecars until after the ready signal, avoid hot-path channel plugin barrel imports, and fast-path trusted bundled plugin metadata during Gateway startup.
- Gateway/performance: reuse the compatible plugin metadata snapshot across dashboard and channel agent turns so auto-enabled runtime config does not repeatedly rescan plugin metadata before provider calls. Thanks @shakkernerd.
- Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed.
- Gateway/diagnostics: add startup phase spans, active work labels, stale terminal bridge markers, and default sync-I/O tracing in `pnpm gateway:watch` so slow Gateway turns are easier to attribute from logs and stability diagnostics.
- Plugins/loader: preserve real compiled plugin module evaluation errors on the native fast path instead of treating every thrown `.js` module as a source-transform fallback miss. Thanks @vincentkoc.

View File

@@ -655,7 +655,12 @@ export async function startGatewayServer(
baseMethods,
runtimePluginsLoaded,
} = pluginBootstrap;
setCurrentPluginMetadataSnapshot(pluginLookUpTable, { config: gatewayPluginConfigAtStart });
setCurrentPluginMetadataSnapshot(pluginLookUpTable, {
config: startupActivationSourceConfig,
compatibleConfigs: [gatewayPluginConfigAtStart],
env: process.env,
workspaceDir: defaultWorkspaceDir,
});
if (pluginLookUpTable) {
const metrics = pluginLookUpTable.metrics;
startupTrace.detail("plugins.lookup-table", [
@@ -1178,7 +1183,11 @@ export async function startGatewayServer(
}
}
await params.beforeReplace(channelsToStopBeforeReplace);
setCurrentPluginMetadataSnapshot(nextPluginLookUpTable, { config: params.nextConfig });
setCurrentPluginMetadataSnapshot(nextPluginLookUpTable, {
config: params.nextConfig,
env: process.env,
workspaceDir: defaultWorkspaceDir,
});
const loaded = prepareGatewayPluginLoad({
cfg: params.nextConfig,
workspaceDir: defaultWorkspaceDir,

View File

@@ -160,6 +160,27 @@ describe("current plugin metadata snapshot", () => {
expect(getCurrentPluginMetadataSnapshot({ config: autoEnabledConfig })).toBeUndefined();
});
it("accepts explicit compatible configs for gateway runtime reuse", () => {
const sourceConfig = { channels: { telegram: { botToken: "token" } } };
const runtimeConfig = {
...sourceConfig,
plugins: { allow: ["telegram"] },
};
const snapshot = createSnapshot({ config: sourceConfig, workspaceDir: "/workspace" });
setCurrentPluginMetadataSnapshot(snapshot, {
config: sourceConfig,
compatibleConfigs: [runtimeConfig],
workspaceDir: "/workspace",
});
expect(
getCurrentPluginMetadataSnapshot({ config: sourceConfig, workspaceDir: "/workspace" }),
).toBe(snapshot);
expect(
getCurrentPluginMetadataSnapshot({ config: runtimeConfig, workspaceDir: "/workspace" }),
).toBe(snapshot);
});
it("clears the current snapshot", () => {
setCurrentPluginMetadataSnapshot(createSnapshot());
clearCurrentPluginMetadataSnapshot();

View File

@@ -25,8 +25,26 @@ export function resolvePluginMetadataControlPlaneFingerprint(
// never accumulate historical metadata snapshots here.
export function setCurrentPluginMetadataSnapshot(
snapshot: PluginMetadataSnapshot | undefined,
options: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv; workspaceDir?: string } = {},
options: {
config?: OpenClawConfig;
compatibleConfigs?: readonly OpenClawConfig[];
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
} = {},
): void {
const compatiblePolicyHashes = snapshot
? options.compatibleConfigs?.map((config) => resolveInstalledPluginIndexPolicyHash(config))
: undefined;
const compatibleConfigFingerprints = snapshot
? options.compatibleConfigs?.map((config, index) =>
resolvePluginMetadataControlPlaneFingerprint(config, {
env: options.env,
index: snapshot.index,
policyHash: compatiblePolicyHashes?.[index],
workspaceDir: options.workspaceDir ?? snapshot.workspaceDir,
}),
)
: undefined;
setCurrentPluginMetadataSnapshotState(
snapshot,
snapshot
@@ -37,6 +55,8 @@ export function setCurrentPluginMetadataSnapshot(
workspaceDir: options.workspaceDir ?? snapshot.workspaceDir,
})
: undefined,
compatiblePolicyHashes,
compatibleConfigFingerprints,
);
}
@@ -52,16 +72,24 @@ export function getCurrentPluginMetadataSnapshot(
allowWorkspaceScopedSnapshot?: boolean;
} = {},
): PluginMetadataSnapshot | undefined {
const { snapshot: rawSnapshot, configFingerprint } = getCurrentPluginMetadataSnapshotState();
const {
snapshot: rawSnapshot,
configFingerprint,
compatiblePolicyHashes,
compatibleConfigFingerprints,
} = getCurrentPluginMetadataSnapshotState();
const snapshot = rawSnapshot as PluginMetadataSnapshot | undefined;
if (!snapshot) {
return undefined;
}
if (
params.config &&
snapshot.policyHash !== resolveInstalledPluginIndexPolicyHash(params.config)
) {
return undefined;
const requestedPolicyHash = 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 ??
@@ -70,13 +98,15 @@ export function getCurrentPluginMetadataSnapshot(
const requestedConfigFingerprint = resolvePluginMetadataControlPlaneFingerprint(params.config, {
env: params.env,
index: snapshot.index,
policyHash: snapshot.policyHash,
policyHash: requestedPolicyHash,
workspaceDir: requestedWorkspaceDir,
});
if (configFingerprint && configFingerprint !== requestedConfigFingerprint) {
return undefined;
}
if (snapshot.configFingerprint && snapshot.configFingerprint !== requestedConfigFingerprint) {
const compatibleFingerprints = new Set(compatibleConfigFingerprints ?? []);
const fingerprintMatches =
configFingerprint === requestedConfigFingerprint ||
snapshot.configFingerprint === requestedConfigFingerprint ||
compatibleFingerprints.has(requestedConfigFingerprint);
if (!fingerprintMatches) {
return undefined;
}
}

View File

@@ -1,25 +1,41 @@
let currentPluginMetadataSnapshot: unknown;
let currentPluginMetadataSnapshotConfigFingerprint: string | undefined;
let currentPluginMetadataSnapshotCompatiblePolicyHashes: readonly string[] | undefined;
let currentPluginMetadataSnapshotCompatibleConfigFingerprints: readonly string[] | undefined;
export function setCurrentPluginMetadataSnapshotState(
snapshot: unknown,
configFingerprint: string | undefined,
compatiblePolicyHashes?: readonly string[],
compatibleConfigFingerprints?: readonly string[],
): void {
currentPluginMetadataSnapshot = snapshot;
currentPluginMetadataSnapshotConfigFingerprint = snapshot ? configFingerprint : undefined;
currentPluginMetadataSnapshotCompatiblePolicyHashes = snapshot
? compatiblePolicyHashes
: undefined;
currentPluginMetadataSnapshotCompatibleConfigFingerprints = snapshot
? compatibleConfigFingerprints
: undefined;
}
export function clearCurrentPluginMetadataSnapshotState(): void {
currentPluginMetadataSnapshot = undefined;
currentPluginMetadataSnapshotConfigFingerprint = undefined;
currentPluginMetadataSnapshotCompatiblePolicyHashes = undefined;
currentPluginMetadataSnapshotCompatibleConfigFingerprints = undefined;
}
export function getCurrentPluginMetadataSnapshotState(): {
snapshot: unknown;
configFingerprint: string | undefined;
compatiblePolicyHashes: readonly string[] | undefined;
compatibleConfigFingerprints: readonly string[] | undefined;
} {
return {
snapshot: currentPluginMetadataSnapshot,
configFingerprint: currentPluginMetadataSnapshotConfigFingerprint,
compatiblePolicyHashes: currentPluginMetadataSnapshotCompatiblePolicyHashes,
compatibleConfigFingerprints: currentPluginMetadataSnapshotCompatibleConfigFingerprints,
};
}

View File

@@ -41,6 +41,7 @@ const pluginRegistryMocks = vi.hoisted(() => {
diagnostics: [],
}));
return {
getCurrentPluginMetadataSnapshot: vi.fn(),
loadPluginManifestRegistryForInstalledIndex: loadManifestRegistry,
loadPluginManifestRegistryForPluginRegistry: loadManifestRegistry,
loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })),
@@ -61,6 +62,10 @@ const pluginRegistryMocks = vi.hoisted(() => {
};
});
vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({
getCurrentPluginMetadataSnapshot: pluginRegistryMocks.getCurrentPluginMetadataSnapshot,
}));
vi.mock("../plugins/manifest-registry-installed.js", () => ({
loadPluginManifestRegistryForInstalledIndex:
pluginRegistryMocks.loadPluginManifestRegistryForInstalledIndex,
@@ -85,6 +90,8 @@ describe("provider env vars dynamic manifest metadata", () => {
});
pluginRegistryMocks.loadPluginRegistrySnapshot.mockReset();
pluginRegistryMocks.loadPluginRegistrySnapshot.mockReturnValue({ plugins: [] });
pluginRegistryMocks.getCurrentPluginMetadataSnapshot.mockReset();
pluginRegistryMocks.getCurrentPluginMetadataSnapshot.mockReturnValue(undefined);
pluginRegistryMocks.loadPluginMetadataSnapshot.mockClear();
__testing.resetProviderEnvVarCachesForTests();
});
@@ -177,6 +184,55 @@ describe("provider env vars dynamic manifest metadata", () => {
});
});
it("reuses the current compatible metadata snapshot for workspace auth evidence", async () => {
pluginRegistryMocks.getCurrentPluginMetadataSnapshot.mockReturnValue({
index: {
plugins: [
{
pluginId: "external-cloud",
origin: "global",
enabled: true,
enabledByDefault: true,
},
],
},
plugins: [
{
id: "external-cloud",
origin: "global",
setup: {
providers: [
{
id: "external-cloud",
authEvidence: [
{
type: "local-file-with-env",
fileEnvVar: "EXTERNAL_CLOUD_CREDENTIALS",
credentialMarker: "external-cloud-local-credentials",
},
],
},
],
},
},
],
});
expect(
resolveProviderAuthEvidence({
config: {},
workspaceDir: "/workspace",
})["external-cloud"],
).toEqual([
{
type: "local-file-with-env",
fileEnvVar: "EXTERNAL_CLOUD_CREDENTIALS",
credentialMarker: "external-cloud-local-credentials",
},
]);
expect(pluginRegistryMocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
});
it("excludes untrusted workspace plugin auth evidence by default", async () => {
pluginRegistryMocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({
plugins: [

View File

@@ -1,12 +1,16 @@
import { resolveProviderAuthAliasMap } from "../agents/provider-auth-aliases.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
import { isInstalledPluginEnabled } from "../plugins/installed-plugin-index.js";
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
import {
isWorkspacePluginAllowedByConfig,
normalizePluginConfigId,
} from "../plugins/plugin-config-trust.js";
import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
import {
loadPluginMetadataSnapshot,
type PluginMetadataSnapshot,
} from "../plugins/plugin-metadata-snapshot.js";
import { hasKind } from "../plugins/slots.js";
const CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES = {
@@ -115,15 +119,31 @@ function appendUniqueAuthEvidence(
}
}
function resolveProviderMetadataSnapshot(
params?: ProviderEnvVarLookupParams,
): PluginMetadataSnapshot {
const config = params?.config ?? {};
const env = params?.env ?? process.env;
const current = getCurrentPluginMetadataSnapshot({
config,
env,
...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}),
});
if (current) {
return current;
}
return loadPluginMetadataSnapshot({
config,
workspaceDir: params?.workspaceDir,
env,
preferPersisted: false,
});
}
function resolveManifestProviderAuthEnvVarCandidates(
params?: ProviderEnvVarLookupParams,
): Record<string, string[]> {
const snapshot = loadPluginMetadataSnapshot({
config: params?.config ?? {},
workspaceDir: params?.workspaceDir,
env: params?.env ?? process.env,
preferPersisted: false,
});
const snapshot = resolveProviderMetadataSnapshot(params);
const candidates: Record<string, string[]> = {};
for (const plugin of snapshot.plugins) {
if (!shouldUsePluginProviderEnvVars(plugin, params)) {
@@ -155,12 +175,7 @@ function resolveManifestProviderAuthEnvVarCandidates(
function resolveManifestProviderAuthEvidence(
params?: ProviderEnvVarLookupParams,
): Record<string, ProviderAuthEvidence[]> {
const snapshot = loadPluginMetadataSnapshot({
config: params?.config ?? {},
workspaceDir: params?.workspaceDir,
env: params?.env ?? process.env,
preferPersisted: false,
});
const snapshot = resolveProviderMetadataSnapshot(params);
const evidenceByProvider: Record<string, ProviderAuthEvidence[]> = {};
for (const plugin of snapshot.plugins) {
if (