refactor(agents): thread plugin metadata snapshots per turn (#112769)

* refactor(agents): thread plugin metadata snapshots per turn

* fix(agents): validate threaded plugin metadata

* fix(agents): preserve prepared metadata fallbacks

* fix(commands): preserve snapshot auth refs typing

* fix(commands): use indexed synthetic auth refs

* test(auto-reply): isolate completed goal session store

* test(agents): serialize embedded runner harness files

* test(auto-reply): serialize reply runtime files

* test(auto-reply): isolate goal context admission

* test(auto-reply): allow persisted goal admission under CI load

* test(agents): allow embedded harness warmup under CI load
This commit is contained in:
Peter Steinberger
2026-07-22 19:33:45 -04:00
committed by GitHub
parent db5e59d15e
commit e085b379f8
42 changed files with 831 additions and 1707 deletions
@@ -60,6 +60,11 @@ vi.mock("./agent-runtime-config.js", () => ({
}),
}));
vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({
isPluginMetadataSnapshotCompatible: () => false,
resolvePluginMetadataSnapshot: () => ({ plugins: [] }),
}));
vi.mock("./agent-scope.js", async () => {
const actual = await vi.importActual<typeof import("./agent-scope.js")>("./agent-scope.js");
return {
@@ -79,10 +84,6 @@ vi.mock("./agent-scope.js", async () => {
};
});
vi.mock("../plugins/manifest-contract-eligibility.js", () => ({
loadManifestMetadataSnapshot: () => ({ plugins: [] }),
}));
vi.mock("./model-catalog.js", () => ({
loadManifestModelCatalog: (params: LoadManifestModelCatalogParams) =>
state.loadManifestModelCatalogMock(params),
@@ -282,13 +282,13 @@ vi.mock("./agent-runtime-config.js", () => {
};
});
vi.mock("../config/runtime-snapshot.js", () => ({
setRuntimeConfigSnapshot: vi.fn(),
vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({
isPluginMetadataSnapshotCompatible: () => false,
resolvePluginMetadataSnapshot: () => ({ plugins: [] }),
}));
// Model selection is mocked below, so plugin discovery cannot affect these assertions.
vi.mock("../plugins/manifest-contract-eligibility.js", () => ({
loadManifestMetadataSnapshot: () => ({ plugins: [] }),
vi.mock("../config/runtime-snapshot.js", () => ({
setRuntimeConfigSnapshot: vi.fn(),
}));
vi.mock("../config/sessions.js", () => ({
+13 -2
View File
@@ -7,6 +7,8 @@ import { getRuntimeConfig, readConfigFileSnapshotForWrite } from "../config/io.j
import { setRuntimeConfigSnapshot } from "../config/runtime-snapshot.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { isSecretRef } from "../config/types.secrets.js";
import { resolvePluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
import type { RuntimeEnv } from "../runtime.js";
import { discoverConfigSecretTargetsByIds } from "../secrets/target-registry.js";
import { listAgentEntries } from "./agent-scope.js";
@@ -22,6 +24,7 @@ export async function resolveAgentRuntimeConfig(
loadedRaw: OpenClawConfig;
sourceConfig: OpenClawConfig;
cfg: OpenClawConfig;
pluginMetadataSnapshot?: PluginMetadataSnapshot;
}> {
const loadedRaw = getRuntimeConfig();
const includeChannelTargets = params?.runtimeTargetsChannelSecrets === true;
@@ -31,15 +34,18 @@ export async function resolveAgentRuntimeConfig(
includeChannelTargets,
channel: channelSecretScope?.channel,
});
let pluginMetadataSnapshot: PluginMetadataSnapshot | undefined;
const sourceConfig = await (async () => {
try {
const { snapshot } = await readConfigFileSnapshotForWrite();
const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
if (snapshot.valid) {
pluginMetadataSnapshot = writeOptions.basePluginMetadataSnapshot;
return snapshot.resolved;
}
} catch {
// Fall back to runtime-loaded config when source snapshot is unavailable.
}
pluginMetadataSnapshot = resolvePluginMetadataSnapshot({ config: loadedRaw });
return loadedRaw;
})();
const cfg = hasRuntimeSecretRefs
@@ -77,7 +83,12 @@ export async function resolveAgentRuntimeConfig(
});
secretsRuntime.activateSecretsRuntimeSnapshot(snapshot);
}
return { loadedRaw, sourceConfig, cfg };
return {
loadedRaw,
sourceConfig,
cfg,
...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}),
};
}
function hasNestedSecretRef(value: unknown): boolean {
+1
View File
@@ -1131,6 +1131,7 @@ function createOpenClawCodingToolsInternal(options?: OpenClawCodingToolsOptions)
auditLogLevel: options?.toolPolicyAuditLogLevel,
declaredToolAllowlist: buildDeclaredToolAllowlistContext({
config: options?.config,
metadataSnapshot: options?.preparedModelRuntime?.metadataSnapshot,
workspaceDir: workspaceRoot,
toolDenylist: pluginToolDenylist,
}),
+15 -3
View File
@@ -10,7 +10,10 @@ import { resolveAgentExplicitRecipientSession } from "../../infra/outbound/agent
import { buildOutboundSessionContext } from "../../infra/outbound/session-context.js";
import { parseStrictNonNegativeInteger } from "../../infra/parse-finite-number.js";
import { normalizePluginsConfig } from "../../plugins/config-state.js";
import { loadManifestMetadataSnapshot } from "../../plugins/manifest-contract-eligibility.js";
import {
isPluginMetadataSnapshotCompatible,
resolvePluginMetadataSnapshot,
} from "../../plugins/plugin-metadata-snapshot.js";
import {
classifySessionKeyShape,
isUnscopedSessionKeySentinel,
@@ -133,7 +136,7 @@ export async function prepareAgentCommandExecution(opts: AgentCommandOpts, runti
);
}
const { cfg } = await resolveAgentRuntimeConfig(runtime, {
const { cfg, pluginMetadataSnapshot } = await resolveAgentRuntimeConfig(runtime, {
runtimeTargetsChannelSecrets: opts.deliver === true,
runtimeChannelSecretScope:
opts.deliver !== true && shouldResolveExplicitRecipientSession && recipientChannel
@@ -291,7 +294,16 @@ export async function prepareAgentCommandExecution(opts: AgentCommandOpts, runti
const agentDir = resolveAgentDir(cfg, sessionAgentId);
const pluginsEnabled = normalizePluginsConfig(cfg.plugins).enabled;
const manifestMetadataSnapshot = pluginsEnabled
? loadManifestMetadataSnapshot({ config: cfg, workspaceDir, env: process.env })
? pluginMetadataSnapshot &&
pluginMetadataSnapshot.pluginIds === undefined &&
isPluginMetadataSnapshotCompatible({
snapshot: pluginMetadataSnapshot,
config: cfg,
env: process.env,
workspaceDir,
})
? pluginMetadataSnapshot
: resolvePluginMetadataSnapshot({ config: cfg, env: process.env, workspaceDir })
: undefined;
const modelManifestContext = {
manifestPlugins: manifestMetadataSnapshot?.plugins ?? [],
@@ -743,6 +743,10 @@ export async function loadCompactHooksHarness(): Promise<{
agentDir: input.agentDir,
config: input.config,
workspaceDir: input.workspaceDir,
metadataSnapshot: {
...emptyPluginMetadataSnapshot,
workspaceDir: input.workspaceDir as string | undefined,
},
createStores: () => ({ authStorage: {}, modelRegistry: {} }),
},
release: vi.fn(),
@@ -2,6 +2,7 @@
* Applies final effective tool policy to embedded-agent runtime settings.
*/
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
import { getPluginToolMeta } from "../../plugins/tools.js";
import type { ResolvedConversationCapabilityProfile } from "../conversation-capability-profile.js";
import { buildDeclaredToolAllowlistContext } from "../tool-policy-declared-context.js";
@@ -30,6 +31,8 @@ type FinalEffectiveToolPolicyParams = {
// metadata no longer survives core-tool wrapping/normalization.
bundledTools: AnyAgentTool[];
config?: OpenClawConfig;
workspaceDir?: string;
metadataSnapshot?: PluginMetadataSnapshot;
conversationCapabilityProfile: ResolvedConversationCapabilityProfile;
warn: (message: string) => void;
toolPolicyAuditLogLevel?: "info" | "debug";
@@ -113,6 +116,8 @@ export function applyFinalEffectiveToolPolicy(
onFilter: params.onFilter,
declaredToolAllowlist: buildDeclaredToolAllowlistContext({
config: params.config,
workspaceDir: params.workspaceDir,
metadataSnapshot: params.metadataSnapshot,
toolDenylist: collectExplicitDenylist(pipelineSteps.map((step) => step.policy)),
}),
});
@@ -64,7 +64,7 @@ describe("runEmbeddedAgent incomplete-turn safety", () => {
beforeAll(async () => {
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
});
}, 300_000);
beforeEach(() => {
resetRunOverflowCompactionHarnessMocks();
@@ -10,6 +10,7 @@ import type {
PluginHookBeforeAgentFinalizeEvent,
PluginHookBeforeAgentFinalizeResult,
} from "../../plugins/hook-types.js";
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
import type {
PluginHookAgentContext,
PluginHookBeforeAgentReplyResult,
@@ -65,6 +66,45 @@ type MockResolvedModel = {
reasoning?: boolean;
};
const emptyPluginMetadataSnapshot: PluginMetadataSnapshot = {
policyHash: "",
index: {
version: 1,
hostContractVersion: "test",
compatRegistryVersion: "test",
migrationVersion: 1,
policyHash: "",
generatedAtMs: 1,
installRecords: {},
plugins: [],
diagnostics: [],
},
registryDiagnostics: [],
manifestRegistry: { plugins: [], diagnostics: [] },
plugins: [],
diagnostics: [],
byPluginId: new Map(),
normalizePluginId: (pluginId: string) => pluginId,
owners: {
channels: new Map(),
channelConfigs: new Map(),
providers: new Map(),
modelCatalogProviders: new Map(),
cliBackends: new Map(),
setupProviders: new Map(),
commandAliases: new Map(),
contracts: new Map(),
},
metrics: {
registrySnapshotMs: 0,
manifestRegistryMs: 0,
ownerMapsMs: 0,
totalMs: 0,
indexPluginCount: 0,
manifestPluginCount: 0,
},
};
type MockAgentDiscoveryStores = {
authStorage: {
setRuntimeApiKey: ReturnType<typeof vi.fn>;
@@ -887,6 +927,10 @@ export async function loadRunOverflowCompactionHarness(): Promise<{
agentDir: input.agentDir,
config: input.config,
workspaceDir: input.workspaceDir,
metadataSnapshot: {
...emptyPluginMetadataSnapshot,
workspaceDir: input.workspaceDir as string | undefined,
},
createStores: () => ({ authStorage: {}, modelRegistry: {} }),
},
release: vi.fn(),
@@ -80,6 +80,9 @@ describe("prepareEmbeddedAttemptBundleTools", () => {
} as unknown as Parameters<typeof prepareEmbeddedAttemptBundleTools>[0];
await expect(prepareEmbeddedAttemptBundleTools(input)).rejects.toThrow("bundle policy failed");
expect(mocks.applyFinalEffectiveToolPolicy).toHaveBeenCalledWith(
expect.objectContaining({ workspaceDir: "/tmp/workspace" }),
);
expect(disposeMcp).toHaveBeenCalledOnce();
expect(disposeLsp).toHaveBeenCalledOnce();
});
@@ -138,6 +138,8 @@ export async function prepareEmbeddedAttemptBundleTools(params: {
const filteredBundledTools = applyFinalEffectiveToolPolicy({
bundledTools: [...allowedBundleMcpTools, ...allowedBundleLspTools],
config: params.attempt.config,
workspaceDir: params.effectiveWorkspace,
metadataSnapshot: bundleMetadataSnapshot,
conversationCapabilityProfile: runtimeCapabilityProfile,
warn: (message) => log.warn(message),
});
@@ -150,6 +152,8 @@ export async function prepareEmbeddedAttemptBundleTools(params: {
const allowedAppTools = applyFinalEffectiveToolPolicy({
bundledTools: runtimeAllowedAppTools,
config: params.attempt.config,
workspaceDir: params.effectiveWorkspace,
metadataSnapshot: bundleMetadataSnapshot,
conversationCapabilityProfile: runtimeCapabilityProfile,
warn: (message) => log.warn(message),
});
@@ -127,6 +127,8 @@ export async function prepareAndDispatchEmbeddedRunAttempt(input: {
harnessId: runtime.agentHarness.id,
harnessRuntime: runtime.agentHarness.id,
preparedAuthPlan: runtime.activePreparedAuthPlan,
metadataSnapshot: runtime.pluginMetadataSnapshot,
providerRuntimeHandle: runtime.providerRuntimeHandle,
config: params.config,
workspaceDir,
agentDir,
@@ -0,0 +1,79 @@
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ProviderRuntimePluginHandle } from "../../../plugins/provider-hook-runtime.js";
import type { EmbeddedRunAttemptParams } from "./types.js";
const resolveProviderRuntimePluginHandle = vi.hoisted(() => vi.fn());
vi.mock("../../../plugins/provider-hook-runtime.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../../plugins/provider-hook-runtime.js")>()),
resolveProviderRuntimePluginHandle,
}));
import { prepareEmbeddedAttemptSetup } from "./attempt-setup.js";
describe("prepareEmbeddedAttemptSetup", () => {
beforeEach(() => {
resolveProviderRuntimePluginHandle.mockReset();
});
it("reuses lifecycle metadata and the provider handle from the runtime plan", async () => {
const metadataSnapshot = { plugins: [] } as never;
const workspaceDir = path.join(os.tmpdir(), "openclaw-attempt-setup-prepared");
const providerRuntimeHandle: ProviderRuntimePluginHandle & { prepared: true } = {
provider: "openai",
modelId: "gpt-5.4",
prepared: true,
workspaceDir,
plugin: {} as never,
};
const setup = await prepareEmbeddedAttemptSetup({
config: {},
modelId: "gpt-5.4",
provider: "openai",
runId: "run-prepared",
sessionId: "session-prepared",
thinkLevel: "high",
timeoutMs: 30_000,
workspaceDir,
preparedModelRuntime: { metadataSnapshot } as never,
runtimePlan: { providerRuntimeHandle } as never,
} as unknown as EmbeddedRunAttemptParams);
expect(setup.getCurrentAttemptPluginMetadataSnapshot()).toBe(metadataSnapshot);
expect(setup.getProviderRuntimeHandle()).toBe(providerRuntimeHandle);
expect(resolveProviderRuntimePluginHandle).not.toHaveBeenCalled();
});
it("resolves partial handles without trusting scoped metadata", async () => {
const resolvedHandle: ProviderRuntimePluginHandle = {
provider: "openai",
modelId: "gpt-5.4",
};
resolveProviderRuntimePluginHandle.mockReturnValue(resolvedHandle);
const setup = await prepareEmbeddedAttemptSetup({
config: {},
modelId: "gpt-5.4",
provider: "openai",
runId: "run-partial",
sessionId: "session-partial",
thinkLevel: "high",
timeoutMs: 30_000,
workspaceDir: path.join(os.tmpdir(), "openclaw-attempt-setup-partial"),
preparedModelRuntime: {
metadataSnapshot: { pluginIds: ["other"] },
} as never,
runtimePlan: { providerRuntimeHandle: { provider: "openai" } } as never,
} as unknown as EmbeddedRunAttemptParams);
const preparedHandle = setup.getProviderRuntimeHandle();
expect(preparedHandle).toMatchObject(resolvedHandle);
expect(preparedHandle.modelId).toBe("gpt-5.4");
expect(setup.getProviderRuntimeHandle()).toBe(preparedHandle);
expect(resolveProviderRuntimePluginHandle).toHaveBeenCalledOnce();
const call = resolveProviderRuntimePluginHandle.mock.calls[0]?.[0];
expect(call).toMatchObject({ provider: "openai", modelId: "gpt-5.4" });
expect(call).not.toHaveProperty("pluginMetadataSnapshot");
});
});
@@ -2,8 +2,7 @@
* Resolves workspace, sandbox, provider runtime, and phase reporting for an embedded attempt.
*/
import fs from "node:fs/promises";
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import { getCurrentPluginMetadataSnapshot } from "../../../plugins/current-plugin-metadata-snapshot.js";
import { isPluginMetadataSnapshotCompatible } from "../../../plugins/plugin-metadata-snapshot.js";
import type { PluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.types.js";
import {
resolveProviderRuntimePluginHandle,
@@ -24,30 +23,10 @@ import {
import { resolveAttemptFsWorkspaceOnly } from "./attempt.prompt-helpers.js";
import type { EmbeddedRunAttemptParams } from "./types.js";
function pluginMetadataSnapshotCoversProvider(
snapshot: PluginMetadataSnapshot | undefined,
provider: string,
): snapshot is PluginMetadataSnapshot {
const normalizedProvider = normalizeProviderId(provider);
if (!snapshot || !normalizedProvider) {
return false;
}
return snapshot.manifestRegistry.plugins.some((plugin) => {
const ownsProvider = plugin.providers.some(
(providerId) => normalizeProviderId(providerId) === normalizedProvider,
);
if (ownsProvider) {
return true;
}
const modelCatalogProviderIds = [
...Object.keys(plugin.modelCatalog?.providers ?? {}),
...Object.keys(plugin.modelCatalog?.aliases ?? {}),
];
return modelCatalogProviderIds.some(
(providerId) => normalizeProviderId(providerId) === normalizedProvider,
);
});
}
type PreparedProviderRuntimePluginHandle = ProviderRuntimePluginHandle & {
modelId: string;
prepared: true;
};
export async function prepareEmbeddedAttemptSetup(params: EmbeddedRunAttemptParams) {
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
@@ -117,40 +96,50 @@ export async function prepareEmbeddedAttemptSetup(params: EmbeddedRunAttemptPara
const effectiveCwd = sandbox?.enabled ? effectiveWorkspace : (requestedCwd ?? effectiveWorkspace);
await fs.mkdir(effectiveWorkspace, { recursive: true });
let currentPluginMetadataSnapshotResolved = false;
let currentPluginMetadataSnapshot: PluginMetadataSnapshot | undefined;
const getCurrentAttemptPluginMetadataSnapshot = () => {
if (!currentPluginMetadataSnapshotResolved) {
currentPluginMetadataSnapshot = getCurrentPluginMetadataSnapshot({
allowScopedSnapshot: true,
config: params.config,
env: process.env,
workspaceDir: effectiveWorkspace,
});
currentPluginMetadataSnapshotResolved = true;
}
return currentPluginMetadataSnapshot;
};
let providerRuntimeHandle: ProviderRuntimePluginHandle | undefined;
const getProviderRuntimeHandle = () => {
if (providerRuntimeHandle?.plugin) {
const getCurrentAttemptPluginMetadataSnapshot = (): PluginMetadataSnapshot | undefined =>
params.preparedModelRuntime?.metadataSnapshot;
let providerRuntimeHandle = params.runtimePlan?.providerRuntimeHandle as
| PreparedProviderRuntimePluginHandle
| undefined;
const getProviderRuntimeHandle = (): PreparedProviderRuntimePluginHandle => {
if (
providerRuntimeHandle &&
providerRuntimeHandle.prepared &&
providerRuntimeHandle.provider === params.provider &&
providerRuntimeHandle.modelId === params.modelId &&
providerRuntimeHandle.workspaceDir === effectiveWorkspace
) {
return providerRuntimeHandle;
}
const pluginMetadataSnapshot = getCurrentAttemptPluginMetadataSnapshot();
const resolvedHandle = resolveProviderRuntimePluginHandle({
const compatibleMetadataSnapshot =
pluginMetadataSnapshot &&
pluginMetadataSnapshot.pluginIds === undefined &&
isPluginMetadataSnapshotCompatible({
snapshot: pluginMetadataSnapshot,
config: params.config,
env: process.env,
workspaceDir: effectiveWorkspace,
})
? pluginMetadataSnapshot
: undefined;
providerRuntimeHandle = {
...resolveProviderRuntimePluginHandle({
provider: params.provider,
modelId: params.modelId,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
...(compatibleMetadataSnapshot
? { pluginMetadataSnapshot: compatibleMetadataSnapshot }
: {}),
}),
provider: params.provider,
modelId: params.modelId,
config: params.config,
prepared: true,
workspaceDir: effectiveWorkspace,
env: process.env,
...(pluginMetadataSnapshotCoversProvider(pluginMetadataSnapshot, params.provider)
? { pluginMetadataSnapshot }
: {}),
});
if (resolvedHandle.plugin) {
providerRuntimeHandle = resolvedHandle;
}
return resolvedHandle;
};
return providerRuntimeHandle;
};
const { sessionAgentId } = resolveSessionAgentIds({
sessionKey: params.sessionKey,
@@ -1,4 +1,6 @@
import type { ThinkLevel } from "../../../auto-reply/thinking.js";
import { isPluginMetadataSnapshotCompatible } from "../../../plugins/plugin-metadata-snapshot.js";
import { resolveProviderRuntimePluginHandle } from "../../../plugins/provider-hook-runtime.js";
import type { AuthProfileStore } from "../../auth-profiles.js";
import { isProfileInCooldown } from "../../auth-profiles.js";
import type { ResolvedProviderAuth } from "../../model-auth.js";
@@ -70,6 +72,7 @@ export async function prepareEmbeddedRunRuntime(input: {
});
provider = modelSetup.provider;
modelId = modelSetup.modelId;
const pluginMetadataSnapshot = input.preparedModelRuntime?.metadataSnapshot;
const {
requestedModelId,
modelSelectionChangedByHook,
@@ -462,6 +465,29 @@ export async function prepareEmbeddedRunRuntime(input: {
}
input.markStartupStage("auth");
input.notifyExecutionPhase("auth", { provider, model: modelId });
const compatibleMetadataSnapshot =
pluginMetadataSnapshot &&
pluginMetadataSnapshot.pluginIds === undefined &&
isPluginMetadataSnapshotCompatible({
snapshot: pluginMetadataSnapshot,
config: params.config,
env: process.env,
workspaceDir: input.workspaceDir,
})
? pluginMetadataSnapshot
: undefined;
const providerRuntimeHandle = {
...resolveProviderRuntimePluginHandle({
provider,
modelId,
config: params.config,
workspaceDir: input.workspaceDir,
env: process.env,
...(compatibleMetadataSnapshot ? { pluginMetadataSnapshot: compatibleMetadataSnapshot } : {}),
}),
modelId,
prepared: true as const,
};
return {
provider,
@@ -514,6 +540,8 @@ export async function prepareEmbeddedRunRuntime(input: {
apiKeyInfo,
lastProfileId,
runtimeAuthState,
pluginMetadataSnapshot,
providerRuntimeHandle,
}),
};
}
+6 -12
View File
@@ -98,15 +98,11 @@ function resolveManifestPluginsForModelIdNormalization(params: {
if (currentManifestPlugins) {
return currentManifestPlugins;
}
return loadManifestMetadataSnapshot({
config: params.cfg,
env: process.env,
}).plugins;
}
return loadManifestMetadataSnapshot({
config: params.cfg,
workspaceDir,
env: process.env,
...(workspaceDir ? { workspaceDir } : {}),
}).plugins;
}
@@ -1356,17 +1352,15 @@ function resolveConfiguredModelManifestPlugins(params: {
}
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState();
if (!workspaceDir) {
return (
getCurrentPluginMetadataSnapshot({
config: params.cfg,
env: process.env,
})?.plugins ?? []
);
return getCurrentPluginMetadataSnapshot({
config: params.cfg,
env: process.env,
})?.plugins;
}
return loadManifestMetadataSnapshot({
config: params.cfg,
workspaceDir,
env: process.env,
...(workspaceDir ? { workspaceDir } : {}),
}).plugins;
}
+2 -2
View File
@@ -10,7 +10,7 @@ import { getCurrentPluginMetadataSnapshotState } from "../plugins/current-plugin
import { buildManifestBuiltInModelSuppressionResolver } from "../plugins/manifest-model-suppression.js";
import { resolvePluginControlPlaneFingerprint } from "../plugins/plugin-control-plane-context.js";
import { registerPluginMetadataProcessMemoLifecycleClear } from "../plugins/plugin-metadata-lifecycle.js";
import { resolvePluginMetadataSnapshotMemoEnvFingerprint } from "../plugins/plugin-metadata-snapshot.js";
import { resolvePluginMetadataEnvFingerprint } from "../plugins/plugin-metadata-snapshot.js";
type ManifestSuppressionResolver = ReturnType<typeof buildManifestBuiltInModelSuppressionResolver>;
@@ -45,7 +45,7 @@ function resolveCachedManifestSuppressionResolver(params: {
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
});
const cwd = process.cwd();
const envFingerprint = resolvePluginMetadataSnapshotMemoEnvFingerprint(params.env);
const envFingerprint = resolvePluginMetadataEnvFingerprint(params.env);
const metadataSnapshot = getCurrentPluginMetadataSnapshotState().snapshot;
if (
cached !== undefined &&
@@ -320,6 +320,11 @@ vi.mock("../agents/provider-model-normalization.runtime.js", () => ({
vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({
getCurrentPluginMetadataSnapshot: () => emptyPluginMetadataSnapshot,
}));
vi.mock("../plugins/plugin-metadata-snapshot.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../plugins/plugin-metadata-snapshot.js")>()),
isPluginMetadataSnapshotCompatible: () => true,
resolvePluginMetadataSnapshot: () => emptyPluginMetadataSnapshot,
}));
vi.mock("../plugins/provider-thinking.js", () => ({
resolveProviderBinaryThinking: () => undefined,
resolveProviderDefaultThinkingLevel: () => undefined,
+2 -4
View File
@@ -255,9 +255,6 @@ export function createOpenClawTools(
const spawnWorkspaceDir = resolveWorkspaceRoot(
options?.spawnWorkspaceDir ?? options?.workspaceDir ?? inferredWorkspaceDir,
);
const runtimeCwd = resolveWorkspaceRoot(
options?.cwd ?? options?.workspaceDir ?? inferredWorkspaceDir,
);
options?.recordToolPrepStage?.("openclaw-tools:session-workspace");
const deliveryContext = normalizeDeliveryContext({
channel: options?.agentChannel,
@@ -530,7 +527,7 @@ export function createOpenClawTools(
? createTaskSuggestionTools({
sessionKey: taskKey,
agentId: sessionAgentId,
cwd: runtimeCwd,
cwd: resolveWorkspaceRoot(options?.cwd ?? options?.workspaceDir ?? inferredWorkspaceDir),
})
: []),
...(messageTool && includeMessageTool ? [messageTool] : []),
@@ -708,6 +705,7 @@ export function createOpenClawTools(
sandboxed: options?.sandboxed,
activeModelProvider: options?.modelProvider,
activeModelId: options?.modelId,
metadataSnapshot: options?.preparedModelRuntime?.metadataSnapshot,
activeDeliveryContext: {
channel: options?.agentChannel,
to: options?.currentChannelId ?? options?.agentTo,
+51 -68
View File
@@ -4,20 +4,14 @@ import { createParameterFreeTool } from "openclaw/plugin-sdk/agent-runtime-test-
import { afterEach, describe, expect, it, vi } from "vitest";
import { resetConfigRuntimeState, setRuntimeConfigSnapshot } from "../../config/config.js";
import {
resolveProviderRuntimePluginHandle,
prepareProviderExtraParams,
resolveProviderFollowupFallbackRoute,
resolveProviderRuntimePluginHandle,
type ProviderRuntimePluginHandle,
} from "../../plugins/provider-hook-runtime.js";
import { buildAgentRuntimeDeliveryPlan, buildAgentRuntimePlan } from "./build.js";
const manifestMocks = vi.hoisted(() => ({
loadManifestMetadataSnapshot: vi.fn(() => ({}) as never),
}));
vi.mock("../../plugins/manifest-contract-eligibility.js", () => ({
loadManifestMetadataSnapshot: manifestMocks.loadManifestMetadataSnapshot,
}));
const isPluginMetadataSnapshotCompatible = vi.hoisted(() => vi.fn(() => true));
vi.mock("../../plugins/provider-hook-runtime.js", () => ({
clearProviderRuntimePluginCacheForTest: vi.fn(),
@@ -37,6 +31,11 @@ vi.mock("../../plugins/provider-hook-runtime.js", () => ({
wrapProviderStreamFn: vi.fn(() => undefined),
}));
vi.mock("../../plugins/plugin-metadata-snapshot.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../plugins/plugin-metadata-snapshot.js")>()),
isPluginMetadataSnapshotCompatible,
}));
const gpt54Model = {
id: "gpt-5.4",
name: "GPT-5.4",
@@ -90,8 +89,6 @@ function latestFollowupRouteCall(): {
describe("AgentRuntimePlan", () => {
afterEach(() => {
resetConfigRuntimeState();
manifestMocks.loadManifestMetadataSnapshot.mockClear();
vi.mocked(resolveProviderRuntimePluginHandle).mockClear();
});
it("defers default transport extra params until they are read", () => {
@@ -292,8 +289,13 @@ describe("AgentRuntimePlan", () => {
route: "dispatcher" as const,
reason: "prepared-route",
});
const providerRuntimeHandle: ProviderRuntimePluginHandle = {
const providerRuntimeHandle: ProviderRuntimePluginHandle & {
modelId: string;
prepared: true;
} = {
provider: "openai",
modelId: "gpt-5.4",
prepared: true,
};
const plan = buildAgentRuntimePlan({
@@ -323,27 +325,22 @@ describe("AgentRuntimePlan", () => {
expect(followupCall.context?.dispatcherAvailable).toBe(true);
});
it("resolves incomplete supplied provider handles before invoking runtime hooks", () => {
const resolveProviderRuntimePluginHandleMock = vi.mocked(resolveProviderRuntimePluginHandle);
it("reuses the provider handle prepared before plan construction", () => {
const resolveProviderFollowupFallbackRouteMock = vi.mocked(
resolveProviderFollowupFallbackRoute,
);
resolveProviderRuntimePluginHandleMock.mockClear();
resolveProviderFollowupFallbackRouteMock.mockClear();
const suppliedHandle = {
const suppliedHandle: ProviderRuntimePluginHandle & { modelId: string; prepared: true } = {
provider: "openai",
modelId: "gpt-5.4",
prepared: true,
config: { plugins: { allow: ["openai"] } },
};
const resolvedHandle: ProviderRuntimePluginHandle = {
...suppliedHandle,
workspaceDir: "/tmp/openclaw-runtime-plan",
env: process.env,
plugin: {} as never,
};
resolveProviderRuntimePluginHandleMock.mockReturnValueOnce(resolvedHandle);
const plan = buildAgentRuntimePlan({
provider: "openai",
modelId: "gpt-5.4",
@@ -352,7 +349,7 @@ describe("AgentRuntimePlan", () => {
providerRuntimeHandle: suppliedHandle,
});
expect(plan.providerRuntimeHandle).toBe(resolvedHandle);
expect(plan.providerRuntimeHandle).toBe(suppliedHandle);
plan.delivery.resolveFollowupRoute({
payload: { text: "hello" },
@@ -360,39 +357,25 @@ describe("AgentRuntimePlan", () => {
dispatcherAvailable: true,
});
expect(resolveProviderRuntimePluginHandleMock).toHaveBeenCalledWith({
provider: "openai",
modelId: "gpt-5.4",
config: suppliedHandle.config,
workspaceDir: "/tmp/openclaw-runtime-plan",
env: process.env,
applyAutoEnable: undefined,
bundledProviderVitestCompat: undefined,
});
const followupCall = latestFollowupRouteCall();
expect(followupCall.runtimeHandle).toBe(resolvedHandle);
expect(followupCall.runtimeHandle).toBe(suppliedHandle);
});
it("resolves incomplete supplied delivery handles before follow-up routing", () => {
const resolveProviderRuntimePluginHandleMock = vi.mocked(resolveProviderRuntimePluginHandle);
it("reuses a delivery-only provider handle", () => {
const resolveProviderFollowupFallbackRouteMock = vi.mocked(
resolveProviderFollowupFallbackRoute,
);
resolveProviderRuntimePluginHandleMock.mockClear();
resolveProviderFollowupFallbackRouteMock.mockClear();
const suppliedHandle = {
provider: "openai",
};
const resolvedHandle: ProviderRuntimePluginHandle = {
const suppliedHandle: ProviderRuntimePluginHandle & { modelId: string; prepared: true } = {
provider: "openai",
modelId: "gpt-5.4",
prepared: true,
workspaceDir: "/tmp/openclaw-runtime-plan",
env: process.env,
plugin: {} as never,
};
resolveProviderRuntimePluginHandleMock.mockReturnValueOnce(resolvedHandle);
const delivery = buildAgentRuntimeDeliveryPlan({
provider: "openai",
modelId: "gpt-5.4",
@@ -407,42 +390,42 @@ describe("AgentRuntimePlan", () => {
dispatcherAvailable: true,
});
expect(resolveProviderRuntimePluginHandleMock).toHaveBeenCalledWith({
provider: "openai",
modelId: "gpt-5.4",
config: {},
workspaceDir: "/tmp/openclaw-runtime-plan",
env: process.env,
applyAutoEnable: undefined,
bundledProviderVitestCompat: undefined,
});
const followupCall = latestFollowupRouteCall();
expect(followupCall.runtimeHandle).toBe(resolvedHandle);
expect(followupCall.runtimeHandle).toBe(suppliedHandle);
});
it("plans tool metadata against the runtime source snapshot lazily", () => {
const sourceConfig = { channels: { telegram: { botToken: "token" } } };
const runtimeConfig = {
...sourceConfig,
plugins: { allow: ["telegram"] },
};
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
it("threads prepared tool metadata without discovery", () => {
const metadataSnapshot = { plugins: [] };
vi.mocked(resolveProviderRuntimePluginHandle).mockClear();
const plan = buildAgentRuntimePlan({
provider: "openai",
modelId: "gpt-5.4",
metadataSnapshot,
});
expect(plan.tools.preparedPlanning?.metadataSnapshot).toBe(metadataSnapshot);
expect(resolveProviderRuntimePluginHandle).toHaveBeenCalledWith(
expect.objectContaining({ pluginMetadataSnapshot: metadataSnapshot }),
);
});
it("validates threaded tool metadata against the source config projection", () => {
const runtimeConfig = { plugins: { entries: { runtimeOnly: { enabled: true } } } };
const sourceConfig = { plugins: { entries: {} } };
const metadataSnapshot = { plugins: [] };
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
isPluginMetadataSnapshotCompatible.mockClear();
buildAgentRuntimePlan({
provider: "openai",
modelId: "gpt-5.4",
config: runtimeConfig,
workspaceDir: "/tmp/openclaw-runtime-plan",
metadataSnapshot,
});
expect(manifestMocks.loadManifestMetadataSnapshot).not.toHaveBeenCalled();
plan.tools.preparedPlanning?.loadMetadataSnapshot?.();
expect(manifestMocks.loadManifestMetadataSnapshot).toHaveBeenCalledWith({
config: sourceConfig,
workspaceDir: "/tmp/openclaw-runtime-plan",
env: process.env,
});
expect(isPluginMetadataSnapshotCompatible).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ config: sourceConfig }),
);
});
});
+63 -58
View File
@@ -8,7 +8,10 @@ import { isSilentReplyPayloadText, SILENT_REPLY_TOKEN } from "../../auto-reply/t
import { projectConfigOntoRuntimeSourceSnapshot } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { hasReplyPayloadContent } from "../../interactive/payload.js";
import { loadManifestMetadataSnapshot } from "../../plugins/manifest-contract-eligibility.js";
import {
isPluginMetadataSnapshotCompatible,
resolvePluginMetadataSnapshot,
} from "../../plugins/plugin-metadata-snapshot.js";
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
import {
resolveProviderRuntimePluginHandle,
@@ -54,40 +57,54 @@ function asProviderRuntimeModel(
return value !== undefined ? (value as ProviderRuntimeModel) : undefined;
}
function isProviderRuntimePluginHandle(
value: BuildAgentRuntimePlanParams["providerRuntimeHandle"] | ProviderRuntimePluginHandle,
): value is ProviderRuntimePluginHandle {
return value !== undefined && "plugin" in value;
type RuntimePlanMetadataParams = BuildAgentRuntimeDeliveryPlanParams & {
metadataSnapshot?: BuildAgentRuntimePlanParams["metadataSnapshot"];
};
function resolveCompatibleMetadataSnapshot(
params: RuntimePlanMetadataParams,
config: OpenClawConfig | undefined = asOpenClawConfig(params.config),
): PluginMetadataSnapshot | undefined {
const metadataSnapshot = params.metadataSnapshot as PluginMetadataSnapshot | undefined;
return metadataSnapshot &&
metadataSnapshot.pluginIds === undefined &&
isPluginMetadataSnapshotCompatible({
snapshot: metadataSnapshot,
config,
env: process.env,
workspaceDir: params.workspaceDir,
})
? metadataSnapshot
: undefined;
}
function resolveProviderRuntimeHandleForPlugins(params: {
provider: string;
modelId?: string;
config?: OpenClawConfig;
workspaceDir?: string;
runtimeHandle?: BuildAgentRuntimePlanParams["providerRuntimeHandle"];
resolveWhenMissing?: boolean;
}): ProviderRuntimePluginHandle | undefined {
function resolvePreparedProviderRuntimeHandle(
params: RuntimePlanMetadataParams,
): ProviderRuntimePluginHandle & { modelId: string; prepared: true } {
if (
isProviderRuntimePluginHandle(params.runtimeHandle) &&
(params.runtimeHandle.plugin ||
!params.modelId ||
params.runtimeHandle.modelId === params.modelId)
params.providerRuntimeHandle?.prepared === true &&
params.providerRuntimeHandle.provider === params.provider &&
params.providerRuntimeHandle.modelId === params.modelId &&
params.providerRuntimeHandle.workspaceDir === params.workspaceDir
) {
return params.runtimeHandle;
return params.providerRuntimeHandle as ProviderRuntimePluginHandle & {
modelId: string;
prepared: true;
};
}
if (!params.runtimeHandle && !params.resolveWhenMissing) {
return undefined;
}
return resolveProviderRuntimePluginHandle({
provider: params.runtimeHandle?.provider ?? params.provider,
const compatibleMetadataSnapshot = resolveCompatibleMetadataSnapshot(params);
return {
...resolveProviderRuntimePluginHandle({
provider: params.provider,
modelId: params.modelId,
config: asOpenClawConfig(params.config),
workspaceDir: params.workspaceDir,
env: process.env,
...(compatibleMetadataSnapshot ? { pluginMetadataSnapshot: compatibleMetadataSnapshot } : {}),
}),
modelId: params.modelId,
config: asOpenClawConfig(params.runtimeHandle?.config) ?? params.config,
workspaceDir: params.runtimeHandle?.workspaceDir ?? params.workspaceDir,
env: params.runtimeHandle?.env ?? process.env,
applyAutoEnable: params.runtimeHandle?.applyAutoEnable,
bundledProviderVitestCompat: params.runtimeHandle?.bundledProviderVitestCompat,
});
prepared: true,
};
}
/** Build delivery-specific runtime decisions for one provider/model. */
@@ -95,13 +112,7 @@ export function buildAgentRuntimeDeliveryPlan(
params: BuildAgentRuntimeDeliveryPlanParams,
): AgentRuntimeDeliveryPlan {
const config = asOpenClawConfig(params.config);
const providerRuntimeHandle = resolveProviderRuntimeHandleForPlugins({
provider: params.provider,
modelId: params.modelId,
config,
workspaceDir: params.workspaceDir,
runtimeHandle: params.providerRuntimeHandle,
});
const providerRuntimeHandle = resolvePreparedProviderRuntimeHandle(params);
return {
isSilentPayload(payload): boolean {
return (
@@ -146,25 +157,21 @@ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): Agen
const modelApi = params.modelApi ?? params.model?.api ?? undefined;
const transport = params.resolvedTransport;
const toolPlanningConfig = config ? projectConfigOntoRuntimeSourceSnapshot(config) : undefined;
let toolPlanningMetadataSnapshot: PluginMetadataSnapshot | undefined;
const loadToolPlanningMetadataSnapshot = () => {
// Metadata is process-stable for one run; load lazily because many attempts
// never need prepared tool planning.
toolPlanningMetadataSnapshot ??= loadManifestMetadataSnapshot({
config: toolPlanningConfig,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
env: process.env,
});
return toolPlanningMetadataSnapshot;
};
const providerRuntimeHandleForPlugins = resolveProviderRuntimeHandleForPlugins({
provider: params.provider,
modelId: params.modelId,
config,
workspaceDir: params.workspaceDir,
runtimeHandle: params.providerRuntimeHandle,
resolveWhenMissing: true,
});
const toolPlanningMetadataSnapshot = resolveCompatibleMetadataSnapshot(
params,
toolPlanningConfig,
);
const preparedPlanning = toolPlanningMetadataSnapshot
? { metadataSnapshot: toolPlanningMetadataSnapshot }
: {
loadMetadataSnapshot: () =>
resolvePluginMetadataSnapshot({
config: toolPlanningConfig,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
env: process.env,
}),
};
const providerRuntimeHandleForPlugins = resolvePreparedProviderRuntimeHandle(params);
const auth =
params.preparedAuthPlan ??
buildAgentRuntimeAuthPlan({
@@ -294,9 +301,7 @@ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): Agen
},
},
tools: {
preparedPlanning: {
loadMetadataSnapshot: loadToolPlanningMetadataSnapshot,
},
preparedPlanning,
normalize<TSchemaType extends TSchema = TSchema, TResult = unknown>(
tools: AgentTool<TSchemaType, TResult>[],
overrides?: {
+12 -3
View File
@@ -88,6 +88,7 @@ type AgentRuntimeTextTransforms = {
/** Resolved provider runtime handle forwarded to plugin-owned hooks. */
type AgentRuntimeProviderHandle = {
provider: string;
modelId?: string | null;
config?: AgentRuntimeConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
@@ -95,6 +96,11 @@ type AgentRuntimeProviderHandle = {
bundledProviderVitestCompat?: boolean;
};
type PreparedAgentRuntimeProviderHandle = AgentRuntimeProviderHandle & {
modelId: string | null;
prepared: true;
};
type AgentRuntimeInteractiveButtonStyle = "primary" | "secondary" | "success" | "danger";
type AgentRuntimeMessagePresentationAction =
@@ -513,7 +519,7 @@ type AgentRuntimeTransportPlan = {
/** Complete prepared runtime plan consumed by embedded-agent attempts. */
export type AgentRuntimePlan = {
resolvedRef: AgentRuntimeResolvedRef;
providerRuntimeHandle?: AgentRuntimeProviderHandle;
providerRuntimeHandle?: PreparedAgentRuntimeProviderHandle;
auth: AgentRuntimeAuthPlan;
prompt: AgentRuntimePromptPlan;
tools: AgentRuntimeToolPlan;
@@ -546,7 +552,7 @@ export type BuildAgentRuntimeDeliveryPlanParams = {
agentDir?: string;
provider: string;
modelId: string;
providerRuntimeHandle?: AgentRuntimeProviderHandle;
providerRuntimeHandle?: PreparedAgentRuntimeProviderHandle;
};
/** Inputs needed to build the full prepared runtime plan. */
@@ -574,5 +580,8 @@ export type BuildAgentRuntimePlanParams = {
thinkingLevel?: AgentRuntimeThinkLevel;
extraParamsOverride?: Record<string, unknown>;
resolvedTransport?: AgentRuntimeTransport;
providerRuntimeHandle?: AgentRuntimeProviderHandle;
/** Omit only when a standalone caller intentionally resolves provider hooks lazily. */
providerRuntimeHandle?: PreparedAgentRuntimeProviderHandle;
/** Lifecycle-owned plugin metadata prepared before the attempt starts. */
metadataSnapshot?: AgentRuntimePreparedMetadataSnapshot;
};
+27 -9
View File
@@ -3,12 +3,12 @@ import { uniqueStrings } from "@openclaw/normalization-core/string-normalization
import { normalizeConfiguredMcpServers } from "../config/mcp-config-normalize.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizePluginsConfig } from "../plugins/config-state.js";
import {
isManifestPluginAvailableForControlPlane,
loadManifestMetadataSnapshot,
} from "../plugins/manifest-contract-eligibility.js";
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
import { isManifestPluginAvailableForControlPlane } from "../plugins/manifest-contract-eligibility.js";
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
import { hasManifestToolAvailability } from "../plugins/manifest-tool-availability.js";
import { isPluginMetadataSnapshotCompatible } from "../plugins/plugin-metadata-snapshot.js";
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
import { sanitizeServerName, TOOL_NAME_SEPARATOR } from "./agent-bundle-mcp-names.js";
import { compileGlobPatterns, matchesAnyGlobPattern } from "./glob-pattern.js";
import type { DeclaredToolAllowlistContext } from "./tool-policy.js";
@@ -126,16 +126,33 @@ function collectDeclaredPluginContext(params: {
workspaceDir?: string;
toolDenylist?: string[];
env?: NodeJS.ProcessEnv;
metadataSnapshot?: PluginMetadataSnapshot;
}): Pick<DeclaredToolAllowlistContext, "pluginIds" | "pluginToolNames"> {
if (params.config?.plugins?.enabled === false) {
return {};
}
const env = params.env ?? process.env;
const snapshot = loadManifestMetadataSnapshot({
config: params.config,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
env,
});
const preparedSnapshot =
params.metadataSnapshot &&
params.metadataSnapshot.pluginIds === undefined &&
isPluginMetadataSnapshotCompatible({
snapshot: params.metadataSnapshot,
config: params.config,
env,
workspaceDir: params.workspaceDir,
})
? params.metadataSnapshot
: undefined;
const snapshot =
preparedSnapshot ??
getCurrentPluginMetadataSnapshot({
config: params.config,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
env,
});
if (!snapshot) {
return {};
}
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
const denylist = normalizeToolDenylist(params.toolDenylist);
const pluginIds = new Set<string>();
@@ -175,6 +192,7 @@ export function buildDeclaredToolAllowlistContext(params: {
workspaceDir?: string;
toolDenylist?: string[];
env?: NodeJS.ProcessEnv;
metadataSnapshot?: PluginMetadataSnapshot;
}): DeclaredToolAllowlistContext | undefined {
const mcpServerNames = uniqueStrings(
collectConfiguredMcpServerNames({
+31 -9
View File
@@ -30,10 +30,10 @@ import {
type MediaUnderstandingProvider,
} from "../../plugin-sdk/media-understanding.js";
import { resolvePluginCapabilityProvider } from "../../plugins/capability-provider-runtime.js";
import {
isManifestPluginAvailableForControlPlane,
loadManifestMetadataSnapshot,
} from "../../plugins/manifest-contract-eligibility.js";
import { getCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js";
import { isManifestPluginAvailableForControlPlane } from "../../plugins/manifest-contract-eligibility.js";
import { isPluginMetadataSnapshotCompatible } from "../../plugins/plugin-metadata-snapshot.js";
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
import { resolveUserPath } from "../../utils.js";
import type { AuthProfileStore } from "../auth-profiles/types.js";
@@ -490,6 +490,7 @@ function providerUsesRuntimeModelAugment(params: {
cfg?: OpenClawConfig;
provider: string;
workspaceDir?: string;
metadataSnapshot?: PluginMetadataSnapshot;
}): boolean {
const provider = normalizeMediaProviderId(params.provider);
if (!provider) {
@@ -499,11 +500,27 @@ function providerUsesRuntimeModelAugment(params: {
return true;
}
const config = params.cfg ?? {};
const snapshot = loadManifestMetadataSnapshot({
config,
env: process.env,
...(params.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}),
});
const preparedSnapshot =
params.metadataSnapshot &&
params.metadataSnapshot.pluginIds === undefined &&
isPluginMetadataSnapshotCompatible({
snapshot: params.metadataSnapshot,
config,
env: process.env,
workspaceDir: params.workspaceDir,
})
? params.metadataSnapshot
: undefined;
const snapshot =
preparedSnapshot ??
getCurrentPluginMetadataSnapshot({
config,
env: process.env,
...(params.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}),
});
if (!snapshot) {
return false;
}
return snapshot.plugins.some((plugin) => {
const ownsProvider =
plugin.providers.some((candidate) => normalizeMediaProviderId(candidate) === provider) ||
@@ -559,6 +576,7 @@ async function resolveCompressionModelPolicy(params: {
model: string;
agentDir?: string;
workspaceDir?: string;
metadataSnapshot?: PluginMetadataSnapshot;
}): Promise<ImageCompressionModelPolicy> {
const configuredStaticPolicy = await resolveCompressionModelPolicyWithHooks({
...params,
@@ -574,6 +592,7 @@ async function resolveCompressionModelPolicy(params: {
cfg: params.cfg,
provider: params.provider,
workspaceDir: params.workspaceDir,
metadataSnapshot: params.metadataSnapshot,
})
) {
return staticPolicy;
@@ -592,6 +611,7 @@ async function resolveImageCompressionPolicy(params: {
imageCount: number;
agentDir?: string;
workspaceDir?: string;
metadataSnapshot?: PluginMetadataSnapshot;
}): Promise<ImageCompressionPolicy> {
const modelCandidates = resolveCompressionModelCandidates(params);
const quality = params.cfg?.agents?.defaults?.imageQuality;
@@ -603,6 +623,7 @@ async function resolveImageCompressionPolicy(params: {
model: candidate.model,
agentDir: params.agentDir,
workspaceDir: params.workspaceDir,
metadataSnapshot: params.metadataSnapshot,
});
}),
);
@@ -974,6 +995,7 @@ export function createImageTool(options?: {
imageCount: imageInputs.length,
agentDir,
workspaceDir: options?.workspaceDir,
metadataSnapshot: options?.preparedModelRuntime?.metadataSnapshot,
});
imageRoute = { kind: "fallback", imageModelConfig, imageCompression };
}
+25 -7
View File
@@ -20,7 +20,11 @@ import {
} from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { triggerSessionPatchHook } from "../../gateway/session-patch-hooks.js";
import { loadManifestMetadataSnapshot } from "../../plugins/manifest-contract-eligibility.js";
import {
isPluginMetadataSnapshotCompatible,
resolvePluginMetadataSnapshot,
} from "../../plugins/plugin-metadata-snapshot.js";
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
import {
buildAgentMainSessionKey,
parseAgentSessionKey,
@@ -441,6 +445,7 @@ async function resolveModelOverride(params: {
agentId: string;
agentDir: string;
workspaceDir: string;
metadataSnapshot?: PluginMetadataSnapshot;
}): Promise<
| { kind: "reset" }
| {
@@ -475,13 +480,24 @@ async function resolveModelOverride(params: {
? { workspaceDir: params.sessionEntry.spawnedWorkspaceDir }
: {}),
});
const manifestMetadataSnapshot = loadManifestMetadataSnapshot({
config: params.cfg,
workspaceDir: params.sessionEntry?.spawnedWorkspaceDir,
env: process.env,
});
const workspaceDir = params.sessionEntry?.spawnedWorkspaceDir ?? params.workspaceDir;
const manifestMetadataSnapshot =
params.metadataSnapshot &&
params.metadataSnapshot.pluginIds === undefined &&
isPluginMetadataSnapshotCompatible({
snapshot: params.metadataSnapshot,
config: params.cfg,
env: process.env,
workspaceDir,
})
? params.metadataSnapshot
: resolvePluginMetadataSnapshot({
config: params.cfg,
...(workspaceDir ? { workspaceDir } : {}),
env: process.env,
});
const modelManifestContext = {
manifestPlugins: manifestMetadataSnapshot.plugins,
manifestPlugins: manifestMetadataSnapshot?.plugins,
};
const policy = createModelVisibilityPolicy({
cfg: params.cfg,
@@ -532,6 +548,7 @@ export function createSessionStatusTool(opts?: {
sandboxed?: boolean;
activeModelProvider?: string;
activeModelId?: string;
metadataSnapshot?: PluginMetadataSnapshot;
/** Active live-run route, kept separate from the persisted/origin delivery route. */
activeDeliveryContext?: DeliveryContext;
}): AnyAgentTool {
@@ -845,6 +862,7 @@ export function createSessionStatusTool(opts?: {
agentId,
agentDir: selectedAgentDir,
workspaceDir: selectedWorkspaceDir,
metadataSnapshot: opts?.metadataSnapshot,
});
const modelSelection =
selection.kind === "reset"
+27 -2
View File
@@ -31,6 +31,7 @@ const resolveCommandSecretRefsViaGatewayMock = vi.fn();
const resolveQueuedReplyExecutionConfigMock = vi.fn();
const resolveProviderFollowupFallbackRouteMock = vi.fn();
const resolveProviderThinkingProfileMock = vi.fn();
const admitReplyTurnMock = vi.fn();
let resolveQueuedReplyExecutionConfigActual:
| (typeof import("./agent-runner-utils.js"))["resolveQueuedReplyExecutionConfig"]
| undefined;
@@ -428,6 +429,18 @@ async function loadFreshFollowupRunnerModuleForTest() {
refreshQueuedFollowupSession: refreshQueuedFollowupSessionForFollowupTest,
resolveQueueSettings: (): QueueSettings => ({ mode: "followup" }),
}));
vi.doMock("./reply-turn-admission.js", async () => {
const actual = await vi.importActual<typeof import("./reply-turn-admission.js")>(
"./reply-turn-admission.js",
);
return {
...actual,
admitReplyTurn: (...args: Parameters<typeof actual.admitReplyTurn>) =>
admitReplyTurnMock.getMockImplementation()
? admitReplyTurnMock(...args)
: actual.admitReplyTurn(...args),
};
});
vi.doMock("./session-run-accounting.js", () => ({
persistRunSessionUsage: persistRunSessionUsageForFollowupTest,
incrementRunCompactionCount: incrementRunCompactionCountForFollowupTest,
@@ -620,6 +633,7 @@ beforeEach(() => {
resolveProviderFollowupFallbackRouteMock.mockReturnValue(undefined);
resolveProviderThinkingProfileMock.mockReset();
resolveProviderThinkingProfileMock.mockReturnValue(undefined);
admitReplyTurnMock.mockReset();
const resolveQueuedReplyExecutionConfig = resolveQueuedReplyExecutionConfigActual;
if (!resolveQueuedReplyExecutionConfig) {
throw new Error("resolveQueuedReplyExecutionConfig mock not initialized");
@@ -692,7 +706,10 @@ function createQueuedRun(
describe("createFollowupRunner reply-lane admission", () => {
it("drops stale active-goal context after the persisted goal completes", async () => {
runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {} });
const storePath = "/tmp/openclaw-followup-completed-goal.json";
const storePath = path.join(
tmpdir(),
`openclaw-followup-completed-goal-${crypto.randomUUID()}.json`,
);
const activeEntry: SessionEntry = {
sessionId: "session-completed-goal",
updatedAt: 1,
@@ -714,6 +731,14 @@ describe("createFollowupRunner reply-lane admission", () => {
goal: { ...activeEntry.goal!, status: "complete", updatedAt: 2 },
};
registerFollowupTestSessionStore(storePath, { main: completedEntry });
admitReplyTurnMock.mockResolvedValueOnce({
status: "admitted",
operation: createReplyOperationForTest({
sessionKey: "main",
sessionId: completedEntry.sessionId,
resetTriggered: false,
}),
});
const runner = createFollowupRunner({
typing: createMockTypingController(),
typingMode: "instant",
@@ -749,7 +774,7 @@ describe("createFollowupRunner reply-lane admission", () => {
const context = requireRecord(call.currentInboundContext, "current inbound context");
expect(context.text).toContain("Current message:\nmessage_id=next-turn");
expect(context.text).not.toContain("Active goal:");
});
}, 300_000);
it("keeps the originating client caps on queued embedded runs", async () => {
// Regression: the queued path built runEmbeddedAgent params inline and
@@ -116,6 +116,34 @@ describe("createModelListAuthIndex", () => {
expect(index.evaluateModelAuth("disabled-provider").availability).toBeUndefined();
});
it("uses enabled synthetic refs from prepared metadata without reloading the registry", () => {
const metadataSnapshot = {
registrySource: "persisted",
registryDiagnostics: [],
plugins: [],
index: {
plugins: [
{ enabled: true, syntheticAuthRefs: ["codex"] },
{ enabled: false, syntheticAuthRefs: ["disabled-provider"] },
],
},
} as unknown as PluginMetadataSnapshot;
const index = createModelListAuthIndex({
cfg: {},
authStore: emptyStore,
env: {},
metadataSnapshot,
routeResolverFactory: dualRouteResolverFactory,
});
expect(index.evaluateModelAuth("openai", { modelId: "gpt-5.5" })).toMatchObject({
availability: undefined,
evidence: "synthetic",
});
expect(index.evaluateModelAuth("disabled-provider").availability).toBeUndefined();
expect(pluginRegistryMocks.loadPluginRegistrySnapshotWithMetadata).not.toHaveBeenCalled();
});
it.each(["derived" as const, "persisted" as const])(
"does not trust unusable synthetic refs from a %s snapshot",
(source) => {
@@ -140,10 +168,15 @@ describe("createModelListAuthIndex", () => {
);
it("uses explicit synthetic refs without loading plugin metadata", () => {
const metadataSnapshot = {
registrySource: "persisted",
plugins: [],
} as unknown as PluginMetadataSnapshot;
const index = createModelListAuthIndex({
cfg: {},
authStore: emptyStore,
env: {},
metadataSnapshot,
syntheticAuthProviderRefs: ["codex"],
routeResolverFactory: dualRouteResolverFactory,
});
+15 -4
View File
@@ -35,16 +35,27 @@ function listValidatedSyntheticAuthProviderRefs(params: {
env: NodeJS.ProcessEnv;
metadataSnapshot?: PluginMetadataSnapshot;
}): readonly string[] {
if (params.metadataSnapshot && (params.metadataSnapshot.registryDiagnostics?.length ?? 0) > 0) {
return [];
if (params.metadataSnapshot) {
if (
params.metadataSnapshot.registryDiagnostics.length > 0 ||
(params.metadataSnapshot.registrySource !== "persisted" &&
params.metadataSnapshot.registrySource !== "provided")
) {
return [];
}
return params.metadataSnapshot.index.plugins
.filter((plugin) => plugin.enabled)
.flatMap((plugin) => plugin.syntheticAuthRefs ?? []);
}
const result = loadPluginRegistrySnapshotWithMetadata({
config: params.cfg,
workspaceDir: params.workspaceDir,
env: params.env,
index: params.metadataSnapshot?.index,
});
if (result.source !== "persisted" && result.source !== "provided") {
if (
result.diagnostics.length > 0 ||
(result.source !== "persisted" && result.source !== "provided")
) {
return [];
}
return result.snapshot.plugins
-2
View File
@@ -7,7 +7,6 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest
import { startGatewayConfigReloader } from "../gateway/config-reload.js";
import { executeSqliteQueryTakeFirstSync, getNodeSqliteKysely } from "../infra/kysely-sync.js";
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
import { clearLoadPluginMetadataSnapshotMemo } from "../plugins/plugin-metadata-snapshot.js";
import { readConfigMachineState } from "../state/config-machine-state.js";
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
import {
@@ -128,7 +127,6 @@ describe("config io write", () => {
afterEach(() => {
resetConfigRuntimeState();
clearLoadPluginMetadataSnapshotMemo();
mockMaintainConfigBackups.mockReset();
mockMaintainConfigBackups.mockResolvedValue(undefined);
});
+6 -3
View File
@@ -11,7 +11,8 @@ import { modelKey, parseModelRef, resolveDefaultModelForAgent } from "../agents/
import { createModelVisibilityPolicy } from "../agents/model-visibility-policy.js";
import { getRuntimeConfig } from "../config/io.js";
import { resolveSessionEntryAccessTarget } from "../config/sessions/session-accessor.js";
import { loadManifestMetadataSnapshot } from "../plugins/manifest-contract-eligibility.js";
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
import { getActivePluginRegistryWorkspaceDirFromState } from "../plugins/runtime-state.js";
import {
buildAgentMainSessionKey,
isAcpSessionKey,
@@ -138,12 +139,14 @@ export async function resolveOpenAiCompatModelOverride(params: {
const cfg = getRuntimeConfig();
const defaultModelRef = resolveDefaultModelForAgent({ cfg, agentId: params.agentId });
const defaultProvider = defaultModelRef.provider;
const manifestMetadataSnapshot = loadManifestMetadataSnapshot({
const workspaceDir = getActivePluginRegistryWorkspaceDirFromState();
const manifestMetadataSnapshot = getCurrentPluginMetadataSnapshot({
config: cfg,
env: process.env,
...(workspaceDir ? { workspaceDir } : {}),
});
const modelManifestContext = {
manifestPlugins: manifestMetadataSnapshot.plugins,
manifestPlugins: manifestMetadataSnapshot?.plugins,
};
const parsed = parseModelRef(raw, defaultProvider, {
allowManifestNormalization: true,
-3
View File
@@ -8,7 +8,6 @@ import type { OpenClawConfig } from "../config/config.js";
import { resetLogger, setLoggerOverride } from "../logging/logger.js";
import { loggingState } from "../logging/state.js";
import type { PluginManifestRecord, PluginManifestRegistry } from "../plugins/manifest-registry.js";
import { clearLoadPluginMetadataSnapshotMemo } from "../plugins/plugin-metadata-snapshot.js";
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
const normalizeProviderModelIdWithRuntimeMock = vi.hoisted(() =>
@@ -138,7 +137,6 @@ function requireAbortSignal(signal: RequestInit["signal"] | undefined): AbortSig
describe("model-pricing-cache", () => {
beforeEach(() => {
clearGatewayModelPricingState();
clearLoadPluginMetadataSnapshotMemo();
pluginManifestRegistryMocks.manifestRegistry = undefined;
pluginManifestRegistryMocks.loadPluginManifestRegistryForInstalledIndex.mockClear();
pluginManifestRegistryMocks.listOpenClawPluginManifestMetadata.mockClear();
@@ -147,7 +145,6 @@ describe("model-pricing-cache", () => {
afterEach(() => {
clearGatewayModelPricingState();
clearLoadPluginMetadataSnapshotMemo();
loggingState.rawConsole = null;
resetLogger();
});
@@ -129,7 +129,6 @@ let prepareMediaCapabilityProviders: typeof import("./capability-provider-runtim
let clearCurrentPluginMetadataSnapshot: typeof import("./current-plugin-metadata-snapshot.js").clearCurrentPluginMetadataSnapshot;
let setCurrentPluginMetadataSnapshot: typeof import("./current-plugin-metadata-snapshot.js").setCurrentPluginMetadataSnapshot;
let clearPluginMetadataLifecycleCaches: typeof import("./plugin-metadata-lifecycle.js").clearPluginMetadataLifecycleCaches;
let clearLoadPluginMetadataSnapshotMemo: typeof import("./plugin-metadata-snapshot.js").clearLoadPluginMetadataSnapshotMemo;
function expectResolvedCapabilityProviderIds(providers: Array<{ id: string }>, expected: string[]) {
expect(providers.map((provider) => provider.id)).toEqual(expected);
@@ -292,11 +291,9 @@ describe("resolvePluginCapabilityProviders", () => {
({ clearCurrentPluginMetadataSnapshot, setCurrentPluginMetadataSnapshot } =
await import("./current-plugin-metadata-snapshot.js"));
({ clearPluginMetadataLifecycleCaches } = await import("./plugin-metadata-lifecycle.js"));
({ clearLoadPluginMetadataSnapshotMemo } = await import("./plugin-metadata-snapshot.js"));
});
beforeEach(() => {
clearLoadPluginMetadataSnapshotMemo();
clearCurrentPluginMetadataSnapshot();
clearPluginMetadataLifecycleCaches();
mocks.resolveRuntimePluginRegistry.mockReset();
@@ -319,7 +316,6 @@ describe("resolvePluginCapabilityProviders", () => {
afterEach(() => {
clearCurrentPluginMetadataSnapshot();
clearLoadPluginMetadataSnapshotMemo();
});
it("resolves bundled capability plugins from the current metadata snapshot", () => {
-2
View File
@@ -3,7 +3,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 { clearLoadPluginMetadataSnapshotMemo } from "./plugin-metadata-snapshot.js";
import type { PluginRegistrySnapshot } from "./plugin-registry.js";
const listPotentialConfiguredChannelIds = vi.hoisted(() => vi.fn());
@@ -160,7 +159,6 @@ async function expectStaleMetadataSnapshotRebuild(params: {
describe("loadPluginLookUpTable", () => {
beforeEach(() => {
clearLoadPluginMetadataSnapshotMemo();
listPotentialConfiguredChannelIds
.mockReset()
.mockImplementation((config: OpenClawConfig) => Object.keys(config.channels ?? {}));
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,223 @@
// Verifies lifecycle snapshot loading, ownership facts, and immutable boundaries.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearCurrentPluginMetadataSnapshot,
setCurrentPluginMetadataSnapshot,
} from "./current-plugin-metadata-snapshot.js";
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
import {
loadPluginMetadataSnapshot,
resolvePluginMetadataSnapshot,
} from "./plugin-metadata-snapshot.js";
const loadPluginRegistrySnapshotWithMetadata = vi.hoisted(() => vi.fn());
const loadPluginManifestRegistryForInstalledIndex = vi.hoisted(() => vi.fn());
vi.mock("./plugin-registry.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./plugin-registry.js")>();
return {
...actual,
loadPluginRegistrySnapshotWithMetadata: (params: unknown) =>
loadPluginRegistrySnapshotWithMetadata(params),
};
});
vi.mock("./manifest-registry-installed.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./manifest-registry-installed.js")>();
return {
...actual,
loadPluginManifestRegistryForInstalledIndex: (params: unknown) =>
loadPluginManifestRegistryForInstalledIndex(params),
};
});
function makeIndex(pluginId = "demo"): InstalledPluginIndex {
const rootDir = `/plugins/${pluginId}`;
return {
version: 1,
hostContractVersion: "test",
compatRegistryVersion: "test",
migrationVersion: 1,
policyHash: "test",
generatedAtMs: 1,
installRecords: {},
diagnostics: [],
plugins: [
{
pluginId,
manifestPath: `${rootDir}/openclaw.plugin.json`,
manifestHash: `${pluginId}-manifest`,
rootDir,
origin: "global",
enabled: true,
startup: {
sidecar: false,
memory: false,
deferConfiguredChannelFullLoadUntilAfterListen: false,
agentHarnesses: [],
},
compat: [],
},
],
};
}
function makeManifestRegistry(pluginId = "demo"): PluginManifestRegistry {
const plugin: PluginManifestRecord = {
id: pluginId,
name: pluginId,
channels: [],
providers: [pluginId],
cliBackends: [],
skills: [],
hooks: [],
commandAliases: [{ name: `${pluginId}-command` }],
rootDir: `/plugins/${pluginId}`,
source: `/plugins/${pluginId}/index.js`,
manifestPath: `/plugins/${pluginId}/openclaw.plugin.json`,
origin: "global",
};
return { plugins: [plugin], diagnostics: [] };
}
describe("plugin metadata snapshot", () => {
beforeEach(() => {
loadPluginRegistrySnapshotWithMetadata.mockReset();
loadPluginManifestRegistryForInstalledIndex.mockReset();
loadPluginManifestRegistryForInstalledIndex.mockReturnValue(makeManifestRegistry());
});
afterEach(() => {
clearCurrentPluginMetadataSnapshot();
});
it("keeps explicit control-plane loads fresh", () => {
const index = makeIndex();
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
source: "provided",
snapshot: index,
diagnostics: [],
});
const first = loadPluginMetadataSnapshot({ config: {}, env: {}, index });
const second = loadPluginMetadataSnapshot({ config: {}, env: {}, index });
expect(second).not.toBe(first);
expect(loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledTimes(2);
expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledTimes(2);
});
it("reuses the lifecycle-owned current snapshot", () => {
const config = {};
const index = makeIndex();
index.policyHash = resolveInstalledPluginIndexPolicyHash(config);
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
source: "provided",
snapshot: index,
diagnostics: [],
});
const snapshot = loadPluginMetadataSnapshot({ config, env: {}, index });
setCurrentPluginMetadataSnapshot(snapshot, { config, env: {} });
loadPluginRegistrySnapshotWithMetadata.mockClear();
loadPluginManifestRegistryForInstalledIndex.mockClear();
expect(resolvePluginMetadataSnapshot({ config, env: {} })).toBe(snapshot);
expect(loadPluginRegistrySnapshotWithMetadata).not.toHaveBeenCalled();
expect(loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled();
});
it("keeps scoped loads separate without an LRU", () => {
const index = makeIndex();
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
source: "provided",
snapshot: index,
diagnostics: [],
});
const scoped = loadPluginMetadataSnapshot({
config: {},
env: {},
index,
pluginIds: ["demo"],
});
const unscoped = loadPluginMetadataSnapshot({ config: {}, env: {}, index });
expect(scoped.pluginIds).toEqual(["demo"]);
expect(unscoped.pluginIds).toBeUndefined();
expect(loadPluginManifestRegistryForInstalledIndex.mock.calls[0]?.[0]).toMatchObject({
pluginIds: ["demo"],
});
expect(loadPluginManifestRegistryForInstalledIndex.mock.calls[1]?.[0]).not.toHaveProperty(
"pluginIds",
);
});
it("prepares provider endpoint and request facts", () => {
const index = makeIndex();
const registry = makeManifestRegistry();
const plugin = registry.plugins[0];
if (!plugin) {
throw new Error("expected manifest plugin fixture");
}
plugin.providerEndpoints = [
{
endpointClass: "openai-public",
hosts: [" API.EXAMPLE.COM "],
baseUrls: ["https://api.example.com/v1/"],
},
];
plugin.providerRequest = {
providers: {
demo: {
family: " demo-family ",
compatibilityFamily: " moonshot " as never,
openAICompletions: { supportsStreamingUsage: true },
},
},
};
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
source: "provided",
snapshot: index,
diagnostics: [],
});
loadPluginManifestRegistryForInstalledIndex.mockReturnValue(registry);
const snapshot = loadPluginMetadataSnapshot({ config: {}, env: {}, index });
expect(snapshot.owners.providerEndpoints).toContainEqual({
endpointClass: "openai-public",
hosts: ["api.example.com"],
hostSuffixes: [],
baseUrls: ["https://api.example.com/v1"],
});
expect(snapshot.owners.providerRequests?.get("demo")).toEqual({
family: "demo-family",
compatibilityFamily: "moonshot",
openAICompletions: { supportsStreamingUsage: true },
});
});
it("freezes a cloned index instead of caller-owned records", () => {
const index = makeIndex();
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
source: "provided",
snapshot: index,
diagnostics: [],
});
const snapshot = loadPluginMetadataSnapshot({ config: {}, env: {}, index });
const callerRecord = index.plugins[0];
const snapshotRecord = snapshot.index.plugins[0];
if (!callerRecord || !snapshotRecord) {
throw new Error("expected metadata records");
}
callerRecord.pluginId = "caller-mutated";
expect(snapshotRecord.pluginId).toBe("demo");
expect(() => {
snapshotRecord.pluginId = "snapshot-mutated";
}).toThrow();
});
});
+46 -360
View File
@@ -1,20 +1,12 @@
import fs from "node:fs";
import path from "node:path";
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { resolveIsNixMode } from "../config/paths.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
getActiveDiagnosticsTimelineSpan,
measureDiagnosticsTimelineSpanSync,
} from "../infra/diagnostics-timeline.js";
import { resolveUserPath } from "../utils.js";
import { resolveCompatibilityHostVersion } from "../version.js";
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
import { resolveDefaultPluginNpmDir, resolvePluginNpmProjectsDir } from "./install-paths.js";
import { hashJson } from "./installed-plugin-index-hash.js";
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
import { readPersistedInstalledPluginIndexSync } from "./installed-plugin-index-store.js";
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
import {
loadPluginManifestRegistryForInstalledIndex,
@@ -22,7 +14,6 @@ import {
} from "./manifest-registry-installed.js";
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
import { resolvePluginControlPlaneFingerprint } from "./plugin-control-plane-context.js";
import { registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle.js";
import { buildPluginMetadataProviderFacts } from "./plugin-metadata-provider-facts.js";
import type {
LoadPluginMetadataSnapshotParams,
@@ -31,37 +22,10 @@ import type {
ResolvePluginMetadataSnapshotParams,
} from "./plugin-metadata-snapshot.types.js";
import { createPluginRegistryIdNormalizer } from "./plugin-registry-id-normalizer.js";
import {
loadPluginRegistrySnapshotWithMetadata,
type PluginRegistrySnapshotSource,
} from "./plugin-registry.js";
import { loadPluginRegistrySnapshotWithMetadata } from "./plugin-registry.js";
import { normalizePluginIdScope, serializePluginIdScope } from "./plugin-scope.js";
import { fileFingerprint } from "./plugin-snapshot-fingerprint.js";
type PluginMetadataSnapshotMemo = {
key: string;
lookupContextHash: string;
registryState?: PersistedRegistryMemoState;
snapshot: PluginMetadataSnapshot;
};
type PersistedRegistryMemoState = {
contextHash: string;
fastHash: string;
fingerprint: unknown;
};
const MAX_PLUGIN_METADATA_SNAPSHOT_MEMOS = 8;
let pluginMetadataSnapshotMemos: PluginMetadataSnapshotMemo[] = [];
export function clearLoadPluginMetadataSnapshotMemo(): void {
pluginMetadataSnapshotMemos = [];
}
registerPluginMetadataProcessMemoLifecycleClear(clearLoadPluginMetadataSnapshotMemo);
const MEMO_RELEVANT_ENV_KEYS = [
const PLUGIN_METADATA_ENV_KEYS = [
"APPDATA",
"HOME",
"OPENCLAW_BUNDLED_PLUGINS_DIR",
@@ -86,47 +50,17 @@ export type {
ResolvePluginMetadataSnapshotParams,
} from "./plugin-metadata-snapshot.types.js";
function directoryChildPackageJsonFingerprint(directoryPath: string): unknown {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(directoryPath, { withFileTypes: true });
} catch {
return [directoryPath, "missing"];
}
return [
directoryPath,
...entries
.filter((entry) => entry.isDirectory())
.toSorted((a, b) => a.name.localeCompare(b.name))
.map((entry) => fileFingerprint(path.join(directoryPath, entry.name, "package.json"))),
];
}
function stableMemoValue(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(stableMemoValue);
}
if (!isRecord(value)) {
return value;
}
function pickPluginMetadataEnv(env: NodeJS.ProcessEnv): Record<string, string> {
return Object.fromEntries(
Object.entries(value)
.toSorted(([left], [right]) => left.localeCompare(right))
.map(([key, entry]) => [key, stableMemoValue(entry)]),
);
}
function pickMemoRelevantEnv(env: NodeJS.ProcessEnv): Record<string, string> {
return Object.fromEntries(
MEMO_RELEVANT_ENV_KEYS.flatMap((key) => {
PLUGIN_METADATA_ENV_KEYS.flatMap((key) => {
const value = env[key];
return value === undefined ? [] : [[key, value]];
}),
);
}
export function resolvePluginMetadataSnapshotMemoEnvFingerprint(env: NodeJS.ProcessEnv): string {
return hashJson(pickMemoRelevantEnv(env));
export function resolvePluginMetadataEnvFingerprint(env: NodeJS.ProcessEnv): string {
return hashJson(pickPluginMetadataEnv(env));
}
function throwReadonlyPluginMetadataMutation(): never {
@@ -174,195 +108,6 @@ function freezePluginMetadataSnapshot(snapshot: PluginMetadataSnapshot): PluginM
return freezeSnapshotValue(snapshot);
}
function resolvePersistedRegistryFastMemoFingerprint(params: {
env: NodeJS.ProcessEnv;
preferPersisted?: boolean;
stateDir?: string;
}): Record<string, unknown> {
const disabled = params.preferPersisted === false;
if (disabled) {
return { disabled: true };
}
const npmRoot = params.stateDir
? path.join(params.stateDir, "npm")
: resolveDefaultPluginNpmDir(params.env);
return {
index: hashJson(
stableMemoValue(
readPersistedInstalledPluginIndexSync({
env: params.env,
...(params.stateDir ? { stateDir: params.stateDir } : {}),
}),
) ?? null,
),
npmPackageJson: fileFingerprint(path.join(npmRoot, "package.json")),
npmProjectPackageJsons: directoryChildPackageJsonFingerprint(
resolvePluginNpmProjectsDir(npmRoot),
),
};
}
function resolvePersistedRegistryMemoContextHash(params: {
env: NodeJS.ProcessEnv;
fastFingerprint: unknown;
preferPersisted?: boolean;
stateDir?: string;
}): string {
return hashJson({
env: pickMemoRelevantEnv(params.env),
fastFingerprint: params.fastFingerprint,
preferPersisted: params.preferPersisted ?? null,
stateDir: params.stateDir ?? null,
});
}
function resolvePersistedRegistryMemoLookupContextHash(params: {
env: NodeJS.ProcessEnv;
preferPersisted?: boolean;
stateDir?: string;
}): string {
return hashJson({
env: pickMemoRelevantEnv(params.env),
preferPersisted: params.preferPersisted ?? null,
stateDir: params.stateDir ?? null,
});
}
function resolvePersistedRegistryMemoState(params: {
env: NodeJS.ProcessEnv;
preferPersisted?: boolean;
stateDir?: string;
}): PersistedRegistryMemoState {
const fastFingerprint = resolvePersistedRegistryFastMemoFingerprint(params);
const fastHash = hashJson(fastFingerprint);
const contextHash = resolvePersistedRegistryMemoContextHash({
...params,
fastFingerprint,
});
if (isRecord(fastFingerprint) && fastFingerprint.disabled === true) {
return {
contextHash,
fastHash,
fingerprint: fastFingerprint,
};
}
const index = readPersistedInstalledPluginIndexSync({
env: params.env,
...(params.stateDir ? { stateDir: params.stateDir } : {}),
});
return {
contextHash,
fastHash,
fingerprint: {
...fastFingerprint,
indexHash: hashJson(stableMemoValue(index) ?? null),
},
};
}
function resolvePersistedRegistryMemoStateForLookup(
params: {
env: NodeJS.ProcessEnv;
preferPersisted?: boolean;
stateDir?: string;
},
memos: readonly PluginMetadataSnapshotMemo[],
): PersistedRegistryMemoState {
const lookupContextHash = resolvePersistedRegistryMemoLookupContextHash(params);
for (const memo of memos) {
if (memo.lookupContextHash === lookupContextHash && memo.registryState) {
// Gateway runtime metadata is process-stable. Installs/reloads clear the
// memo lifecycle explicitly, so hot lookups can reuse the prepared
// registry stamp instead of re-statting plugin roots on every turn.
return memo.registryState;
}
}
const fastFingerprint = resolvePersistedRegistryFastMemoFingerprint(params);
const fastHash = hashJson(fastFingerprint);
const contextHash = resolvePersistedRegistryMemoContextHash({
...params,
fastFingerprint,
});
for (const memo of memos) {
const registryState = memo.registryState;
if (
registryState &&
registryState.contextHash === contextHash &&
registryState.fastHash === fastHash
) {
// Plugin files are immutable for a running gateway; plugin edits require
// an explicit reload/restart, so hot lookups only validate the registry envelope.
return registryState;
}
}
return resolvePersistedRegistryMemoState(params);
}
function resolveProvidedIndexMemoState(index: InstalledPluginIndex): PersistedRegistryMemoState {
const fingerprint = {
providedIndex: resolveInstalledManifestRegistryIndexFingerprint(index),
};
const fingerprintHash = hashJson(fingerprint);
return {
contextHash: fingerprintHash,
fastHash: fingerprintHash,
fingerprint,
};
}
function findPluginMetadataSnapshotMemo(key: string): PluginMetadataSnapshotMemo | undefined {
const index = pluginMetadataSnapshotMemos.findIndex((memo) => memo.key === key);
if (index === -1) {
return undefined;
}
const [memo] = pluginMetadataSnapshotMemos.splice(index, 1);
if (!memo) {
return undefined;
}
pluginMetadataSnapshotMemos.unshift(memo);
return memo;
}
function rememberPluginMetadataSnapshotMemo(memo: PluginMetadataSnapshotMemo): void {
pluginMetadataSnapshotMemos = [
memo,
...pluginMetadataSnapshotMemos.filter((existing) => existing.key !== memo.key),
].slice(0, MAX_PLUGIN_METADATA_SNAPSHOT_MEMOS);
}
function computePluginMetadataSnapshotMemoKey(params: {
params: LoadPluginMetadataSnapshotParams;
registryState: PersistedRegistryMemoState;
}): string {
const { params: snapshotParams, registryState } = params;
const env = snapshotParams.env ?? process.env;
const indexFingerprint = snapshotParams.index
? resolveInstalledManifestRegistryIndexFingerprint(snapshotParams.index)
: undefined;
return hashJson({
controlPlane: resolvePluginControlPlaneFingerprint({
config: snapshotParams.config,
env,
workspaceDir: snapshotParams.workspaceDir,
policyHash: resolveInstalledPluginIndexPolicyHash(snapshotParams.config),
...(indexFingerprint ? { inventoryFingerprint: indexFingerprint } : {}),
}),
cwd: process.cwd(),
env: pickMemoRelevantEnv(env),
index: indexFingerprint ?? null,
pathPolicy: {
compatibilityHostVersion: resolveCompatibilityHostVersion(env),
nixMode: resolveIsNixMode(env),
},
pluginIds: serializePluginIdScope(normalizePluginIdScope(snapshotParams.pluginIds)),
pluginIdScopeKey: snapshotParams.pluginIdScope?.key ?? null,
preferPersisted: snapshotParams.preferPersisted ?? null,
registry: registryState.fingerprint,
stateDir: snapshotParams.stateDir ? resolveUserPath(snapshotParams.stateDir, env) : null,
workspaceDir: snapshotParams.workspaceDir ?? null,
});
}
function resolvePluginMetadataControlPlaneFingerprint(
params: Pick<LoadPluginMetadataSnapshotParams, "config" | "env" | "workspaceDir"> & {
index?: InstalledPluginIndex;
@@ -550,80 +295,25 @@ export function listPluginOriginsFromMetadataSnapshot(
return new Map(snapshot.plugins.map((record) => [record.id, record.origin]));
}
// Process-local memoization keeps the hot snapshot work cached while checking
// the persisted metadata files that the installed-index loader consumes.
export function loadPluginMetadataSnapshot(
params: LoadPluginMetadataSnapshotParams,
): PluginMetadataSnapshot {
const activeTimelineSpan = getActiveDiagnosticsTimelineSpan();
const env = params.env ?? process.env;
const registryState = params.index
? resolveProvidedIndexMemoState(params.index)
: resolvePersistedRegistryMemoStateForLookup(
{
env,
...(params.stateDir ? { stateDir: resolveUserPath(params.stateDir, env) } : {}),
...(params.preferPersisted !== undefined
? { preferPersisted: params.preferPersisted }
: {}),
return freezePluginMetadataSnapshot(
measureDiagnosticsTimelineSpanSync(
"plugins.metadata.scan",
() => loadPluginMetadataSnapshotImpl(params),
{
phase: activeTimelineSpan?.phase ?? "startup",
config: params.config,
env: params.env,
attributes: {
hasWorkspaceDir: params.workspaceDir !== undefined,
hasInstalledIndex: params.index !== undefined,
},
pluginMetadataSnapshotMemos,
);
const memoKey = computePluginMetadataSnapshotMemoKey({ params, registryState });
const memo = findPluginMetadataSnapshotMemo(memoKey);
if (memo?.key === memoKey) {
return memo.snapshot;
}
const result = measureDiagnosticsTimelineSpanSync(
"plugins.metadata.scan",
() => loadPluginMetadataSnapshotImpl(params),
{
phase: activeTimelineSpan?.phase ?? "startup",
config: params.config,
env: params.env,
attributes: {
hasWorkspaceDir: params.workspaceDir !== undefined,
hasInstalledIndex: params.index !== undefined,
},
},
),
);
const snapshot = freezePluginMetadataSnapshot(result.snapshot);
if (canMemoizePluginMetadataSnapshotResult(result)) {
// Store under the exact key this call looked up by. Derived registries used
// to re-key off the freshly built snapshot.index, so the store key never
// matched the next lookup and every call re-ran the full manifest scan.
rememberPluginMetadataSnapshotMemo({
key: memoKey,
lookupContextHash: resolvePersistedRegistryMemoLookupContextHash({
env,
...(params.stateDir ? { stateDir: resolveUserPath(params.stateDir, env) } : {}),
...(params.preferPersisted !== undefined
? { preferPersisted: params.preferPersisted }
: {}),
}),
registryState,
snapshot,
});
}
return snapshot;
}
function canMemoizePluginMetadataSnapshotResult(result: {
registrySource: PluginRegistrySnapshotSource;
snapshot: PluginMetadataSnapshot;
}): boolean {
const snapshot = result.snapshot;
const hasCompleteSnapshotShape =
Array.isArray(snapshot.plugins) &&
Array.isArray(snapshot.diagnostics) &&
Array.isArray(snapshot.registryDiagnostics) &&
Array.isArray(snapshot.manifestRegistry.plugins) &&
Array.isArray(snapshot.manifestRegistry.diagnostics) &&
Array.isArray(snapshot.index.plugins) &&
Array.isArray(snapshot.index.diagnostics);
const hasPluginMetadata = snapshot.plugins.length > 0 || snapshot.index.plugins.length > 0;
return hasCompleteSnapshotShape && hasPluginMetadata;
}
export function resolvePluginMetadataSnapshot(
@@ -668,10 +358,9 @@ export function resolvePluginMetadataSnapshot(
return loadPluginMetadataSnapshot(params);
}
function loadPluginMetadataSnapshotImpl(params: LoadPluginMetadataSnapshotParams): {
snapshot: PluginMetadataSnapshot;
registrySource: PluginRegistrySnapshotSource;
} {
function loadPluginMetadataSnapshotImpl(
params: LoadPluginMetadataSnapshotParams,
): PluginMetadataSnapshot {
const totalStartedAt = performance.now();
const registryStartedAt = performance.now();
const registryResult = loadPluginRegistrySnapshotWithMetadata({
@@ -716,36 +405,33 @@ function loadPluginMetadataSnapshotImpl(params: LoadPluginMetadataSnapshotParams
const totalMs = performance.now() - totalStartedAt;
return {
policyHash: index.policyHash,
registrySource: registryResult.source,
snapshot: {
policyHash: index.policyHash,
registrySource: registryResult.source,
configFingerprint: resolvePluginMetadataControlPlaneFingerprint({
config: params.config,
env: params.env,
index,
policyHash: index.policyHash,
workspaceDir: params.workspaceDir,
}),
...(pluginIds !== undefined ? { pluginIds } : {}),
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
configFingerprint: resolvePluginMetadataControlPlaneFingerprint({
config: params.config,
env: params.env,
index,
registryDiagnostics: registryResult.diagnostics,
manifestRegistry,
plugins: manifestRegistry.plugins,
diagnostics: manifestRegistry.diagnostics,
byPluginId,
normalizePluginId,
owners,
metrics: {
registrySnapshotMs,
manifestRegistryMs,
ownerMapsMs,
totalMs,
indexPluginCount: index.plugins.length,
manifestPluginCount: manifestRegistry.plugins.length,
},
discovery: registryResult.discovery,
policyHash: index.policyHash,
workspaceDir: params.workspaceDir,
}),
...(pluginIds !== undefined ? { pluginIds } : {}),
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
index,
registryDiagnostics: registryResult.diagnostics,
manifestRegistry,
plugins: manifestRegistry.plugins,
diagnostics: manifestRegistry.diagnostics,
byPluginId,
normalizePluginId,
owners,
metrics: {
registrySnapshotMs,
manifestRegistryMs,
ownerMapsMs,
totalMs,
indexPluginCount: index.plugins.length,
manifestPluginCount: manifestRegistry.plugins.length,
},
discovery: registryResult.discovery,
};
}
@@ -9,10 +9,7 @@ import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js";
import {
clearLoadPluginMetadataSnapshotMemo,
loadPluginMetadataSnapshot,
} from "./plugin-metadata-snapshot.js";
import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
import { resetPluginRuntimeStateForTest } from "./runtime.js";
// Mock the persisted-registry loaders so direct metadata loads are observable.
@@ -124,7 +121,6 @@ describe("provider runtime consults the current plugin metadata snapshot", () =>
beforeEach(() => {
resetPluginRuntimeStateForTest();
clearPluginMetadataLifecycleCaches();
clearLoadPluginMetadataSnapshotMemo();
clearCurrentPluginMetadataSnapshot();
loadPluginRegistrySnapshotWithMetadata.mockReset();
loadPluginManifestRegistryForInstalledIndex.mockReset();
@@ -134,7 +130,6 @@ describe("provider runtime consults the current plugin metadata snapshot", () =>
afterEach(() => {
clearCurrentPluginMetadataSnapshot();
clearPluginMetadataLifecycleCaches();
clearLoadPluginMetadataSnapshotMemo();
resetPluginRuntimeStateForTest();
});
+2 -2
View File
@@ -15,7 +15,7 @@ import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-re
import { createPluginCacheKey, PluginLruCache } from "./plugin-cache-primitives.js";
import { resolvePluginControlPlaneFingerprint } from "./plugin-control-plane-context.js";
import { registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle.js";
import { resolvePluginMetadataSnapshotMemoEnvFingerprint } from "./plugin-metadata-snapshot.js";
import { resolvePluginMetadataEnvFingerprint } from "./plugin-metadata-snapshot.js";
import { getCachedPluginModuleLoader } from "./plugin-module-loader-cache.js";
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
import type { PluginRuntime } from "./runtime/types.js";
@@ -360,7 +360,7 @@ function resolveSetupRegistryCacheKey(params?: {
env,
workspaceDir: params?.workspaceDir,
}),
resolvePluginMetadataSnapshotMemoEnvFingerprint(env),
resolvePluginMetadataEnvFingerprint(env),
resolveCurrentSetupSnapshotCacheId(),
process.cwd(),
params?.pluginIds ? [...params.pluginIds].toSorted() : null,
@@ -15,7 +15,6 @@ let loadOpenClawPluginsMock: ReturnType<typeof vi.fn>;
let setActivePluginRegistry: RuntimeModule["setActivePluginRegistry"];
let resetPluginRuntimeStateForTest: RuntimeModule["resetPluginRuntimeStateForTest"];
let resolvePluginWebFetchProviders: WebFetchProvidersRuntimeModule["resolvePluginWebFetchProviders"];
let clearLoadPluginMetadataSnapshotMemo: typeof import("./plugin-metadata-snapshot.js").clearLoadPluginMetadataSnapshotMemo;
const DEFAULT_WORKSPACE = "/tmp/workspace";
@@ -121,12 +120,10 @@ describe("resolvePluginWebFetchProviders", () => {
manifestRegistryModule = await import("./manifest-registry.js");
webFetchProvidersSharedModule = await import("./web-fetch-providers.shared.js");
({ resetPluginRuntimeStateForTest, setActivePluginRegistry } = await import("./runtime.js"));
({ clearLoadPluginMetadataSnapshotMemo } = await import("./plugin-metadata-snapshot.js"));
({ resolvePluginWebFetchProviders } = await import("./web-fetch-providers.runtime.js"));
});
beforeEach(() => {
clearLoadPluginMetadataSnapshotMemo();
vi.spyOn(manifestRegistryModule, "loadPluginManifestRegistry").mockReturnValue(
createManifestRegistryFixture() as ManifestRegistryModule["loadPluginManifestRegistry"] extends (
...args: unknown[]
@@ -146,7 +143,6 @@ describe("resolvePluginWebFetchProviders", () => {
afterEach(() => {
resetPluginRuntimeStateForTest();
clearLoadPluginMetadataSnapshotMemo();
vi.restoreAllMocks();
});
@@ -38,7 +38,6 @@ let loaderModule: typeof import("./loader.js");
let pluginAutoEnableModule: PluginAutoEnableModule;
let applyPluginAutoEnableSpy: ReturnType<typeof vi.fn>;
let resetPluginRuntimeStateForTest: RuntimeModule["resetPluginRuntimeStateForTest"];
let clearLoadPluginMetadataSnapshotMemo: typeof import("./plugin-metadata-snapshot.js").clearLoadPluginMetadataSnapshotMemo;
const DEFAULT_WEB_SEARCH_WORKSPACE = "/tmp/workspace";
const EXPECTED_BUNDLED_RUNTIME_WEB_SEARCH_PROVIDER_KEYS = [
@@ -325,12 +324,10 @@ describe("resolvePluginWebSearchProviders", () => {
loaderModule = await import("./loader.js");
pluginAutoEnableModule = await import("../config/plugin-auto-enable.js");
({ resetPluginRuntimeStateForTest, setActivePluginRegistry } = await import("./runtime.js"));
({ clearLoadPluginMetadataSnapshotMemo } = await import("./plugin-metadata-snapshot.js"));
({ resolvePluginWebSearchProviders } = await import("./web-search-providers.runtime.js"));
});
beforeEach(() => {
clearLoadPluginMetadataSnapshotMemo();
applyPluginAutoEnableSpy?.mockRestore();
applyPluginAutoEnableSpy = vi
.spyOn(pluginAutoEnableModule, "applyPluginAutoEnable")
@@ -359,7 +356,6 @@ describe("resolvePluginWebSearchProviders", () => {
afterEach(() => {
resetPluginRuntimeStateForTest();
clearLoadPluginMetadataSnapshotMemo();
vi.restoreAllMocks();
});
@@ -6,6 +6,7 @@ export function createAgentsEmbeddedVitestConfig(env?: Record<string, string | u
return createScopedVitestConfig(agentsEmbeddedTestPatterns, {
dir: "src/agents",
env,
fileParallelism: false,
name: "agents-embedded-agent",
});
}
@@ -6,6 +6,7 @@ export function createAutoReplyReplyVitestConfig(env?: Record<string, string | u
return createScopedVitestConfig([...autoReplyReplySubtreeTestInclude], {
dir: "src/auto-reply",
env,
fileParallelism: false,
name: "auto-reply-reply",
});
}