mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
fix: prevent plugin metadata rescans from stalling Gateway (#125596)
* fix(plugins): preserve gateway metadata ownership Generic runtime contexts no longer displace the Gateway lifecycle snapshot, preventing repeated synchronous plugin inventory validation on session hot paths. * fix(gateway): reuse lifecycle plugin generation for turns * test(agents): align static runtime lifecycle expectations * test(gateway): refresh minimal runtime fixtures * test(runtime): pass lifecycle metadata explicitly * fix(skills): reuse gateway metadata for commands * test(gateway): prepare reply runtime for agent RPCs * style(agents): format rebased runtime preparation * test(gateway): prepare runtime for direct agent RPCs * test(gateway): provide direct agent runtime fixture * test(gateway): prepare chat send reply runtime * test(qa): await descendant process exit
This commit is contained in:
committed by
GitHub
parent
f95b5a0062
commit
fd216cb550
@@ -2012,7 +2012,6 @@ src/agents/run-timeout-attribution.ts 1
|
||||
src/agents/run-wait.ts 8
|
||||
src/agents/runtime-plan/build.ts 4
|
||||
src/agents/runtime-plan/tools.ts 5
|
||||
src/agents/runtime-plugins.ts 1
|
||||
src/agents/runtime/proxy.ts 7
|
||||
src/agents/sandbox-paths.ts 1
|
||||
src/agents/sandbox-tool-policy.ts 1
|
||||
|
||||
@@ -153,7 +153,7 @@ describe.skipIf(process.platform === "win32")("qa scenario command real POSIX li
|
||||
if (descendantPid === undefined) {
|
||||
throw new Error("scenario command descendant did not expose its pid");
|
||||
}
|
||||
expect(isProcessRunning(descendantPid)).toBe(false);
|
||||
await waitForProcessExit(descendantPid);
|
||||
} finally {
|
||||
if (descendantPid && isProcessRunning(descendantPid)) {
|
||||
process.kill(descendantPid, "SIGKILL");
|
||||
|
||||
@@ -112,6 +112,9 @@ const state = vi.hoisted(() => ({
|
||||
resolveSupportedThinkingLevelMock: vi.fn(({ level }: { level?: string }) => level),
|
||||
resolveThinkingDefaultMock: vi.fn((_args: unknown) => "low"),
|
||||
loadManifestModelCatalogMock: vi.fn(() => []),
|
||||
manifestMetadataSnapshot: { plugins: [] },
|
||||
resolvePluginMetadataSnapshotMock: vi.fn(),
|
||||
listSkillCommandsForWorkspaceMock: vi.fn((_params: unknown) => []),
|
||||
loadProviderScopedThinkingCatalogMock: vi.fn(
|
||||
async (_params: unknown): Promise<ModelCatalogSnapshot["entries"] | undefined> => undefined,
|
||||
),
|
||||
@@ -374,7 +377,16 @@ vi.mock("./agent-runtime-config.js", () => {
|
||||
|
||||
vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({
|
||||
isPluginMetadataSnapshotCompatible: () => false,
|
||||
resolvePluginMetadataSnapshot: () => ({ plugins: [] }),
|
||||
resolvePluginMetadataSnapshot: (...args: unknown[]) =>
|
||||
state.resolvePluginMetadataSnapshotMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../skills/discovery/chat-commands.runtime.js", () => ({
|
||||
expandExplicitSkillReferences: ({ text }: { text: string }) => ({ body: text, skills: [] }),
|
||||
hasSkillReferenceCandidate: () => true,
|
||||
listSkillCommandsForWorkspace: (params: unknown) =>
|
||||
state.listSkillCommandsForWorkspaceMock(params),
|
||||
resolveEffectiveAgentSkillFilter: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock("../config/runtime-snapshot.js", () => ({
|
||||
@@ -727,11 +739,13 @@ vi.mock("../acp/control-plane/manager.js", () => ({
|
||||
|
||||
let agentCommand: typeof import("./agent-command.js").agentCommand;
|
||||
let agentCommandFromSystem: typeof import("./agent-command.js").agentCommandFromSystem;
|
||||
let prepareAgentCommandExecution: typeof import("./command/prepare.js").prepareAgentCommandExecution;
|
||||
|
||||
beforeAll(async () => {
|
||||
const mod = await import("./agent-command.js");
|
||||
agentCommand ??= mod.agentCommand;
|
||||
agentCommandFromSystem ??= mod.agentCommandFromSystem;
|
||||
({ prepareAgentCommandExecution } = await import("./command/prepare.js"));
|
||||
});
|
||||
|
||||
type FallbackRunnerParams = {
|
||||
@@ -950,6 +964,7 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
state.resolveThinkingDefaultMock.mockReturnValue("low");
|
||||
state.resolveAgentSkillsFilterMock.mockReturnValue(undefined);
|
||||
state.loadManifestModelCatalogMock.mockReturnValue([]);
|
||||
state.resolvePluginMetadataSnapshotMock.mockReturnValue(state.manifestMetadataSnapshot);
|
||||
state.loadProviderScopedThinkingCatalogMock.mockReset().mockResolvedValue(undefined);
|
||||
state.loadFullModelCatalogMock.mockClear();
|
||||
state.loadPreparedModelCatalogSnapshotMock.mockResolvedValue({
|
||||
@@ -1120,6 +1135,25 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("uses Gateway command metadata without resolving the agent workspace", async () => {
|
||||
const pluginGeneration = {
|
||||
pluginMetadataSnapshot: state.manifestMetadataSnapshot,
|
||||
} as never;
|
||||
|
||||
const prepared = await prepareAgentCommandExecution(
|
||||
{ message: "/demo", to: "+1234567890" },
|
||||
{} as never,
|
||||
{ config: {}, pluginGeneration },
|
||||
);
|
||||
|
||||
expect(prepared.manifestMetadataSnapshot).toBe(state.manifestMetadataSnapshot);
|
||||
expect(prepared.commandRuntimeContext?.pluginGeneration).toBe(pluginGeneration);
|
||||
expect(state.listSkillCommandsForWorkspaceMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pluginMetadataSnapshot: state.manifestMetadataSnapshot }),
|
||||
);
|
||||
expect(state.resolvePluginMetadataSnapshotMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retries with the switched provider/model when LiveSessionModelSwitchError is thrown", async () => {
|
||||
setupModelSwitchRetry({
|
||||
provider: "openai",
|
||||
@@ -3706,6 +3740,9 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
|
||||
if (allowlisted) {
|
||||
expect(state.loadManifestModelCatalogMock).toHaveBeenCalledTimes(1);
|
||||
expect(state.loadManifestModelCatalogMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ metadataSnapshot: state.manifestMetadataSnapshot }),
|
||||
);
|
||||
}
|
||||
const thinkingArgs = requireRecord(
|
||||
mockCallArg(state.isThinkingLevelSupportedMock),
|
||||
|
||||
+54
-29
@@ -19,6 +19,7 @@ import {
|
||||
import { clearAgentRunContext } from "../infra/agent-run-registry.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { withPluginRuntimeGenerationScope } from "../plugins/runtime/generation-scope.js";
|
||||
import { isSubagentSessionKey } from "../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { isAgentMediatedCompletionSourceTool } from "../sessions/input-provenance.js";
|
||||
@@ -46,7 +47,10 @@ import { persistAgentSession } from "./command/attempt-execution.shared.js";
|
||||
import { emitIngressModelUsageDiagnostic } from "./command/ingress-diagnostics.js";
|
||||
import { resolveEmbeddedModelSelection } from "./command/model-selection.js";
|
||||
import { finalizeEmbeddedAgentCommand } from "./command/post-run.js";
|
||||
import { prepareAgentCommandExecution } from "./command/prepare.js";
|
||||
import {
|
||||
prepareAgentCommandExecution,
|
||||
type PreparedAgentCommandRuntimeContext,
|
||||
} from "./command/prepare.js";
|
||||
import { runEmbeddedAgentAttempt } from "./command/run-embedded-attempt.js";
|
||||
import { loadSessionStoreRuntime, resolveAgentCommandDeps } from "./command/runtime-loaders.js";
|
||||
import { prepareCurrentRunDelivery } from "./command/session-helpers.js";
|
||||
@@ -457,6 +461,9 @@ async function agentCommandInternal(
|
||||
persistedVerbose,
|
||||
verboseDefault: agentCfg?.verboseDefault as VerboseLevel | undefined,
|
||||
sessionStateActor,
|
||||
...(manifestMetadataSnapshot
|
||||
? { pluginMetadataSnapshot: manifestMetadataSnapshot }
|
||||
: {}),
|
||||
}),
|
||||
{ config: cfg },
|
||||
);
|
||||
@@ -634,47 +641,64 @@ async function agentCommandFromIngressInternal(
|
||||
recovery?: {
|
||||
restoreAdmittedRecovery?: () => Promise<MainSessionRecoveryPendingTarget | undefined>;
|
||||
},
|
||||
runtimeContext?: PreparedAgentCommandRuntimeContext,
|
||||
) {
|
||||
if (typeof opts.allowModelOverride !== "boolean") {
|
||||
throw new Error("allowModelOverride must be explicitly set for ingress agent runs.");
|
||||
}
|
||||
const lifecycleGeneration =
|
||||
opts.lifecycleGeneration ?? captureAgentRunLifecycleGeneration(opts.runId ?? "");
|
||||
return await withAgentRunLifecycleGeneration(lifecycleGeneration, async () => {
|
||||
let preparedAgentDir: string | undefined;
|
||||
const result = await runWithAgentCommandRecoveryOwner({
|
||||
lifecycleGeneration,
|
||||
mode: "claim",
|
||||
opts: {
|
||||
...opts,
|
||||
const generation = runtimeContext?.pluginGeneration;
|
||||
const executeIngress = () =>
|
||||
withAgentRunLifecycleGeneration(lifecycleGeneration, async () => {
|
||||
let preparedAgentDir: string | undefined;
|
||||
const result = await runWithAgentCommandRecoveryOwner({
|
||||
lifecycleGeneration,
|
||||
senderIsOwner: opts.senderIsOwner === true,
|
||||
},
|
||||
prepare: async (preparedOpts) => await prepareAgentCommandExecution(preparedOpts, runtime),
|
||||
restoreAdmittedRecovery: recovery?.restoreAdmittedRecovery,
|
||||
run: async (prepared) => {
|
||||
preparedAgentDir = prepared.agentDir;
|
||||
return await withAgentPluginRegistry({
|
||||
config: prepared.cfg,
|
||||
workspaceDir: prepared.workspaceDir,
|
||||
run: async () =>
|
||||
mode: "claim",
|
||||
opts: {
|
||||
...opts,
|
||||
lifecycleGeneration,
|
||||
senderIsOwner: opts.senderIsOwner === true,
|
||||
},
|
||||
prepare: async (preparedOpts) =>
|
||||
await prepareAgentCommandExecution(preparedOpts, runtime, runtimeContext),
|
||||
restoreAdmittedRecovery: recovery?.restoreAdmittedRecovery,
|
||||
run: async (prepared) => {
|
||||
preparedAgentDir = prepared.agentDir;
|
||||
const run = async () =>
|
||||
await agentCommandInternal(
|
||||
prepared,
|
||||
prepared.opts,
|
||||
{ kind: "api", boundary: "agent-command.from-ingress", state: "unknown" },
|
||||
runtime,
|
||||
deps,
|
||||
),
|
||||
});
|
||||
},
|
||||
);
|
||||
return generation
|
||||
? await run()
|
||||
: await withAgentPluginRegistry({
|
||||
config: prepared.cfg,
|
||||
workspaceDir: prepared.workspaceDir,
|
||||
run,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (result && preparedAgentDir) {
|
||||
emitIngressModelUsageDiagnostic(result, opts, preparedAgentDir);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
if (result && preparedAgentDir) {
|
||||
emitIngressModelUsageDiagnostic(result, opts, preparedAgentDir);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
return generation && runtimeContext
|
||||
? await withPluginRuntimeGenerationScope(
|
||||
{
|
||||
config: runtimeContext.config,
|
||||
metadataSnapshot: generation.pluginMetadataSnapshot,
|
||||
pluginRegistry: generation.pluginRegistry,
|
||||
},
|
||||
executeIngress,
|
||||
)
|
||||
: await executeIngress();
|
||||
}
|
||||
|
||||
/** Runs an agent turn from an inbound channel/gateway ingress context. */
|
||||
@@ -700,6 +724,7 @@ export async function agentCommandFromGatewayIngress(
|
||||
recovery: {
|
||||
restoreAdmittedRecovery?: () => Promise<MainSessionRecoveryPendingTarget | undefined>;
|
||||
},
|
||||
runtimeContext?: PreparedAgentCommandRuntimeContext,
|
||||
) {
|
||||
return await agentCommandFromIngressInternal(opts, runtime, deps, recovery);
|
||||
return await agentCommandFromIngressInternal(opts, runtime, deps, recovery, runtimeContext);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@ import {
|
||||
normalizePluginsConfigWithResolver,
|
||||
resolvePolicyPluginActivationState,
|
||||
} from "../plugins/config-policy.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import {
|
||||
isPluginMetadataSnapshotCompatible,
|
||||
loadPluginMetadataSnapshot,
|
||||
type PluginMetadataSnapshot,
|
||||
} from "../plugins/plugin-metadata-snapshot.js";
|
||||
@@ -47,28 +45,6 @@ function sanitizeProjectSettings(settings: AgentSettingsSnapshot): AgentSettings
|
||||
return sanitizeAgentSettingsSnapshot(settings);
|
||||
}
|
||||
|
||||
function canReuseUnscopedCurrentPluginMetadataSnapshot(config: OpenClawConfig): boolean {
|
||||
// Unscoped snapshots are only reusable when config does not introduce
|
||||
// workspace-local plugin load paths that would change the registry contents.
|
||||
return normalizePluginsConfigWithResolver(config.plugins).loadPaths.length === 0;
|
||||
}
|
||||
|
||||
function resolveUnscopedCurrentPluginMetadataSnapshot(params: {
|
||||
config: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
workspaceDir?: string;
|
||||
}): PluginMetadataSnapshot | undefined {
|
||||
if (!canReuseUnscopedCurrentPluginMetadataSnapshot(params.config)) {
|
||||
return undefined;
|
||||
}
|
||||
return getCurrentPluginMetadataSnapshot({
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
allowWorkspaceScopedSnapshot: true,
|
||||
requireDefaultDiscoveryContext: true,
|
||||
});
|
||||
}
|
||||
|
||||
function loadBundleSettingsFile(params: {
|
||||
rootDir: string;
|
||||
relativePath: string;
|
||||
@@ -111,29 +87,12 @@ export function loadEnabledBundleAgentSettingsSnapshot(params: {
|
||||
const env = params.env ?? process.env;
|
||||
const providedSnapshot = params.pluginMetadataSnapshot;
|
||||
const metadataSnapshot =
|
||||
providedSnapshot &&
|
||||
isPluginMetadataSnapshotCompatible({
|
||||
snapshot: providedSnapshot,
|
||||
providedSnapshot ??
|
||||
loadPluginMetadataSnapshot({
|
||||
workspaceDir,
|
||||
config,
|
||||
env,
|
||||
workspaceDir,
|
||||
})
|
||||
? providedSnapshot
|
||||
: (getCurrentPluginMetadataSnapshot({
|
||||
config,
|
||||
env,
|
||||
workspaceDir,
|
||||
}) ??
|
||||
resolveUnscopedCurrentPluginMetadataSnapshot({
|
||||
config,
|
||||
env,
|
||||
workspaceDir,
|
||||
}) ??
|
||||
loadPluginMetadataSnapshot({
|
||||
workspaceDir,
|
||||
config,
|
||||
env,
|
||||
}));
|
||||
});
|
||||
const registry = metadataSnapshot.manifestRegistry;
|
||||
if (registry.plugins.length === 0) {
|
||||
return {};
|
||||
|
||||
@@ -194,11 +194,10 @@ describe("loadEnabledBundleAgentSettingsSnapshot", () => {
|
||||
});
|
||||
|
||||
expect(snapshot.hideThinkingBlock).toBe(true);
|
||||
expect(pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible).toHaveBeenCalledOnce();
|
||||
expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to a fresh plugin metadata load for an incompatible snapshot", async () => {
|
||||
it("treats a supplied lifecycle snapshot as authoritative across workspaces", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-workspace-");
|
||||
const pluginRoot = await createWorkspaceBundle({ workspaceDir });
|
||||
await fs.writeFile(
|
||||
@@ -207,7 +206,7 @@ describe("loadEnabledBundleAgentSettingsSnapshot", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible.mockReturnValueOnce(false);
|
||||
pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mockClear();
|
||||
pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear();
|
||||
|
||||
const snapshot = loadEnabledBundleAgentSettingsSnapshot({
|
||||
@@ -220,7 +219,20 @@ describe("loadEnabledBundleAgentSettingsSnapshot", () => {
|
||||
},
|
||||
},
|
||||
pluginMetadataSnapshot: {
|
||||
manifestRegistry: { diagnostics: [], plugins: [] },
|
||||
workspaceDir: "/tmp/gateway-plugin-workspace",
|
||||
manifestRegistry: {
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "claude-bundle",
|
||||
format: "bundle",
|
||||
origin: "global",
|
||||
enabledByDefault: true,
|
||||
settingsFiles: ["settings.json"],
|
||||
rootDir: pluginRoot,
|
||||
},
|
||||
],
|
||||
},
|
||||
normalizePluginId: (id: string) => id.trim(),
|
||||
} as unknown as Parameters<
|
||||
typeof loadEnabledBundleAgentSettingsSnapshot
|
||||
@@ -228,147 +240,10 @@ describe("loadEnabledBundleAgentSettingsSnapshot", () => {
|
||||
});
|
||||
|
||||
expect(snapshot.hideThinkingBlock).toBe(true);
|
||||
expect(pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible).toHaveBeenCalledOnce();
|
||||
expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("reuses the current plugin metadata snapshot for bundle settings", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-workspace-");
|
||||
const pluginRoot = await createWorkspaceBundle({ workspaceDir });
|
||||
const resolvedPluginRoot = await fs.realpath(pluginRoot);
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, "settings.json"),
|
||||
JSON.stringify({ hideThinkingBlock: true }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mockReturnValueOnce({
|
||||
manifestRegistry: {
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "claude-bundle",
|
||||
origin: "workspace",
|
||||
format: "bundle",
|
||||
bundleFormat: "claude",
|
||||
settingsFiles: ["settings.json"],
|
||||
rootDir: resolvedPluginRoot,
|
||||
},
|
||||
],
|
||||
},
|
||||
normalizePluginId: (id: string) => id.trim(),
|
||||
});
|
||||
pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear();
|
||||
|
||||
const snapshot = loadEnabledBundleAgentSettingsSnapshot({
|
||||
cwd: workspaceDir,
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"claude-bundle": { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(snapshot.hideThinkingBlock).toBe(true);
|
||||
expect(pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot).not.toHaveBeenCalled();
|
||||
expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not reuse an unscoped current snapshot when plugin load paths change", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-workspace-");
|
||||
const pluginRoot = await createWorkspaceBundle({ workspaceDir });
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, "settings.json"),
|
||||
JSON.stringify({ hideThinkingBlock: true }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mockReturnValueOnce(undefined);
|
||||
pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear();
|
||||
|
||||
const snapshot = loadEnabledBundleAgentSettingsSnapshot({
|
||||
cwd: workspaceDir,
|
||||
cfg: {
|
||||
plugins: {
|
||||
load: { paths: ["/tmp/changed-plugin-root"] },
|
||||
entries: {
|
||||
"claude-bundle": { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(snapshot.hideThinkingBlock).toBe(true);
|
||||
expect(pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledOnce();
|
||||
const [snapshotLookup] =
|
||||
pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mock.calls[0] ?? [];
|
||||
expect(snapshotLookup?.config?.plugins?.load).toEqual({
|
||||
paths: ["/tmp/changed-plugin-root"],
|
||||
});
|
||||
expect(snapshotLookup?.env).toBe(process.env);
|
||||
expect(snapshotLookup?.workspaceDir).toBe(workspaceDir);
|
||||
expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not reuse a load-path current snapshot for a config with default load paths", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-workspace-");
|
||||
const pluginRoot = await createWorkspaceBundle({ workspaceDir });
|
||||
const resolvedPluginRoot = await fs.realpath(pluginRoot);
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, "settings.json"),
|
||||
JSON.stringify({ hideThinkingBlock: true }),
|
||||
"utf-8",
|
||||
);
|
||||
const staleSnapshot = {
|
||||
policyHash: "policy",
|
||||
manifestRegistry: {
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "claude-bundle",
|
||||
origin: "workspace",
|
||||
format: "bundle",
|
||||
bundleFormat: "claude",
|
||||
settingsFiles: ["settings.json"],
|
||||
rootDir: resolvedPluginRoot,
|
||||
},
|
||||
],
|
||||
},
|
||||
normalizePluginId: (id: string) => id.trim(),
|
||||
};
|
||||
pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mockImplementation(
|
||||
(params: { config?: unknown; requireDefaultDiscoveryContext?: boolean }) => {
|
||||
if (params.config || params.requireDefaultDiscoveryContext) {
|
||||
return undefined;
|
||||
}
|
||||
return staleSnapshot;
|
||||
},
|
||||
);
|
||||
pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear();
|
||||
|
||||
const snapshot = loadEnabledBundleAgentSettingsSnapshot({
|
||||
cwd: workspaceDir,
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"claude-bundle": { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(snapshot.hideThinkingBlock).toBe(true);
|
||||
expect(pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledTimes(2);
|
||||
expect(pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot).toHaveBeenLastCalledWith({
|
||||
env: process.env,
|
||||
workspaceDir,
|
||||
allowWorkspaceScopedSnapshot: true,
|
||||
requireDefaultDiscoveryContext: true,
|
||||
});
|
||||
expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("loads sanitized settings and MCP defaults from enabled bundle plugins", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-workspace-");
|
||||
const pluginRoot = await createWorkspaceBundle({ workspaceDir });
|
||||
|
||||
@@ -317,6 +317,7 @@ function makeRunAgentAttemptParams(overrides: RunAgentAttemptOverrides): RunAgen
|
||||
authProfileProvider: provider,
|
||||
sessionHasHistory: false,
|
||||
...overrides,
|
||||
pluginGeneration: overrides.pluginGeneration,
|
||||
preparedRunAdmission: overrides.preparedRunAdmission ?? createTestPreparedRunAdmission(runId),
|
||||
lifecycleGeneration: overrides.lifecycleGeneration ?? "test-generation",
|
||||
opts: { ...overrides.opts } as RunAgentAttemptParams["opts"],
|
||||
|
||||
@@ -76,6 +76,7 @@ import {
|
||||
} from "../cli-session.js";
|
||||
import { resolveConversationCapabilityProfile } from "../conversation-capability-profile.js";
|
||||
import { resolveConversationToolPolicies } from "../conversation-tool-policy-pipeline.js";
|
||||
import type { RunEmbeddedAgentInternalParams } from "../embedded-agent-runner/run/internal-params.js";
|
||||
import { runEmbeddedAgent, type EmbeddedAgentRunResult } from "../embedded-agent.js";
|
||||
import { appendGitCoauthorContext } from "../git-coauthor-attribution.js";
|
||||
import type { ContextEngineLogicalTurnLease } from "../harness/context-engine-logical-turn.js";
|
||||
@@ -85,6 +86,7 @@ import { resolveAvailableAgentHarnessPolicy } from "../harness/selection.js";
|
||||
import { resolveCliRuntimeExecutionProvider } from "../model-runtime-aliases.js";
|
||||
import { isCliProvider } from "../model-selection.js";
|
||||
import { resolveOpenAIRuntimeProvider } from "../openai-routing.js";
|
||||
import type { PreparedModelRuntimePluginGeneration } from "../prepared-model-runtime.types.js";
|
||||
import { hasVerifiedRequesterCompletionHandoff } from "../requester-tool-policy.js";
|
||||
import { resolveAgentRunAbortLifecycleFields } from "../run-termination.js";
|
||||
import { buildAgentRuntimeAuthPlan } from "../runtime-plan/auth.js";
|
||||
@@ -531,6 +533,7 @@ export function runAgentAttempt(params: {
|
||||
storePath?: string;
|
||||
pluginsEnabled?: boolean;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
pluginGeneration: PreparedModelRuntimePluginGeneration | undefined;
|
||||
allowTransientCooldownProbe?: boolean;
|
||||
modelFallbacksOverride?: string[];
|
||||
sessionHasHistory?: boolean;
|
||||
@@ -1126,7 +1129,7 @@ export function runAgentAttempt(params: {
|
||||
const embeddedPersistencePrompt = params.opts.gitCoauthorAttribution
|
||||
? (continuationTranscriptBody ?? effectivePrompt)
|
||||
: continuationTranscriptBody;
|
||||
const embeddedRunParams: Parameters<typeof runEmbeddedAgent>[0] = {
|
||||
const embeddedRunParams: RunEmbeddedAgentInternalParams = {
|
||||
preparedRunAdmission: params.preparedRunAdmission,
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
@@ -1159,6 +1162,7 @@ export function runAgentAttempt(params: {
|
||||
permissionMode: params.sessionEntry?.permissionMode,
|
||||
sessionRoot: params.sessionEntry?.sessionRoot,
|
||||
config: params.cfg,
|
||||
...(params.pluginGeneration ? { pluginGeneration: params.pluginGeneration } : {}),
|
||||
agentHarnessId: embeddedAgentHarnessOverride,
|
||||
modelSelectionLocked: !isRawModelRun && params.sessionEntry?.modelSelectionLocked === true,
|
||||
agentHarnessRuntimeOverride: embeddedAgentHarnessOverride,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { resolveChannelModelOverride } from "../../channels/model-overrides.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { requireActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { isSubagentSessionKey } from "../../routing/session-key.js";
|
||||
import { isValidAgentHarnessSessionStoreEntry } from "../../sessions/agent-harness-session-key.js";
|
||||
@@ -83,9 +84,7 @@ export async function resolveEmbeddedModelSelection(params: {
|
||||
sessionAgentId: string;
|
||||
workspaceDir: string;
|
||||
pluginsEnabled: boolean;
|
||||
manifestMetadataSnapshot?: NonNullable<
|
||||
Parameters<typeof resolveProviderIdForAuth>[1]
|
||||
>["metadataSnapshot"];
|
||||
manifestMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
modelManifestContext: ModelManifestNormalizationContext;
|
||||
configuredThinkingCatalog: ReturnType<typeof loadManifestModelCatalog>;
|
||||
requestedThinkLevel?: ThinkLevel;
|
||||
@@ -155,7 +154,11 @@ export async function resolveEmbeddedModelSelection(params: {
|
||||
Object.keys(agentModels ?? {}).length > 0;
|
||||
if (hasAllowlist || hasConfiguredModels) {
|
||||
modelCatalog = params.pluginsEnabled
|
||||
? loadManifestModelCatalog({ config: params.cfg, workspaceDir: params.workspaceDir })
|
||||
? loadManifestModelCatalog({
|
||||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
metadataSnapshot: params.manifestMetadataSnapshot,
|
||||
})
|
||||
: [];
|
||||
visibilityPolicy = createModelVisibilityPolicy({
|
||||
cfg: params.cfg,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { throwAgentRunRestartAbortReason } from "../run-termination.js";
|
||||
import { persistAssistantTranscriptRepairRecord } from "./assistant-transcript-repair.js";
|
||||
import { persistAgentSession } from "./attempt-execution.shared.js";
|
||||
import type { PreparedAgentCommandExecution } from "./prepare.js";
|
||||
import type { EmbeddedAgentAttempt } from "./run-embedded-attempt.js";
|
||||
import type { runEmbeddedAgentAttempt } from "./run-embedded-attempt.js";
|
||||
import {
|
||||
loadAgentRunnerMemoryRuntime,
|
||||
loadCliCompactionRuntime,
|
||||
@@ -35,6 +35,8 @@ import { clearPendingFinalDelivery } from "./session-helpers.js";
|
||||
import type { EmbeddedSessionState } from "./session-preparation.js";
|
||||
import type { AgentCommandOpts } from "./types.js";
|
||||
|
||||
type EmbeddedAgentAttempt = Awaited<ReturnType<typeof runEmbeddedAgentAttempt>>;
|
||||
|
||||
const log = createSubsystemLogger("agents/agent-command");
|
||||
|
||||
export async function finalizeEmbeddedAgentCommand(params: {
|
||||
|
||||
@@ -8,13 +8,11 @@ import {
|
||||
normalizeVerboseLevel,
|
||||
} from "../../auto-reply/thinking.js";
|
||||
import { formatCliCommand } from "../../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { resolveAgentExplicitRecipientSession } from "../../infra/outbound/agent-delivery.js";
|
||||
import { buildOutboundSessionContext } from "../../infra/outbound/session-context.js";
|
||||
import { normalizePluginsConfig } from "../../plugins/config-state.js";
|
||||
import {
|
||||
isPluginMetadataSnapshotCompatible,
|
||||
resolvePluginMetadataSnapshot,
|
||||
} from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import { resolvePluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import {
|
||||
classifySessionKeyShape,
|
||||
isUnscopedSessionKeySentinel,
|
||||
@@ -39,6 +37,7 @@ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
|
||||
import type { ModelManifestNormalizationContext } from "../model-ref-shared.js";
|
||||
import { buildConfiguredModelCatalog, resolveConfiguredModelRef } from "../model-selection.js";
|
||||
import type { PreparedModelRuntimePluginGeneration } from "../prepared-model-runtime.types.js";
|
||||
import { normalizeSpawnedRunMetadata } from "../spawned-context.js";
|
||||
import { resolveEffectiveAgentRuntime } from "../thinking-runtime.js";
|
||||
import { resolveAgentTimeoutMs } from "../timeout.js";
|
||||
@@ -85,7 +84,16 @@ export function normalizeExplicitOverrideInput(raw: string, kind: "provider" | "
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export async function prepareAgentCommandExecution(opts: AgentCommandOpts, runtime: RuntimeEnv) {
|
||||
export type PreparedAgentCommandRuntimeContext = Readonly<{
|
||||
config: OpenClawConfig;
|
||||
pluginGeneration: PreparedModelRuntimePluginGeneration;
|
||||
}>;
|
||||
|
||||
export async function prepareAgentCommandExecution(
|
||||
opts: AgentCommandOpts,
|
||||
runtime: RuntimeEnv,
|
||||
runtimeContext?: PreparedAgentCommandRuntimeContext,
|
||||
) {
|
||||
const isRawModelRun = opts.modelRun === true || opts.promptMode === "none";
|
||||
const message = opts.message ?? "";
|
||||
if (!message.trim()) {
|
||||
@@ -114,7 +122,7 @@ export async function prepareAgentCommandExecution(opts: AgentCommandOpts, runti
|
||||
);
|
||||
}
|
||||
|
||||
const { cfg, pluginMetadataSnapshot } = await resolveAgentRuntimeConfig(runtime, {
|
||||
const { cfg } = await resolveAgentRuntimeConfig(runtime, {
|
||||
runtimeTargetsChannelSecrets: opts.deliver === true,
|
||||
runtimeChannelSecretScope:
|
||||
opts.deliver !== true && shouldResolveExplicitRecipientSession && recipientChannel
|
||||
@@ -271,17 +279,10 @@ export async function prepareAgentCommandExecution(opts: AgentCommandOpts, runti
|
||||
normalizeOptionalString(opts.cwd) ?? normalizeOptionalString(sessionEntryRaw?.spawnedCwd);
|
||||
const agentDir = resolveAgentDir(cfg, sessionAgentId);
|
||||
const pluginsEnabled = normalizePluginsConfig(cfg.plugins).enabled;
|
||||
const preparedMetadataSnapshot = runtimeContext?.pluginGeneration.pluginMetadataSnapshot;
|
||||
const manifestMetadataSnapshot = pluginsEnabled
|
||||
? pluginMetadataSnapshot &&
|
||||
pluginMetadataSnapshot.pluginIds === undefined &&
|
||||
isPluginMetadataSnapshotCompatible({
|
||||
snapshot: pluginMetadataSnapshot,
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
workspaceDir,
|
||||
})
|
||||
? pluginMetadataSnapshot
|
||||
: resolvePluginMetadataSnapshot({ config: cfg, env: process.env, workspaceDir })
|
||||
? (preparedMetadataSnapshot ??
|
||||
resolvePluginMetadataSnapshot({ config: cfg, env: process.env, workspaceDir }))
|
||||
: undefined;
|
||||
const modelManifestContext = {
|
||||
manifestPlugins: manifestMetadataSnapshot?.plugins ?? [],
|
||||
@@ -376,6 +377,7 @@ export async function prepareAgentCommandExecution(opts: AgentCommandOpts, runti
|
||||
agentId: sessionAgentId,
|
||||
sessionEntry: sessionEntryRaw,
|
||||
sessionKey,
|
||||
...(preparedMetadataSnapshot ? { pluginMetadataSnapshot: preparedMetadataSnapshot } : {}),
|
||||
...(skillFilter ? { skillFilter } : {}),
|
||||
};
|
||||
const skillCommands = listSkillCommandsForWorkspace(commandParams);
|
||||
@@ -429,6 +431,7 @@ export async function prepareAgentCommandExecution(opts: AgentCommandOpts, runti
|
||||
agentDir,
|
||||
pluginsEnabled,
|
||||
manifestMetadataSnapshot,
|
||||
...(runtimeContext ? { commandRuntimeContext: runtimeContext } : {}),
|
||||
modelManifestContext,
|
||||
runId,
|
||||
isSubagentLane,
|
||||
|
||||
@@ -61,7 +61,6 @@ import { loadAttemptExecutionRuntime, type AgentAttemptResult } from "./runtime-
|
||||
import { resolveInternalSessionEffectsSource } from "./session-helpers.js";
|
||||
import type { EmbeddedSessionState } from "./session-preparation.js";
|
||||
import type { AgentCommandOpts } from "./types.js";
|
||||
|
||||
const log = createSubsystemLogger("agents/agent-command");
|
||||
const MAX_LIVE_SWITCH_RETRIES = 5;
|
||||
|
||||
@@ -495,6 +494,7 @@ export async function runEmbeddedAgentAttempt(params: {
|
||||
storePath: params.suppressVisibleSessionEffects ? undefined : storePath,
|
||||
pluginsEnabled,
|
||||
...(manifestMetadataSnapshot ? { metadataSnapshot: manifestMetadataSnapshot } : {}),
|
||||
pluginGeneration: params.prepared.commandRuntimeContext?.pluginGeneration,
|
||||
allowTransientCooldownProbe: runOptions?.allowTransientCooldownProbe,
|
||||
sessionHasHistory:
|
||||
!isNewSession ||
|
||||
@@ -511,7 +511,6 @@ export async function runEmbeddedAgentAttempt(params: {
|
||||
onUserMessagePersisted: attemptLifecycleCallbacks.onUserMessagePersisted,
|
||||
onLifecycleGenerationChanged: (nextLifecycleGeneration) => {
|
||||
lifecycleGeneration = nextLifecycleGeneration;
|
||||
// Outer cleanup owns the run context, so publish before the attempt can reject.
|
||||
params.onLifecycleGenerationChanged(nextLifecycleGeneration);
|
||||
},
|
||||
onAgentEvent: attemptLifecycleCallbacks.onAgentEvent,
|
||||
@@ -705,5 +704,3 @@ export async function runEmbeddedAgentAttempt(params: {
|
||||
terminal,
|
||||
};
|
||||
}
|
||||
|
||||
export type EmbeddedAgentAttempt = Awaited<ReturnType<typeof runEmbeddedAgentAttempt>>;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { assertAgentRunLifecycleGenerationCurrent } from "../../infra/agent-events.js";
|
||||
import { registerAgentRunContext } from "../../infra/agent-run-registry.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||
import { recordSessionHumanDirectMessage } from "../../sessions/session-state-events.js";
|
||||
import { resolveEffectiveAgentSkillFilter } from "../../skills/discovery/agent-filter.js";
|
||||
@@ -34,6 +35,7 @@ export async function prepareEmbeddedSessionState(params: {
|
||||
persistedVerbose?: VerboseLevel;
|
||||
verboseDefault?: VerboseLevel;
|
||||
sessionStateActor: Parameters<typeof recordSessionHumanDirectMessage>[0]["actor"];
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
}) {
|
||||
const requestedThinkLevel = params.thinkOnce ?? params.thinkOverride ?? params.persistedThinking;
|
||||
const resolvedVerboseLevel =
|
||||
@@ -77,6 +79,9 @@ export async function prepareEmbeddedSessionState(params: {
|
||||
}),
|
||||
},
|
||||
watch: false,
|
||||
...(params.pluginMetadataSnapshot
|
||||
? { pluginMetadataSnapshot: params.pluginMetadataSnapshot }
|
||||
: {}),
|
||||
});
|
||||
const needsSkillsSnapshot =
|
||||
params.isNewSession || !currentSkillsSnapshot || skillSnapshotState.shouldRefresh;
|
||||
|
||||
@@ -365,9 +365,6 @@ function createCompactHooksRuntimePlan(params: BuildAgentRuntimePlanParams): Age
|
||||
transformSystemPrompt: vi.fn((context: { systemPrompt: string }) => context.systemPrompt),
|
||||
},
|
||||
tools: {
|
||||
preparedPlanning: {
|
||||
loadMetadataSnapshot: () => ({}),
|
||||
},
|
||||
normalize: vi.fn((tools) => tools),
|
||||
logDiagnostics: vi.fn(),
|
||||
},
|
||||
|
||||
@@ -15,12 +15,12 @@ import {
|
||||
buildHandledBeforeAgentReplyPayloads,
|
||||
runBeforeAgentReplyForTurn,
|
||||
} from "../../plugins/before-agent-reply.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js";
|
||||
import {
|
||||
buildAgentHookContextChannelFields,
|
||||
buildAgentHookContextIdentityFields,
|
||||
} from "../../plugins/hook-agent-context.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { loadPluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import { withPluginRuntimeGenerationScope } from "../../plugins/runtime/generation-scope.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import { isMarkdownCapableMessageChannel } from "../../utils/message-channel.js";
|
||||
@@ -230,15 +230,16 @@ async function runEmbeddedAgentInternal(
|
||||
agentId: requestedWorkspaceResolution.agentId,
|
||||
sessionKey: params.sessionKey,
|
||||
});
|
||||
const currentPluginMetadataSnapshot = getCurrentPluginMetadataSnapshot({
|
||||
config,
|
||||
workspaceDir: requestedWorkspaceResolution.workspaceDir,
|
||||
env: process.env,
|
||||
allowWorkspaceScopedSnapshot: true,
|
||||
});
|
||||
const pluginMetadataSnapshot =
|
||||
params.pluginGeneration?.pluginMetadataSnapshot ??
|
||||
loadPluginMetadataSnapshot({
|
||||
config,
|
||||
workspaceDir: requestedWorkspaceResolution.workspaceDir,
|
||||
env: process.env,
|
||||
});
|
||||
const runtimePluginSelections = resolveModelCandidateChain({
|
||||
cfg: config,
|
||||
manifestPlugins: currentPluginMetadataSnapshot?.plugins ?? [],
|
||||
manifestPlugins: pluginMetadataSnapshot.plugins,
|
||||
provider: requestedRuntimeSelection.provider,
|
||||
model: requestedRuntimeSelection.modelId,
|
||||
requestedRouteResolution: "resolved",
|
||||
@@ -287,11 +288,19 @@ async function runEmbeddedAgentInternal(
|
||||
// Turns need only configured admission facts. Full live model inventory remains
|
||||
// available through the snapshot's lazy control-plane loader.
|
||||
catalogMode: "static",
|
||||
...(params.pluginGeneration ? { pluginGeneration: params.pluginGeneration } : {}),
|
||||
}),
|
||||
);
|
||||
startupStages.mark("prepared-runtime");
|
||||
const preparedModelRuntimeOwnerSnapshot = preparedModelRuntimeLease.snapshot;
|
||||
try {
|
||||
if (
|
||||
params.pluginGeneration &&
|
||||
preparedModelRuntimeOwnerSnapshot.metadataSnapshot !==
|
||||
params.pluginGeneration.pluginMetadataSnapshot
|
||||
) {
|
||||
throw new Error("prepared model runtime replaced the admitted plugin generation");
|
||||
}
|
||||
// A reload may complete while admission waits. The committed generation owns config,
|
||||
// directories, model selection, hooks, fallbacks, and every later run projection.
|
||||
const rebound = bindRunToPreparedModelRuntime({
|
||||
|
||||
@@ -195,12 +195,13 @@ describe("prepareEmbeddedAttemptSetup", () => {
|
||||
expect(resolveProviderRuntimePluginHandle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves partial handles without trusting scoped metadata", async () => {
|
||||
it("resolves partial handles with the exact lifecycle metadata", async () => {
|
||||
const resolvedHandle: ProviderRuntimePluginHandle = {
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
};
|
||||
resolveProviderRuntimePluginHandle.mockReturnValue(resolvedHandle);
|
||||
const metadataSnapshot = { pluginIds: ["other"] };
|
||||
const setup = await prepareEmbeddedAttemptSetup({
|
||||
config: {},
|
||||
modelId: "gpt-5.4",
|
||||
@@ -211,7 +212,7 @@ describe("prepareEmbeddedAttemptSetup", () => {
|
||||
timeoutMs: 30_000,
|
||||
workspaceDir: path.join(os.tmpdir(), "openclaw-attempt-setup-partial"),
|
||||
preparedModelRuntime: {
|
||||
metadataSnapshot: { pluginIds: ["other"] },
|
||||
metadataSnapshot,
|
||||
} as never,
|
||||
runtimePlan: { providerRuntimeHandle: { provider: "openai" } } as never,
|
||||
} as unknown as EmbeddedRunAttemptParams);
|
||||
@@ -223,7 +224,7 @@ describe("prepareEmbeddedAttemptSetup", () => {
|
||||
expect(resolveProviderRuntimePluginHandle).toHaveBeenCalledOnce();
|
||||
const call = resolveProviderRuntimePluginHandle.mock.calls[0]?.[0];
|
||||
expect(call).toMatchObject({ provider: "openai", modelId: "gpt-5.4" });
|
||||
expect(call).not.toHaveProperty("pluginMetadataSnapshot");
|
||||
expect(call?.pluginMetadataSnapshot).toBe(metadataSnapshot);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
getActiveDiagnosticTraceContext,
|
||||
} from "../../../infra/diagnostic-trace-context.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../../../media/local-roots.js";
|
||||
import { isPluginMetadataSnapshotCompatible } from "../../../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import {
|
||||
resolveProviderRuntimePluginHandle,
|
||||
@@ -224,17 +223,6 @@ export async function prepareEmbeddedAttemptSetup(params: EmbeddedRunAttemptPara
|
||||
return providerRuntimeHandle;
|
||||
}
|
||||
const pluginMetadataSnapshot = getCurrentAttemptPluginMetadataSnapshot();
|
||||
const compatibleMetadataSnapshot =
|
||||
pluginMetadataSnapshot &&
|
||||
pluginMetadataSnapshot.pluginIds === undefined &&
|
||||
isPluginMetadataSnapshotCompatible({
|
||||
snapshot: pluginMetadataSnapshot,
|
||||
config: params.config,
|
||||
env: process.env,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
})
|
||||
? pluginMetadataSnapshot
|
||||
: undefined;
|
||||
providerRuntimeHandle = {
|
||||
...resolveProviderRuntimePluginHandle({
|
||||
provider: params.provider,
|
||||
@@ -242,9 +230,7 @@ export async function prepareEmbeddedAttemptSetup(params: EmbeddedRunAttemptPara
|
||||
config: params.config,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
env: process.env,
|
||||
...(compatibleMetadataSnapshot
|
||||
? { pluginMetadataSnapshot: compatibleMetadataSnapshot }
|
||||
: {}),
|
||||
...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}),
|
||||
}),
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentExecutionAuthBinding } from "../../execution-auth-binding.js";
|
||||
import type { PreparedModelRuntimePluginGeneration } from "../../prepared-model-runtime.types.js";
|
||||
import type { SystemAgentToolOptions } from "../../tools/system-agent-tool.js";
|
||||
import type { RunEmbeddedAgentParams } from "./params.js";
|
||||
|
||||
@@ -9,6 +10,8 @@ export type RunEmbeddedAgentInternalParams = RunEmbeddedAgentParams & {
|
||||
preparedModelRuntimeMode?: "isolated-read-only";
|
||||
/** Ring-zero tool override, supplied only by the OpenClaw orchestrator. */
|
||||
systemAgentTool?: SystemAgentToolOptions;
|
||||
/** Gateway-private lifecycle generation selected before command admission. */
|
||||
pluginGeneration?: PreparedModelRuntimePluginGeneration;
|
||||
};
|
||||
|
||||
export type RunEmbeddedAgentParamsWithSessionFile = RunEmbeddedAgentInternalParams & {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { readSourceReplyDeliveryRuntime } from "../../../auto-reply/reply/source-reply-delivery-runtime.js";
|
||||
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 { resolvePreparedRunAdmission } from "../../admitted-run-context.js";
|
||||
import type { AuthProfileStore } from "../../auth-profiles.js";
|
||||
@@ -484,22 +483,10 @@ 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 routeFacts = getModelProviderRequestRouteFacts(effectiveModel);
|
||||
const fallbackEndpointClass = routeFacts
|
||||
? undefined
|
||||
: resolveProviderEndpoint(effectiveModel.baseUrl, compatibleMetadataSnapshot?.owners)
|
||||
.endpointClass;
|
||||
: resolveProviderEndpoint(effectiveModel.baseUrl, pluginMetadataSnapshot?.owners).endpointClass;
|
||||
const providerOwner =
|
||||
routeFacts?.providerOwner ??
|
||||
(fallbackEndpointClass &&
|
||||
@@ -514,7 +501,7 @@ export async function prepareEmbeddedRunRuntime(input: {
|
||||
config: params.config,
|
||||
workspaceDir: input.workspaceDir,
|
||||
env: process.env,
|
||||
...(compatibleMetadataSnapshot ? { pluginMetadataSnapshot: compatibleMetadataSnapshot } : {}),
|
||||
...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}),
|
||||
}),
|
||||
modelId,
|
||||
prepared: true as const,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
resolveSelectedContextEnginePluginId,
|
||||
} from "../../plugins/config-state.js";
|
||||
import { isPluginEnabledByDefaultForPlatform } from "../../plugins/default-enablement.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import {
|
||||
loadPluginRegistrySnapshot,
|
||||
normalizePluginsConfigWithRegistry,
|
||||
@@ -51,12 +52,17 @@ function restrictiveAllowlistOmitsPlugin(config: OpenClawConfig | undefined, plu
|
||||
function resolveSelectedMemoryPluginIds(params: {
|
||||
config: OpenClawConfig | undefined;
|
||||
workspaceDir: string;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): string[] {
|
||||
// Honor config-owned test defaults before discovery forces an implicit memory owner.
|
||||
if (isTestDefaultMemorySlotDisabled(params.config ?? {})) {
|
||||
return [];
|
||||
}
|
||||
const registry = loadPluginRegistrySnapshot(params);
|
||||
const registry = loadPluginRegistrySnapshot({
|
||||
config: params.config,
|
||||
workspaceDir: params.metadataSnapshot?.workspaceDir ?? params.workspaceDir,
|
||||
...(params.metadataSnapshot ? { index: params.metadataSnapshot.index } : {}),
|
||||
});
|
||||
const plugins = normalizePluginsConfigWithRegistry(params.config?.plugins, registry);
|
||||
const memorySlot = plugins.slots.memory;
|
||||
if (
|
||||
@@ -86,6 +92,7 @@ function resolveSelectedProviderOwnerPluginIds(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir: string;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): string[] {
|
||||
const providerOwnerPluginIds = dedupePluginIds(
|
||||
resolveOwningPluginIdsForProviderRef(params) ?? [],
|
||||
@@ -98,11 +105,18 @@ function resolveSelectedProviderOwnerPluginIds(params: {
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
onlyPluginIds: providerOwnerPluginIds,
|
||||
manifestRegistry: params.metadataSnapshot?.manifestRegistry,
|
||||
}),
|
||||
...resolveActivatableProviderOwnerPluginIds({
|
||||
pluginIds: providerOwnerPluginIds,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
...(params.metadataSnapshot
|
||||
? {
|
||||
registry: params.metadataSnapshot.index,
|
||||
manifestRegistry: params.metadataSnapshot.manifestRegistry,
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
]);
|
||||
return providerOwnerPluginIds.filter((pluginId) => safeProviderOwnerPluginIds.includes(pluginId));
|
||||
@@ -115,12 +129,14 @@ export function resolveAgentHarnessOwnerPluginIds(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir: string;
|
||||
providerOwnerPluginIds?: readonly string[];
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): string[] {
|
||||
const harnessPluginIds = resolveManifestActivationPlan({
|
||||
trigger: { kind: "agentHarness", runtime: params.runtime },
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
requireExplicitManifestOwnerTrust: true,
|
||||
manifestRecords: params.metadataSnapshot?.plugins,
|
||||
}).entries.map((entry) => entry.pluginId);
|
||||
if (
|
||||
harnessPluginIds.length === 0 ||
|
||||
@@ -197,11 +213,13 @@ export function resolveAgentRuntimePluginLoadPlan(params: {
|
||||
workspaceDir: string;
|
||||
basePluginIds?: readonly string[];
|
||||
selections: readonly AgentHarnessPluginSelection[];
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): { config?: OpenClawConfig; pluginIds?: string[] } {
|
||||
let config = params.config;
|
||||
const memoryPluginIds = resolveSelectedMemoryPluginIds({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
metadataSnapshot: params.metadataSnapshot,
|
||||
});
|
||||
const contextEnginePluginId = resolveSelectedContextEnginePluginId(params.config);
|
||||
const contextEnginePluginIds = contextEnginePluginId ? [contextEnginePluginId] : [];
|
||||
@@ -216,6 +234,7 @@ export function resolveAgentRuntimePluginLoadPlan(params: {
|
||||
provider: selection.provider,
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
metadataSnapshot: params.metadataSnapshot,
|
||||
});
|
||||
pluginIds.push(...providerOwnerPluginIds);
|
||||
forceActivatedPluginIds.push(...providerOwnerPluginIds);
|
||||
@@ -228,6 +247,7 @@ export function resolveAgentRuntimePluginLoadPlan(params: {
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
providerOwnerPluginIds,
|
||||
metadataSnapshot: params.metadataSnapshot,
|
||||
});
|
||||
pluginIds.push(...harnessPluginIds);
|
||||
const allowedHarnessPluginIds =
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
createPluginMetadataSnapshot,
|
||||
makeRegistry,
|
||||
} from "../config/plugin-auto-enable.test-helpers.js";
|
||||
import * as pluginMetadata from "../plugins/plugin-metadata-snapshot.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot } from "../secrets/runtime.js";
|
||||
@@ -191,20 +190,14 @@ describe("createOpenClawTools browser plugin integration", () => {
|
||||
manifestRegistry: makeRegistry([]),
|
||||
workspaceDir: "/tmp",
|
||||
});
|
||||
const resolveMetadata = vi
|
||||
.spyOn(pluginMetadata, "resolvePluginMetadataSnapshot")
|
||||
.mockReturnValue(metadataSnapshot);
|
||||
try {
|
||||
expect(
|
||||
prepareOwnedPluginLoadContext(
|
||||
{ agentDir: "/tmp/agent", config, workspaceDir: "/tmp" },
|
||||
process.env,
|
||||
pluginRegistry,
|
||||
),
|
||||
).toBe(metadataSnapshot);
|
||||
} finally {
|
||||
resolveMetadata.mockRestore();
|
||||
}
|
||||
expect(
|
||||
prepareOwnedPluginLoadContext(
|
||||
{ agentDir: "/tmp/agent", config, workspaceDir: "/tmp" },
|
||||
process.env,
|
||||
pluginRegistry,
|
||||
metadataSnapshot,
|
||||
),
|
||||
).toBe(metadataSnapshot);
|
||||
const loadContext = getPreparedPluginRuntimeLoadContext(pluginRegistry);
|
||||
if (!loadContext) {
|
||||
throw new Error("expected prepared plugin load context");
|
||||
|
||||
@@ -61,7 +61,6 @@ function refreshAuthStore(params: {
|
||||
config: params.config,
|
||||
metadataSnapshot: params.pluginGeneration.pluginMetadataSnapshot,
|
||||
pluginRegistry: params.pluginGeneration.pluginRegistry,
|
||||
workspaceDir: params.pluginGeneration.pluginMetadataSnapshot.workspaceDir,
|
||||
},
|
||||
() =>
|
||||
overlayExternalAuthProfiles(prepared, {
|
||||
@@ -141,7 +140,6 @@ export async function runPreparedModelCatalogWorkerRequest(
|
||||
config: value.input.config,
|
||||
metadataSnapshot: prepared.pluginGeneration.pluginMetadataSnapshot,
|
||||
pluginRegistry: prepared.pluginGeneration.pluginRegistry,
|
||||
workspaceDir: value.input.workspaceDir,
|
||||
},
|
||||
() =>
|
||||
resolveAmbientAgentCredentialsForDiscovery({
|
||||
|
||||
@@ -99,6 +99,7 @@ export async function acquirePreparedModelRuntimeLeaseFromOwners(
|
||||
options: {
|
||||
retainIdleRunOwner?: boolean;
|
||||
catalogMode?: PreparedModelRuntimeCatalogMode;
|
||||
pluginGeneration?: PreparedModelRuntimeOwner["pluginGeneration"];
|
||||
} = {},
|
||||
): Promise<PreparedModelRuntimeLease> {
|
||||
let normalizedInput = normalizePreparedModelRuntimeInput({
|
||||
@@ -109,6 +110,7 @@ export async function acquirePreparedModelRuntimeLeaseFromOwners(
|
||||
if (
|
||||
provenance === "run" &&
|
||||
context.getGatewayLifecycleActive() &&
|
||||
!options.pluginGeneration &&
|
||||
!context.getPendingReplacement()
|
||||
) {
|
||||
try {
|
||||
@@ -143,7 +145,7 @@ export async function acquirePreparedModelRuntimeLeaseFromOwners(
|
||||
if (context.getPendingReplacement()) {
|
||||
continue;
|
||||
}
|
||||
if (provenance === "run") {
|
||||
if (provenance === "run" && !options.pluginGeneration) {
|
||||
input = rebindInputToCommittedConfiguredOwner(context.owners, input);
|
||||
key = ownerKey(input);
|
||||
}
|
||||
@@ -157,6 +159,7 @@ export async function acquirePreparedModelRuntimeLeaseFromOwners(
|
||||
if (
|
||||
context.getGatewayLifecycleActive() &&
|
||||
provenance === "run" &&
|
||||
!options.pluginGeneration &&
|
||||
(!existing || staleDynamicOwner)
|
||||
) {
|
||||
// Dynamic workspaces still inherit the committed agent/config generation. Only their
|
||||
@@ -184,11 +187,9 @@ export async function acquirePreparedModelRuntimeLeaseFromOwners(
|
||||
}
|
||||
}
|
||||
try {
|
||||
const reusablePluginGeneration = resolveReusableConfiguredPluginGeneration(
|
||||
input,
|
||||
workspacePluginRootPresent,
|
||||
context,
|
||||
);
|
||||
const reusablePluginGeneration =
|
||||
options.pluginGeneration ??
|
||||
resolveReusableConfiguredPluginGeneration(input, workspacePluginRootPresent, context);
|
||||
if (existing && !staleDynamicOwner) {
|
||||
snapshot = await context.prepareSnapshot(input);
|
||||
} else {
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
createPreparedInboundRegistryLoader,
|
||||
preparedModelRuntimeWorkspaceFactsKey,
|
||||
} from "./prepared-model-runtime.inbound-registry.js";
|
||||
import { projectPreparedPluginGeneration } from "./prepared-model-runtime.plugin-generation.js";
|
||||
import type {
|
||||
PreparedModelRuntimeBuildStats,
|
||||
PreparedModelRuntimeCatalogMode,
|
||||
@@ -281,6 +280,7 @@ async function buildSnapshotBatch(
|
||||
PreparedModelRuntimeInput,
|
||||
PreparedModelRuntimePluginGeneration
|
||||
>,
|
||||
pluginMetadataSnapshot?: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"],
|
||||
onBuildStats?: (stats: PreparedModelRuntimeBuildStats) => void,
|
||||
): Promise<PreparedModelRuntimeBuildResult[]> {
|
||||
const freshGroups = new Map<string, PreparedModelRuntimeInput[]>();
|
||||
@@ -346,6 +346,7 @@ async function buildSnapshotBatch(
|
||||
{},
|
||||
prepareInboundPluginRegistry ? loadInboundPluginRegistry : undefined,
|
||||
pluginGeneration,
|
||||
pluginMetadataSnapshot,
|
||||
);
|
||||
assertPreparedModelRuntimeInputsCurrent(groupInputs, buildGuards);
|
||||
runtimePluginMs += prepared.buildStats.runtimePluginMs;
|
||||
@@ -356,13 +357,7 @@ async function buildSnapshotBatch(
|
||||
configuredProjectionMs += prepared.buildStats.configuredProjectionMs;
|
||||
for (const agentFacts of prepared.agentFacts) {
|
||||
preparedInputs.set(agentFacts.input, agentFacts);
|
||||
pluginGenerations.set(
|
||||
agentFacts.input,
|
||||
projectPreparedPluginGeneration({
|
||||
input: agentFacts.input,
|
||||
pluginGeneration: prepared.pluginGeneration,
|
||||
}),
|
||||
);
|
||||
pluginGenerations.set(agentFacts.input, prepared.pluginGeneration);
|
||||
}
|
||||
}
|
||||
const workspaceFactsMs = performance.now() - workspaceFactsStartedAt;
|
||||
@@ -552,6 +547,7 @@ export function startSerializedSnapshotBuildBatch(
|
||||
PreparedModelRuntimeInput,
|
||||
PreparedModelRuntimePluginGeneration
|
||||
> = new Map(),
|
||||
pluginMetadataSnapshot?: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"],
|
||||
): {
|
||||
pending: Promise<PreparedModelRuntimeBuildResult[]>;
|
||||
completion: Promise<void>;
|
||||
@@ -579,6 +575,7 @@ export function startSerializedSnapshotBuildBatch(
|
||||
buildGuards,
|
||||
inboundPluginRegistryInputs,
|
||||
reusablePluginGenerations,
|
||||
pluginMetadataSnapshot,
|
||||
onBuildStats,
|
||||
),
|
||||
};
|
||||
@@ -618,6 +615,7 @@ export function startSerializedSnapshotBuild(
|
||||
generationGuard: () => boolean = () => true,
|
||||
prepareInboundPluginRegistry = false,
|
||||
reusablePluginGeneration?: PreparedModelRuntimePluginGeneration,
|
||||
pluginMetadataSnapshot?: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"],
|
||||
): {
|
||||
pending: Promise<PreparedModelRuntimeBuildResult>;
|
||||
completion: Promise<void>;
|
||||
@@ -632,6 +630,7 @@ export function startSerializedSnapshotBuild(
|
||||
undefined,
|
||||
prepareInboundPluginRegistry ? new Set([input]) : undefined,
|
||||
reusablePluginGeneration ? new Map([[input, reusablePluginGeneration]]) : undefined,
|
||||
pluginMetadataSnapshot,
|
||||
);
|
||||
return {
|
||||
pending: build.pending.then((results) => results[0]!),
|
||||
|
||||
@@ -175,6 +175,7 @@ export async function prepareWorkspaceBuildGroup(
|
||||
options: { providerDiscoveryProviderIds?: readonly string[] } = {},
|
||||
loadInboundPluginRegistry?: PreparedInboundRegistryLoader,
|
||||
reusablePluginGeneration?: PreparedModelRuntimePluginGeneration,
|
||||
preparedPluginMetadataSnapshot?: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"],
|
||||
): Promise<{
|
||||
agentFacts: PreparedModelRuntimeAgentFacts[];
|
||||
pluginGeneration: PreparedModelRuntimePluginGeneration;
|
||||
@@ -193,23 +194,24 @@ export async function prepareWorkspaceBuildGroup(
|
||||
throw new Error("prepared model runtime workspace group is empty");
|
||||
}
|
||||
const env = input.env ?? process.env;
|
||||
const pluginMetadataStartedAt = performance.now();
|
||||
const pluginMetadataSnapshot =
|
||||
preparedPluginMetadataSnapshot ??
|
||||
reusablePluginGeneration?.pluginMetadataSnapshot ??
|
||||
prepareOwnedPluginLoadContext(input, env, undefined);
|
||||
const pluginMetadataMs = reusablePluginGeneration
|
||||
? 0
|
||||
: performance.now() - pluginMetadataStartedAt;
|
||||
const runtimePluginStartedAt = performance.now();
|
||||
const { inboundPluginRegistry, runtimePluginRegistry } = reusablePluginGeneration
|
||||
? {
|
||||
inboundPluginRegistry: reusablePluginGeneration.inboundPluginRegistry,
|
||||
runtimePluginRegistry: reusablePluginGeneration.pluginRegistry,
|
||||
}
|
||||
: prepareWorkspacePluginRegistries(input, loadInboundPluginRegistry);
|
||||
: prepareWorkspacePluginRegistries(input, pluginMetadataSnapshot, loadInboundPluginRegistry);
|
||||
const runtimePluginMs = reusablePluginGeneration ? 0 : performance.now() - runtimePluginStartedAt;
|
||||
const prepare = async (
|
||||
preparedMetadataSnapshot?: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"],
|
||||
) => {
|
||||
const pluginMetadataStartedAt = performance.now();
|
||||
const pluginMetadataSnapshot =
|
||||
preparedMetadataSnapshot ?? prepareOwnedPluginLoadContext(input, env, runtimePluginRegistry);
|
||||
const pluginMetadataMs = reusablePluginGeneration
|
||||
? 0
|
||||
: performance.now() - pluginMetadataStartedAt;
|
||||
prepareOwnedPluginLoadContext(input, env, runtimePluginRegistry, pluginMetadataSnapshot);
|
||||
const prepare = async () => {
|
||||
const matchesStaticModelId = createStaticModelIdMatcher({
|
||||
manifestPlugins: pluginMetadataSnapshot.plugins,
|
||||
});
|
||||
@@ -421,7 +423,7 @@ export async function prepareWorkspaceBuildGroup(
|
||||
return reusablePluginGeneration
|
||||
? await withPreparedPluginGenerationScope(
|
||||
{ input, pluginGeneration: reusablePluginGeneration },
|
||||
prepare,
|
||||
() => prepare(),
|
||||
)
|
||||
: await withPluginRuntimeRegistryScope(runtimePluginRegistry, prepare);
|
||||
}
|
||||
@@ -489,18 +491,14 @@ export async function prepareFullCatalogFacts(
|
||||
inlineProviderModels: pluginGeneration.inlineProviderModels,
|
||||
};
|
||||
}
|
||||
|
||||
/** Reports whether a catalog came from the complete prepared-catalog build path. */
|
||||
export function isPreparedModelCatalogFull(snapshot: ModelCatalogSnapshot): boolean {
|
||||
return fullModelCatalogSnapshots.has(snapshot);
|
||||
}
|
||||
|
||||
export const isPreparedModelCatalogFull = (snapshot: ModelCatalogSnapshot): boolean =>
|
||||
fullModelCatalogSnapshots.has(snapshot);
|
||||
/** Restores process-local provenance after a complete catalog crosses a worker boundary. */
|
||||
export function markPreparedModelCatalogFull(snapshot: ModelCatalogSnapshot): ModelCatalogSnapshot {
|
||||
fullModelCatalogSnapshots.add(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function captureModelsJsonContents(agentDir: string): string | null {
|
||||
try {
|
||||
return fs.readFileSync(path.join(agentDir, "models.json"), "utf8");
|
||||
@@ -511,17 +509,13 @@ function captureModelsJsonContents(agentDir: string): string | null {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function fingerprintPreparedRuntimeFacts(value: unknown): string {
|
||||
return sha256Base64Url(stableStringify(value));
|
||||
}
|
||||
|
||||
export const fingerprintPreparedRuntimeFacts = (value: unknown): string =>
|
||||
sha256Base64Url(stableStringify(value));
|
||||
function hasSameOAuthProviderGeneration(
|
||||
left: ReturnType<AuthStorage["getOAuthProviders"]>,
|
||||
right: ReturnType<AuthStorage["getOAuthProviders"]>,
|
||||
): boolean {
|
||||
// OAuth descriptors carry executable hooks. Match those hooks by identity so equivalent
|
||||
// AuthStorage instances share built-ins without merging distinct closure generations.
|
||||
// Match executable hooks by identity so distinct AuthStorage closure generations never merge.
|
||||
return (
|
||||
left.length === right.length &&
|
||||
left.every((provider, index) => {
|
||||
@@ -547,8 +541,7 @@ function groupConfiguredRegistrySources(
|
||||
for (const facts of agentFacts) {
|
||||
const modelsJsonContents = captureModelsJsonContents(facts.input.agentDir);
|
||||
const oauthProviders = facts.templateAuthStorage.getOAuthProviders();
|
||||
// Generated catalogs are agent-owned. Capture only plugins needed by unresolved configured
|
||||
// refs, then group exact bytes and OAuth behavior so publication never mixes generations.
|
||||
// Capture only unresolved configured catalogs, then group exact bytes and OAuth behavior.
|
||||
const pluginCatalogs = loadPersistedPluginModelCatalogsReadOnly(
|
||||
facts.input.agentDir,
|
||||
facts.configuredGeneratedCatalogPluginIds,
|
||||
|
||||
@@ -48,6 +48,7 @@ describe("prepared reply dispatch runtime", () => {
|
||||
gatewayLifecycle: true,
|
||||
catalogMode: "static",
|
||||
allowGatewaySubagentBinding: true,
|
||||
pluginMetadataSnapshot: mocks.pluginMetadataSnapshot as never,
|
||||
});
|
||||
const input = {
|
||||
agentId: "default",
|
||||
@@ -59,7 +60,7 @@ describe("prepared reply dispatch runtime", () => {
|
||||
};
|
||||
const firstSnapshot = getPreparedModelRuntimeSnapshot(input);
|
||||
const firstRuntime = await loadPublishedGatewayReplyDispatchRuntime({ agentId: "default" });
|
||||
expect(firstRuntime).toEqual({
|
||||
expect(firstRuntime).toMatchObject({
|
||||
agentId: "default",
|
||||
agentDir: "/tmp/unused-agent",
|
||||
workspaceDir: "/tmp/unused-workspace",
|
||||
@@ -67,6 +68,10 @@ describe("prepared reply dispatch runtime", () => {
|
||||
modelCatalog: firstSnapshot?.modelCatalog,
|
||||
inboundPluginRegistry: firstRegistry,
|
||||
});
|
||||
expect(firstRuntime?.pluginGeneration?.pluginMetadataSnapshot).toBe(
|
||||
mocks.pluginMetadataSnapshot,
|
||||
);
|
||||
expect(firstSnapshot?.metadataSnapshot).toBe(mocks.pluginMetadataSnapshot);
|
||||
expect(Object.isFrozen(firstRuntime)).toBe(true);
|
||||
|
||||
const replacementCatalog = createDeferred<{ entries: [] }>();
|
||||
@@ -74,6 +79,7 @@ describe("prepared reply dispatch runtime", () => {
|
||||
const refresh = refreshPreparedModelRuntimeSnapshots(replacementConfig, {
|
||||
catalogMode: "static",
|
||||
allowGatewaySubagentBinding: true,
|
||||
pluginMetadataSnapshot: mocks.pluginMetadataSnapshot as never,
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(mocks.loadAgentRuntimePluginRegistryHandle).toHaveBeenCalledTimes(4),
|
||||
@@ -138,28 +144,6 @@ describe("prepared reply dispatch runtime", () => {
|
||||
expect(mocks.loadAgentRuntimePluginRegistryHandle).toHaveBeenCalledTimes(publicationLoadCount);
|
||||
});
|
||||
|
||||
it("projects shared plugin metadata into each configured runtime workspace", async () => {
|
||||
mocks.configuredAgentIds = ["default"];
|
||||
const workspaceDir = "/tmp/configured-metadata-workspace";
|
||||
const config = retainLegacyDefaultAgentId({ agents: { entries: { default: {} } } }, "default");
|
||||
await refreshPreparedModelRuntimeSnapshots(config, {
|
||||
gatewayLifecycle: true,
|
||||
catalogMode: "static",
|
||||
defaultWorkspaceDir: workspaceDir,
|
||||
});
|
||||
|
||||
const snapshot = getPreparedModelRuntimeSnapshot({
|
||||
agentId: "default",
|
||||
agentDir: "/tmp/unused-agent",
|
||||
inheritedAuthDir: "/tmp/unused-agent",
|
||||
config,
|
||||
workspaceDir,
|
||||
});
|
||||
|
||||
expect(snapshot?.metadataSnapshot.workspaceDir).toBe(workspaceDir);
|
||||
expect(snapshot?.metadataSnapshot.index.workspaceDir).toBe(workspaceDir);
|
||||
});
|
||||
|
||||
it("reuses configured and retained dynamic plugin generations during auth refresh", async () => {
|
||||
mocks.configuredAgentIds = ["default"];
|
||||
const workspaceDir = "/tmp/dynamic-auth-workspace";
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { hashRuntimeConfigValue } from "../config/runtime-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||
import type { PluginRegistry } from "../plugins/registry-types.js";
|
||||
import type { PreparedModelRuntimeInput } from "./prepared-model-runtime.types.js";
|
||||
import { loadAgentRuntimePluginRegistryHandle } from "./runtime-plugins.js";
|
||||
|
||||
export type PreparedInboundRegistryLoader = (input: PreparedModelRuntimeInput) => PluginRegistry;
|
||||
export type PreparedInboundRegistryLoader = (
|
||||
input: PreparedModelRuntimeInput,
|
||||
metadataSnapshot: PluginMetadataSnapshot,
|
||||
) => PluginRegistry;
|
||||
|
||||
function inboundRegistryIdentity(input: PreparedModelRuntimeInput): string {
|
||||
return JSON.stringify({
|
||||
@@ -30,7 +34,7 @@ export function preparedModelRuntimeWorkspaceFactsKey(input: PreparedModelRuntim
|
||||
/** Creates one lifecycle-batch loader that shares exact generic registry identities. */
|
||||
export function createPreparedInboundRegistryLoader(): PreparedInboundRegistryLoader {
|
||||
const registries = new Map<string, PluginRegistry>();
|
||||
return (input) => {
|
||||
return (input, metadataSnapshot) => {
|
||||
const key = inboundRegistryIdentity(input);
|
||||
const existing = registries.get(key);
|
||||
if (existing) {
|
||||
@@ -41,6 +45,7 @@ export function createPreparedInboundRegistryLoader(): PreparedInboundRegistryLo
|
||||
env: input.env ?? process.env,
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
...(input.allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true } : {}),
|
||||
metadataSnapshot,
|
||||
});
|
||||
registries.set(key, registry);
|
||||
return registry;
|
||||
@@ -50,6 +55,7 @@ export function createPreparedInboundRegistryLoader(): PreparedInboundRegistryLo
|
||||
/** Prepares distinct generic-inbound and model-selected registries for one workspace generation. */
|
||||
export function prepareWorkspacePluginRegistries(
|
||||
input: PreparedModelRuntimeInput,
|
||||
metadataSnapshot: PluginMetadataSnapshot,
|
||||
loadInboundRegistry?: PreparedInboundRegistryLoader,
|
||||
): {
|
||||
runtimePluginRegistry?: PluginRegistry;
|
||||
@@ -60,7 +66,9 @@ export function prepareWorkspacePluginRegistries(
|
||||
if (input.readOnly && !input.loadRuntimePlugins && !input.runtimePluginSelections) {
|
||||
return {};
|
||||
}
|
||||
const inboundPluginRegistry = input.readOnly ? undefined : loadInboundRegistry?.(input);
|
||||
const inboundPluginRegistry = input.readOnly
|
||||
? undefined
|
||||
: loadInboundRegistry?.(input, metadataSnapshot);
|
||||
const runtimePluginRegistry =
|
||||
input.runtimePluginSelections || !inboundPluginRegistry
|
||||
? loadAgentRuntimePluginRegistryHandle({
|
||||
@@ -68,6 +76,7 @@ export function prepareWorkspacePluginRegistries(
|
||||
env: input.env ?? process.env,
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
...(input.allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true } : {}),
|
||||
metadataSnapshot,
|
||||
selections: input.runtimePluginSelections,
|
||||
})
|
||||
: inboundPluginRegistry;
|
||||
|
||||
@@ -405,6 +405,7 @@ export async function publishPreparedModelRuntimeOwnerBatch(params: {
|
||||
onBuildStats?: (stats: PreparedModelRuntimeBuildStats) => void;
|
||||
registerEntriesAfterBuildStart?: boolean;
|
||||
reusePluginGenerations?: boolean;
|
||||
pluginMetadataSnapshot?: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"];
|
||||
}): Promise<void> {
|
||||
const candidates = params.entries.map(({ owner, input }) => {
|
||||
owner.input = input;
|
||||
@@ -486,6 +487,7 @@ export async function publishPreparedModelRuntimeOwnerBatch(params: {
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
params.pluginMetadataSnapshot,
|
||||
);
|
||||
for (const candidate of currentGroup) {
|
||||
if (params.registerEntriesAfterBuildStart === true) {
|
||||
@@ -575,6 +577,7 @@ export async function publishModelRuntimeSnapshot(
|
||||
provenance: PreparedModelRuntimeOwner["provenance"] = "explicit",
|
||||
catalogMode: PreparedModelRuntimeCatalogMode = existing?.catalogMode ?? "live",
|
||||
reusablePluginGeneration?: PreparedModelRuntimePluginGeneration,
|
||||
pluginMetadataSnapshot?: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"],
|
||||
): Promise<PreparedModelRuntimeSnapshot> {
|
||||
const key = ownerKey(input);
|
||||
const owner = existing ?? createPreparedModelRuntimeOwner(input, provenance, catalogMode);
|
||||
@@ -595,6 +598,7 @@ export async function publishModelRuntimeSnapshot(
|
||||
() => owner.generation === generation && owners.get(key) === owner,
|
||||
provenance === "configured",
|
||||
reusablePluginGeneration,
|
||||
pluginMetadataSnapshot,
|
||||
);
|
||||
owner.buildCompletion = build.completion;
|
||||
void build.completion.then(() => {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createPluginMetadataSnapshot,
|
||||
makeRegistry,
|
||||
} from "../config/plugin-auto-enable.test-helpers.js";
|
||||
import * as currentPluginMetadata from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "../plugins/plugin-metadata-lifecycle.js";
|
||||
import * as pluginMetadata from "../plugins/plugin-metadata-snapshot.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
||||
import {
|
||||
getPreparedPluginRuntimeLoadContext,
|
||||
prepareOwnedPluginLoadContext,
|
||||
} from "./prepared-model-runtime.plugin-context.js";
|
||||
import { withPreparedPluginGenerationScope } from "./prepared-model-runtime.plugin-generation.js";
|
||||
|
||||
describe("prepared model runtime plugin metadata ownership", () => {
|
||||
afterEach(() => {
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
});
|
||||
|
||||
it("uses one explicit Gateway metadata generation across agent workspaces", () => {
|
||||
const config = { plugins: { allow: ["synthetic"] } };
|
||||
const gatewayWorkspace = "/tmp/gateway-plugin-workspace";
|
||||
const gatewaySnapshot = createPluginMetadataSnapshot({
|
||||
config,
|
||||
manifestRegistry: makeRegistry([{ id: "synthetic", channels: [] }]),
|
||||
workspaceDir: gatewayWorkspace,
|
||||
});
|
||||
const inputs = ["first", "second"].map((name) => ({
|
||||
agentDir: `/tmp/${name}-agent`,
|
||||
config,
|
||||
workspaceDir: `/tmp/${name}-workspace`,
|
||||
workspacePluginRootPresent: false,
|
||||
}));
|
||||
const pluginGeneration = {
|
||||
configuredCatalogEntries: [],
|
||||
inlineProviderModels: [],
|
||||
pluginMetadataSnapshot: gatewaySnapshot,
|
||||
};
|
||||
const resolveMetadata = vi.spyOn(pluginMetadata, "loadPluginMetadataSnapshot");
|
||||
const getCurrentMetadata = vi.spyOn(currentPluginMetadata, "getCurrentPluginMetadataSnapshot");
|
||||
|
||||
try {
|
||||
for (const input of inputs) {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
expect(prepareOwnedPluginLoadContext(input, process.env, registry, gatewaySnapshot)).toBe(
|
||||
gatewaySnapshot,
|
||||
);
|
||||
expect(getPreparedPluginRuntimeLoadContext(registry)?.metadataSnapshot).toBe(
|
||||
gatewaySnapshot,
|
||||
);
|
||||
expect(
|
||||
withPreparedPluginGenerationScope({ input, pluginGeneration }, (snapshot) => snapshot),
|
||||
).toBe(gatewaySnapshot);
|
||||
}
|
||||
expect(getCurrentMetadata).not.toHaveBeenCalled();
|
||||
expect(resolveMetadata).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
getCurrentMetadata.mockRestore();
|
||||
resolveMetadata.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps direct no-current preparation on the requested workspace", () => {
|
||||
const config = { plugins: { allow: ["synthetic"] } };
|
||||
const workspaceDir = "/tmp/direct-plugin-workspace";
|
||||
const directSnapshot = createPluginMetadataSnapshot({
|
||||
config,
|
||||
manifestRegistry: makeRegistry([{ id: "synthetic", channels: [] }]),
|
||||
workspaceDir,
|
||||
});
|
||||
const resolveMetadata = vi
|
||||
.spyOn(pluginMetadata, "loadPluginMetadataSnapshot")
|
||||
.mockReturnValue(directSnapshot);
|
||||
|
||||
try {
|
||||
expect(
|
||||
prepareOwnedPluginLoadContext(
|
||||
{
|
||||
agentDir: "/tmp/direct-agent",
|
||||
config,
|
||||
workspaceDir,
|
||||
workspacePluginRootPresent: false,
|
||||
},
|
||||
process.env,
|
||||
undefined,
|
||||
),
|
||||
).toBe(directSnapshot);
|
||||
expect(resolveMetadata).toHaveBeenCalledWith({
|
||||
config,
|
||||
env: process.env,
|
||||
workspaceDir,
|
||||
});
|
||||
} finally {
|
||||
resolveMetadata.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { PluginDiscoveryResult } from "../plugins/discovery.js";
|
||||
import { extractPluginInstallRecordsFromInstalledPluginIndex } from "../plugins/installed-plugin-index-install-records.js";
|
||||
import {
|
||||
projectPluginMetadataSnapshotWorkspace,
|
||||
resolvePluginMetadataSnapshot,
|
||||
} from "../plugins/plugin-metadata-snapshot.js";
|
||||
import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||
import type { PluginRegistry } from "../plugins/registry-types.js";
|
||||
import {
|
||||
@@ -32,9 +29,10 @@ function preparePluginLoadContext(
|
||||
registry: PluginRegistry | undefined,
|
||||
metadataSnapshot: PluginMetadataSnapshot,
|
||||
): PluginRuntimeLoadContext & { metadataSnapshot: PluginMetadataSnapshot } {
|
||||
const { config, workspaceDir } = input;
|
||||
// The prepared owner already resolved metadata for this exact config/env/workspace tuple.
|
||||
// Missing discovery facts stay empty here instead of reopening cold channel discovery.
|
||||
const { config } = input;
|
||||
const workspaceDir = metadataSnapshot.workspaceDir ?? input.workspaceDir;
|
||||
// The prepared owner already selected the exact metadata generation for this runtime.
|
||||
// Missing discovery facts stay empty here instead of reopening cold plugin discovery.
|
||||
const preparedMetadataSnapshot = metadataSnapshot.discovery
|
||||
? metadataSnapshot
|
||||
: { ...metadataSnapshot, discovery: emptyPluginDiscovery };
|
||||
@@ -61,25 +59,23 @@ export function prepareOwnedPluginLoadContext(
|
||||
input: PreparedModelRuntimeInput,
|
||||
env: NodeJS.ProcessEnv,
|
||||
registry: PluginRegistry | undefined,
|
||||
preparedMetadataSnapshot?: PluginMetadataSnapshot,
|
||||
): PluginMetadataSnapshot {
|
||||
const resolvedMetadataSnapshot = resolvePluginMetadataSnapshot({
|
||||
const metadataSnapshot = preparedMetadataSnapshot ?? resolveColdMetadataSnapshot(input, env);
|
||||
preparePluginLoadContext(input, env, registry, metadataSnapshot);
|
||||
return metadataSnapshot;
|
||||
}
|
||||
|
||||
function resolveColdMetadataSnapshot(
|
||||
input: PreparedModelRuntimeInput,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): PluginMetadataSnapshot {
|
||||
const resolvedMetadataSnapshot = loadPluginMetadataSnapshot({
|
||||
config: input.config,
|
||||
env,
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
...(input.workspacePluginRootPresent === undefined
|
||||
? {}
|
||||
: { workspacePluginRootPresent: input.workspacePluginRootPresent }),
|
||||
});
|
||||
const metadataSnapshot = input.workspaceDir
|
||||
? projectPluginMetadataSnapshotWorkspace({
|
||||
snapshot: resolvedMetadataSnapshot,
|
||||
config: input.config,
|
||||
env,
|
||||
workspaceDir: input.workspaceDir,
|
||||
})
|
||||
: resolvedMetadataSnapshot;
|
||||
preparePluginLoadContext(input, env, registry, metadataSnapshot);
|
||||
return metadataSnapshot;
|
||||
return resolvedMetadataSnapshot;
|
||||
}
|
||||
|
||||
/** Reads plugin facts carried by a lifecycle-owned prepared runtime snapshot. */
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { projectPluginMetadataSnapshotWorkspace } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import { withPluginRuntimeGenerationScope } from "../plugins/runtime/generation-scope.js";
|
||||
import { augmentPreparedModelCatalogWithAgentHarness } from "./harness/model-catalog.js";
|
||||
import { buildPreparedModelCatalogSnapshot } from "./model-catalog.js";
|
||||
@@ -48,25 +47,6 @@ export function createPreparedPluginGeneration(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function projectPreparedPluginGeneration(params: {
|
||||
input: PreparedModelRuntimeInput;
|
||||
pluginGeneration: PreparedModelRuntimePluginGeneration;
|
||||
}): PreparedModelRuntimePluginGeneration {
|
||||
const { input, pluginGeneration } = params;
|
||||
if (!input.workspaceDir) {
|
||||
return pluginGeneration;
|
||||
}
|
||||
const pluginMetadataSnapshot = projectPluginMetadataSnapshotWorkspace({
|
||||
snapshot: pluginGeneration.pluginMetadataSnapshot,
|
||||
config: input.config,
|
||||
env: input.env ?? process.env,
|
||||
workspaceDir: input.workspaceDir,
|
||||
});
|
||||
return pluginMetadataSnapshot === pluginGeneration.pluginMetadataSnapshot
|
||||
? pluginGeneration
|
||||
: Object.freeze({ ...pluginGeneration, pluginMetadataSnapshot });
|
||||
}
|
||||
|
||||
export async function buildPreparedPluginModelCatalog(params: {
|
||||
agentFacts: {
|
||||
credentials: Parameters<typeof buildPreparedModelCatalogSnapshot>[0]["authCredentials"];
|
||||
@@ -110,15 +90,13 @@ export function withPreparedPluginGenerationScope<T>(
|
||||
},
|
||||
run: (metadataSnapshot: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"]) => T,
|
||||
): T {
|
||||
const { input } = params;
|
||||
const pluginGeneration = projectPreparedPluginGeneration(params);
|
||||
const { input, pluginGeneration } = params;
|
||||
const metadataSnapshot = pluginGeneration.pluginMetadataSnapshot;
|
||||
return withPluginRuntimeGenerationScope(
|
||||
{
|
||||
config: input.config,
|
||||
metadataSnapshot,
|
||||
pluginRegistry: pluginGeneration.pluginRegistry,
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
},
|
||||
() => run(metadataSnapshot),
|
||||
);
|
||||
|
||||
@@ -395,23 +395,17 @@ describe("prepared model runtime Gateway catalog mode", () => {
|
||||
expect(mocks.discoverModels).toHaveBeenLastCalledWith(
|
||||
mocks.authStorage,
|
||||
expect.objectContaining({
|
||||
config,
|
||||
includePluginCatalogs: true,
|
||||
modelsJsonContents: null,
|
||||
pluginCatalogs: [],
|
||||
pluginMetadataSnapshot: expect.objectContaining({
|
||||
...mocks.metadataSnapshot,
|
||||
index: expect.objectContaining({
|
||||
...mocks.metadataSnapshot.index,
|
||||
workspaceDir: "/tmp/prepared-static-workspace",
|
||||
}),
|
||||
workspaceDir: "/tmp/prepared-static-workspace",
|
||||
}),
|
||||
pluginMetadataSnapshot: mocks.metadataSnapshot,
|
||||
workspaceDir: "/tmp/prepared-static-workspace",
|
||||
}),
|
||||
);
|
||||
expect(mocks.buildPreparedModelCatalogSnapshot).not.toHaveBeenCalled();
|
||||
expect(mocks.loadStaticCatalog).not.toHaveBeenCalled();
|
||||
expect(mocks.resolvePluginMetadataSnapshot).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.resolvePluginMetadataSnapshot).toHaveBeenCalledOnce();
|
||||
expect(configuredRuntimeModelCount).toBe(1);
|
||||
expect(generatedCatalogReadCount).toBe(0);
|
||||
const snapshot = getPreparedModelRuntimeSnapshot({
|
||||
|
||||
@@ -85,13 +85,6 @@ const preparedModelRuntimeMocks = vi.hoisted(() => ({
|
||||
vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({
|
||||
isPluginMetadataSnapshotCompatible: () => true,
|
||||
loadPluginMetadataSnapshot: () => preparedModelRuntimeMocks.pluginMetadataSnapshot,
|
||||
projectPluginMetadataSnapshotWorkspace: ({
|
||||
snapshot,
|
||||
workspaceDir,
|
||||
}: {
|
||||
snapshot: typeof preparedModelRuntimeMocks.pluginMetadataSnapshot & { workspaceDir?: string };
|
||||
workspaceDir: string;
|
||||
}) => ({ ...snapshot, index: { ...snapshot.index, workspaceDir }, workspaceDir }),
|
||||
resolvePluginMetadataSnapshot: () => preparedModelRuntimeMocks.pluginMetadataSnapshot,
|
||||
}));
|
||||
|
||||
|
||||
@@ -111,12 +111,15 @@ describe("prepared model runtime snapshots", () => {
|
||||
mocks.loadAgentRuntimePluginRegistryHandle.mockReturnValue(pluginRegistry);
|
||||
|
||||
expect(
|
||||
prepareWorkspacePluginRegistries({
|
||||
config: {},
|
||||
agentDir: "/tmp/native-provider-probe",
|
||||
readOnly: true,
|
||||
loadRuntimePlugins: true,
|
||||
}).runtimePluginRegistry,
|
||||
prepareWorkspacePluginRegistries(
|
||||
{
|
||||
config: {},
|
||||
agentDir: "/tmp/native-provider-probe",
|
||||
readOnly: true,
|
||||
loadRuntimePlugins: true,
|
||||
},
|
||||
mocks.pluginMetadataSnapshot as never,
|
||||
).runtimePluginRegistry,
|
||||
).toBe(pluginRegistry);
|
||||
expect(mocks.loadAgentRuntimePluginRegistryHandle).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ selections: undefined }),
|
||||
@@ -180,6 +183,7 @@ describe("prepared model runtime snapshots", () => {
|
||||
expect(mocks.loadAgentRuntimePluginRegistryHandle).toHaveBeenCalledWith({
|
||||
config: {},
|
||||
env: process.env,
|
||||
metadataSnapshot: mocks.pluginMetadataSnapshot,
|
||||
workspaceDir: "/tmp/prepared-model-runtime-plugin-workspace",
|
||||
selections: undefined,
|
||||
});
|
||||
|
||||
@@ -290,6 +290,7 @@ export async function acquireAgentRunPreparedModelRuntime(
|
||||
options: {
|
||||
retainIdleRunOwner?: boolean;
|
||||
catalogMode?: PreparedModelRuntimeCatalogMode;
|
||||
pluginGeneration?: PreparedModelRuntimeOwner["pluginGeneration"];
|
||||
} = {},
|
||||
): Promise<PreparedModelRuntimeLease> {
|
||||
return await acquirePreparedModelRuntimeLeaseFromOwners(
|
||||
@@ -490,6 +491,7 @@ async function refreshPreparedModelRuntimeSnapshotsNow(
|
||||
// candidates, while a newer config epoch stops every remaining build in this publication.
|
||||
isBuildCurrent: () => publicationEpoch === refreshRequestEpoch,
|
||||
onBuildStats: options.onBuildStats,
|
||||
pluginMetadataSnapshot: options.pluginMetadataSnapshot,
|
||||
registerEntriesAfterBuildStart: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ export type PreparedReplyDispatchRuntime = Readonly<{
|
||||
config: OpenClawConfig;
|
||||
modelCatalog: ModelCatalogSnapshot;
|
||||
inboundPluginRegistry: PluginRegistry;
|
||||
pluginGeneration?: PreparedModelRuntimePluginGeneration;
|
||||
}>;
|
||||
|
||||
export type PreparedModelRuntimeStores = {
|
||||
@@ -114,6 +115,7 @@ export type PreparedModelRuntimeRefreshOptions = {
|
||||
catalogMode?: PreparedModelRuntimeCatalogMode;
|
||||
onBuildStats?: (stats: PreparedModelRuntimeBuildStats) => void;
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
};
|
||||
|
||||
export type PreparedModelRuntimeBuildStats = Readonly<{
|
||||
|
||||
@@ -18,8 +18,9 @@ function createReplyDispatchRuntime(
|
||||
): PreparedReplyDispatchRuntime {
|
||||
const snapshot = runtimeOwner.snapshot!;
|
||||
const owner = resolvePublishedModelCatalogOwner(snapshot);
|
||||
const inboundPluginRegistry = runtimeOwner.pluginGeneration?.inboundPluginRegistry;
|
||||
if (!inboundPluginRegistry) {
|
||||
const pluginGeneration = runtimeOwner.pluginGeneration;
|
||||
const inboundPluginRegistry = pluginGeneration?.inboundPluginRegistry;
|
||||
if (!pluginGeneration || !inboundPluginRegistry) {
|
||||
throw new PreparedModelRuntimeOwnerNotPublishedError(
|
||||
`prepared inbound plugin registry was not published for ${snapshot.agentDir}`,
|
||||
);
|
||||
@@ -31,6 +32,7 @@ function createReplyDispatchRuntime(
|
||||
config: owner.config,
|
||||
modelCatalog: owner.modelCatalog,
|
||||
inboundPluginRegistry,
|
||||
pluginGeneration,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// auth, transport, tools, prompt, delivery, transcript, and observability.
|
||||
import { createParameterFreeTool } from "openclaw/plugin-sdk/agent-runtime-test-contracts";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resetConfigRuntimeState, setRuntimeConfigSnapshot } from "../../config/config.js";
|
||||
import { resetConfigRuntimeState } from "../../config/config.js";
|
||||
import {
|
||||
prepareProviderExtraParams,
|
||||
resolveProviderFollowupFallbackRoute,
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from "../../plugins/provider-hook-runtime.js";
|
||||
import { buildAgentRuntimeDeliveryPlan, buildAgentRuntimePlan } from "./build.js";
|
||||
|
||||
const isPluginMetadataSnapshotCompatible = vi.hoisted(() => vi.fn(() => true));
|
||||
const resolveProviderIdForAuth = vi.hoisted(() => vi.fn((provider: string) => provider));
|
||||
|
||||
vi.mock("../provider-auth-aliases.js", async (importOriginal) => ({
|
||||
@@ -37,11 +36,6 @@ 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",
|
||||
@@ -419,24 +413,4 @@ describe("AgentRuntimePlan", () => {
|
||||
expect.objectContaining({ 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,
|
||||
metadataSnapshot,
|
||||
});
|
||||
|
||||
expect(isPluginMetadataSnapshotCompatible).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ config: sourceConfig }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,13 +6,8 @@ import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
*/
|
||||
import type { TSchema } from "typebox";
|
||||
import { isSilentReplyPayloadText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
|
||||
import { projectConfigOntoRuntimeSourceSnapshot } from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { hasReplyPayloadContent } from "../../interactive/payload.js";
|
||||
import {
|
||||
isPluginMetadataSnapshotCompatible,
|
||||
resolvePluginMetadataSnapshot,
|
||||
} from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import {
|
||||
resolveProviderRuntimePluginHandle,
|
||||
@@ -60,21 +55,10 @@ type RuntimePlanMetadataParams = BuildAgentRuntimeDeliveryPlanParams & {
|
||||
metadataSnapshot?: BuildAgentRuntimePlanParams["metadataSnapshot"];
|
||||
};
|
||||
|
||||
function resolveCompatibleMetadataSnapshot(
|
||||
function resolvePreparedMetadataSnapshot(
|
||||
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;
|
||||
return params.metadataSnapshot as PluginMetadataSnapshot | undefined;
|
||||
}
|
||||
|
||||
function resolvePreparedProviderRuntimeHandle(
|
||||
@@ -91,7 +75,7 @@ function resolvePreparedProviderRuntimeHandle(
|
||||
prepared: true;
|
||||
};
|
||||
}
|
||||
const compatibleMetadataSnapshot = resolveCompatibleMetadataSnapshot(params);
|
||||
const metadataSnapshot = resolvePreparedMetadataSnapshot(params);
|
||||
return {
|
||||
...resolveProviderRuntimePluginHandle({
|
||||
provider: params.provider,
|
||||
@@ -99,7 +83,7 @@ function resolvePreparedProviderRuntimeHandle(
|
||||
config: asOpenClawConfig(params.config),
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: process.env,
|
||||
...(compatibleMetadataSnapshot ? { pluginMetadataSnapshot: compatibleMetadataSnapshot } : {}),
|
||||
...(metadataSnapshot ? { pluginMetadataSnapshot: metadataSnapshot } : {}),
|
||||
}),
|
||||
modelId: params.modelId,
|
||||
prepared: true,
|
||||
@@ -155,21 +139,10 @@ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): Agen
|
||||
const model = asProviderRuntimeModel(params.model);
|
||||
const modelApi = params.modelApi ?? params.model?.api ?? undefined;
|
||||
const transport = params.resolvedTransport;
|
||||
const toolPlanningConfig = config ? projectConfigOntoRuntimeSourceSnapshot(config) : undefined;
|
||||
const toolPlanningMetadataSnapshot = resolveCompatibleMetadataSnapshot(
|
||||
params,
|
||||
toolPlanningConfig,
|
||||
);
|
||||
const toolPlanningMetadataSnapshot = resolvePreparedMetadataSnapshot(params);
|
||||
const preparedPlanning = toolPlanningMetadataSnapshot
|
||||
? { metadataSnapshot: toolPlanningMetadataSnapshot }
|
||||
: {
|
||||
loadMetadataSnapshot: () =>
|
||||
resolvePluginMetadataSnapshot({
|
||||
config: toolPlanningConfig,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
env: process.env,
|
||||
}),
|
||||
};
|
||||
: undefined;
|
||||
const providerRuntimeHandleForPlugins = resolvePreparedProviderRuntimeHandle(params);
|
||||
const auth =
|
||||
params.preparedAuthPlan ??
|
||||
|
||||
@@ -434,7 +434,6 @@ type AgentRuntimePreparedMetadataSnapshot = object;
|
||||
/** Prepared metadata loader used by tool planning without eager manifest reads. */
|
||||
type PreparedOpenClawToolPlanning = {
|
||||
metadataSnapshot?: AgentRuntimePreparedMetadataSnapshot;
|
||||
loadMetadataSnapshot?: () => AgentRuntimePreparedMetadataSnapshot;
|
||||
};
|
||||
|
||||
/** Tool normalization and diagnostics hooks for one runtime attempt. */
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
getCurrentPluginMetadataSnapshot: vi.fn(),
|
||||
loadPluginMetadataSnapshot: vi.fn(),
|
||||
getActivePluginRegistry: vi.fn(),
|
||||
loadPluginRegistryHandle: vi.fn(),
|
||||
adoptRuntimeContextEngineRegistrations: vi.fn((target: unknown) => target),
|
||||
@@ -17,8 +17,8 @@ vi.mock("../plugins/runtime.js", () => ({
|
||||
getActivePluginRegistry: hoisted.getActivePluginRegistry,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({
|
||||
getCurrentPluginMetadataSnapshot: hoisted.getCurrentPluginMetadataSnapshot,
|
||||
vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({
|
||||
loadPluginMetadataSnapshot: hoisted.loadPluginMetadataSnapshot,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
@@ -38,9 +38,27 @@ import {
|
||||
withAgentPluginRegistry,
|
||||
} from "./runtime-plugins.js";
|
||||
|
||||
function createMetadataSnapshot(
|
||||
workspaceDir = "/tmp/gateway-workspace",
|
||||
pluginIds: string[] | undefined = ["telegram", "memory-core"],
|
||||
) {
|
||||
return {
|
||||
workspaceDir,
|
||||
index: { installRecords: {}, plugins: [] },
|
||||
manifestRegistry: { diagnostics: [], plugins: [] },
|
||||
discovery: { candidates: [], diagnostics: [] },
|
||||
pluginIds,
|
||||
};
|
||||
}
|
||||
|
||||
describe("agent runtime plugin registries", () => {
|
||||
beforeEach(() => {
|
||||
hoisted.getCurrentPluginMetadataSnapshot.mockReset().mockReturnValue(undefined);
|
||||
hoisted.loadPluginMetadataSnapshot
|
||||
.mockReset()
|
||||
.mockImplementation((params: { workspaceDir?: string }) => ({
|
||||
...createMetadataSnapshot(params.workspaceDir),
|
||||
pluginIds: undefined,
|
||||
}));
|
||||
hoisted.getActivePluginRegistry.mockReset().mockReturnValue(undefined);
|
||||
hoisted.loadPluginRegistryHandle.mockReset().mockReturnValue({ handle: true });
|
||||
hoisted.adoptRuntimeContextEngineRegistrations
|
||||
@@ -67,7 +85,7 @@ describe("agent runtime plugin registries", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a non-activating handle for a prepared runtime", () => {
|
||||
it("keeps direct no-current loads on the requested workspace", () => {
|
||||
const config = {} as never;
|
||||
const env = { OPENCLAW_STATE_DIR: "/tmp/openclaw-state" };
|
||||
const selections = [{ provider: "openai", modelId: "gpt-5.5", runtime: "codex" }];
|
||||
@@ -81,7 +99,8 @@ describe("agent runtime plugin registries", () => {
|
||||
selections,
|
||||
}),
|
||||
).toEqual({ handle: true });
|
||||
expect(hoisted.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({
|
||||
const metadataSnapshot = hoisted.loadPluginMetadataSnapshot.mock.results[0]?.value;
|
||||
expect(hoisted.loadPluginMetadataSnapshot).toHaveBeenCalledWith({
|
||||
config,
|
||||
env,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
@@ -90,12 +109,16 @@ describe("agent runtime plugin registries", () => {
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
selections,
|
||||
metadataSnapshot,
|
||||
});
|
||||
expect(hoisted.loadPluginRegistryHandle).toHaveBeenCalledWith({
|
||||
activate: false,
|
||||
config,
|
||||
activationSourceConfig: config,
|
||||
env,
|
||||
discovery: metadataSnapshot.discovery,
|
||||
installRecords: {},
|
||||
manifestRegistry: metadataSnapshot.manifestRegistry,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: { allowGatewaySubagentBinding: true },
|
||||
});
|
||||
@@ -120,17 +143,20 @@ describe("agent runtime plugin registries", () => {
|
||||
|
||||
it("preserves the gateway startup scope and ordering", () => {
|
||||
const config = {} as never;
|
||||
hoisted.getCurrentPluginMetadataSnapshot.mockReturnValue({
|
||||
startup: { pluginIds: ["telegram", "memory-core"] },
|
||||
});
|
||||
const metadataSnapshot = createMetadataSnapshot();
|
||||
|
||||
loadAgentRuntimePluginRegistryHandle({ config, workspaceDir: "/tmp/workspace" });
|
||||
loadAgentRuntimePluginRegistryHandle({
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
metadataSnapshot: metadataSnapshot as never,
|
||||
});
|
||||
|
||||
expect(hoisted.resolveAgentRuntimePluginLoadPlan).toHaveBeenCalledWith({
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
workspaceDir: "/tmp/gateway-workspace",
|
||||
basePluginIds: ["telegram", "memory-core"],
|
||||
selections: [],
|
||||
metadataSnapshot,
|
||||
});
|
||||
expect(hoisted.loadPluginRegistryHandle).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -141,9 +167,7 @@ describe("agent runtime plugin registries", () => {
|
||||
|
||||
it("inherits the current request registry before process-wide startup metadata", () => {
|
||||
const config = {} as never;
|
||||
hoisted.getCurrentPluginMetadataSnapshot.mockReturnValue({
|
||||
startup: { pluginIds: ["telegram", "memory-core"] },
|
||||
});
|
||||
const metadataSnapshot = createMetadataSnapshot();
|
||||
const requestRegistry = {
|
||||
plugins: [
|
||||
{ id: "memory-core", status: "loaded" },
|
||||
@@ -152,14 +176,19 @@ describe("agent runtime plugin registries", () => {
|
||||
} as never;
|
||||
|
||||
withPluginRuntimeRegistryScope(requestRegistry, () =>
|
||||
loadAgentRuntimePluginRegistryHandle({ config, workspaceDir: "/tmp/workspace" }),
|
||||
loadAgentRuntimePluginRegistryHandle({
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
metadataSnapshot: metadataSnapshot as never,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(hoisted.resolveAgentRuntimePluginLoadPlan).toHaveBeenCalledWith({
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
workspaceDir: "/tmp/gateway-workspace",
|
||||
basePluginIds: ["memory-core"],
|
||||
selections: [],
|
||||
metadataSnapshot,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -172,12 +201,47 @@ describe("agent runtime plugin registries", () => {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
|
||||
expect(hoisted.getCurrentPluginMetadataSnapshot).not.toHaveBeenCalled();
|
||||
const metadataSnapshot = hoisted.loadPluginMetadataSnapshot.mock.results[0]?.value;
|
||||
expect(hoisted.resolveAgentRuntimePluginLoadPlan).toHaveBeenCalledWith({
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
basePluginIds: [],
|
||||
selections: [],
|
||||
metadataSnapshot,
|
||||
});
|
||||
});
|
||||
|
||||
it("loads selected runtimes from the Gateway metadata workspace", () => {
|
||||
const config = {} as never;
|
||||
const env = { OPENCLAW_STATE_DIR: "/tmp/openclaw-state" };
|
||||
const snapshot = createMetadataSnapshot();
|
||||
|
||||
loadAgentRuntimePluginRegistryHandle({
|
||||
config,
|
||||
env,
|
||||
workspaceDir: "/tmp/agent-workspace",
|
||||
metadataSnapshot: snapshot as never,
|
||||
});
|
||||
|
||||
expect(hoisted.resolveAgentRuntimePluginLoadPlan).toHaveBeenCalledWith({
|
||||
config,
|
||||
workspaceDir: snapshot.workspaceDir,
|
||||
basePluginIds: ["telegram", "memory-core"],
|
||||
selections: [],
|
||||
metadataSnapshot: snapshot,
|
||||
});
|
||||
expect(hoisted.loadPluginRegistryHandle).toHaveBeenCalledWith({
|
||||
activate: false,
|
||||
activationSourceConfig: config,
|
||||
channelPluginLoadIntent: "full",
|
||||
config,
|
||||
discovery: snapshot.discovery,
|
||||
env,
|
||||
installRecords: {},
|
||||
manifestRegistry: snapshot.manifestRegistry,
|
||||
onlyPluginIds: ["codex", "memory-core"],
|
||||
runtimeOptions: undefined,
|
||||
workspaceDir: snapshot.workspaceDir,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -200,6 +264,7 @@ describe("agent runtime plugin registries", () => {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
basePluginIds: [],
|
||||
selections: [],
|
||||
metadataSnapshot: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { adoptRuntimeContextEngineRegistrations } from "../context-engine/registry.js";
|
||||
import { listRuntimePluginIdsFromRegistry } from "../plugins/active-runtime-registry.js";
|
||||
import { normalizePluginsConfig } from "../plugins/config-state.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { extractPluginInstallRecordsFromInstalledPluginIndex } from "../plugins/installed-plugin-index-install-records.js";
|
||||
import { loadPluginRegistryHandle } from "../plugins/loader.js";
|
||||
import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||
import type { PluginRegistry } from "../plugins/registry-types.js";
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import {
|
||||
@@ -17,31 +19,6 @@ import {
|
||||
type AgentHarnessPluginSelection,
|
||||
} from "./harness/runtime-plugin-load-plan.js";
|
||||
|
||||
type StartupScopedPluginSnapshot = NonNullable<
|
||||
ReturnType<typeof getCurrentPluginMetadataSnapshot>
|
||||
> & {
|
||||
startup?: {
|
||||
pluginIds?: readonly unknown[];
|
||||
};
|
||||
};
|
||||
|
||||
function resolveStartupPluginIdsFromCurrentSnapshot(params: {
|
||||
config?: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
workspaceDir?: string;
|
||||
}): string[] | undefined {
|
||||
const snapshot = getCurrentPluginMetadataSnapshot({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
}) as StartupScopedPluginSnapshot | undefined;
|
||||
const pluginIds = snapshot?.startup?.pluginIds;
|
||||
if (!Array.isArray(pluginIds)) {
|
||||
return undefined;
|
||||
}
|
||||
return pluginIds.filter((pluginId): pluginId is string => typeof pluginId === "string");
|
||||
}
|
||||
|
||||
type AgentRuntimePluginRegistryParams = {
|
||||
config?: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -50,10 +27,12 @@ type AgentRuntimePluginRegistryParams = {
|
||||
/** Explicit base scope for hosts without a Gateway startup registry. */
|
||||
basePluginIds?: readonly string[];
|
||||
selections?: readonly AgentHarnessPluginSelection[];
|
||||
/** Lifecycle-selected metadata. Omission selects one standalone cold generation. */
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
};
|
||||
|
||||
function resolveAgentRuntimePluginRegistryLoad(params: AgentRuntimePluginRegistryParams) {
|
||||
const workspaceDir =
|
||||
const requestedWorkspaceDir =
|
||||
typeof params.workspaceDir === "string" && params.workspaceDir.trim()
|
||||
? resolveUserPath(params.workspaceDir)
|
||||
: undefined;
|
||||
@@ -63,7 +42,7 @@ function resolveAgentRuntimePluginRegistryLoad(params: AgentRuntimePluginRegistr
|
||||
config: params.config,
|
||||
activationSourceConfig: params.config,
|
||||
...(params.env ? { env: params.env } : {}),
|
||||
workspaceDir,
|
||||
workspaceDir: requestedWorkspaceDir,
|
||||
onlyPluginIds: [],
|
||||
runtimeOptions: params.allowGatewaySubagentBinding
|
||||
? { allowGatewaySubagentBinding: true }
|
||||
@@ -71,18 +50,30 @@ function resolveAgentRuntimePluginRegistryLoad(params: AgentRuntimePluginRegistr
|
||||
},
|
||||
};
|
||||
}
|
||||
const metadataSnapshot =
|
||||
params.metadataSnapshot ??
|
||||
loadPluginMetadataSnapshot({
|
||||
config: params.config ?? {},
|
||||
env: params.env ?? process.env,
|
||||
...(requestedWorkspaceDir ? { workspaceDir: requestedWorkspaceDir } : {}),
|
||||
});
|
||||
const workspaceDir = metadataSnapshot.workspaceDir ?? requestedWorkspaceDir;
|
||||
const metadataLoadOptions = {
|
||||
...(metadataSnapshot.discovery ? { discovery: metadataSnapshot.discovery } : {}),
|
||||
installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(metadataSnapshot.index),
|
||||
manifestRegistry: metadataSnapshot.manifestRegistry,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
};
|
||||
const requestPluginRegistry = getPluginRuntimeGatewayRequestScope()?.pluginRegistry;
|
||||
const startupPluginIds =
|
||||
params.basePluginIds !== undefined
|
||||
? [...params.basePluginIds]
|
||||
: requestPluginRegistry
|
||||
? listRuntimePluginIdsFromRegistry(requestPluginRegistry)
|
||||
: resolveStartupPluginIdsFromCurrentSnapshot({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
workspaceDir,
|
||||
});
|
||||
const plan = resolveAgentRuntimePluginLoadPlan({
|
||||
: metadataSnapshot.pluginIds
|
||||
? [...metadataSnapshot.pluginIds]
|
||||
: undefined;
|
||||
const planParams = {
|
||||
config: params.config,
|
||||
workspaceDir: workspaceDir ?? process.cwd(),
|
||||
...(startupPluginIds === undefined ? {} : { basePluginIds: startupPluginIds }),
|
||||
@@ -94,13 +85,15 @@ function resolveAgentRuntimePluginRegistryLoad(params: AgentRuntimePluginRegistr
|
||||
})),
|
||||
...(params.selections ?? []),
|
||||
],
|
||||
});
|
||||
metadataSnapshot,
|
||||
};
|
||||
const plan = resolveAgentRuntimePluginLoadPlan(planParams);
|
||||
return {
|
||||
loadOptions: {
|
||||
config: plan.config,
|
||||
...(plan.config ? { activationSourceConfig: plan.config } : {}),
|
||||
...(params.env ? { env: params.env } : {}),
|
||||
workspaceDir,
|
||||
...metadataLoadOptions,
|
||||
...(startupPluginIds === undefined || plan.pluginIds === undefined
|
||||
? {}
|
||||
: { onlyPluginIds: plan.pluginIds }),
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
classifyAgentRunTerminalOutcome,
|
||||
type AgentRunTerminalOutcome,
|
||||
} from "../../agents/agent-run-terminal-outcome.js";
|
||||
import type { PreparedAgentCommandRuntimeContext } from "../../agents/command/prepare.js";
|
||||
import {
|
||||
createCronCreatorAuthorityCapability,
|
||||
runWithCronCreatorAuthorityCapability,
|
||||
@@ -127,6 +128,7 @@ export function dispatchAgentRunFromGateway(params: {
|
||||
taskTrackingMode: Exclude<GatewayAgentTaskTrackingMode, "plugin_subagent">;
|
||||
canonicalSkillWorkspaceDir?: string;
|
||||
restoreAdmittedRecovery?: () => Promise<MainSessionRecoveryPendingTarget | undefined>;
|
||||
commandRuntimeContext?: PreparedAgentCommandRuntimeContext;
|
||||
onSettled?: (outcome: {
|
||||
terminalOutcome: AgentRunTerminalOutcome;
|
||||
onRecovered?: () => void;
|
||||
@@ -197,6 +199,7 @@ export function dispatchAgentRunFromGateway(params: {
|
||||
{
|
||||
restoreAdmittedRecovery: params.restoreAdmittedRecovery,
|
||||
},
|
||||
params.commandRuntimeContext,
|
||||
),
|
||||
);
|
||||
const agentRun = cronCreatorAuthorityCapability
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type MainSessionRecoveryPendingTarget,
|
||||
type MainSessionRecoveryOwnerLease,
|
||||
} from "../../agents/main-session-recovery/main-session-recovery-store.js";
|
||||
import { loadPublishedGatewayReplyDispatchRuntime } from "../../agents/prepared-model-runtime.js";
|
||||
import { resolveScheduledToolPolicyContext } from "../../agents/scheduled-tool-policy.js";
|
||||
import { resolveIngressWorkspaceOverrideForSessionRun } from "../../agents/spawned-context.js";
|
||||
import { isExecutionIdentityCollectionEnabled } from "../../audit/audit-config.js";
|
||||
@@ -196,6 +197,14 @@ export function startAgentRunExecution(params: {
|
||||
const ingressAgentId = params.resolvedSessionKey
|
||||
? params.activeSessionAgentId
|
||||
: params.agentId;
|
||||
const replyDispatchRuntime = await loadPublishedGatewayReplyDispatchRuntime({
|
||||
agentId: params.activeSessionAgentId,
|
||||
});
|
||||
if (!replyDispatchRuntime?.pluginGeneration) {
|
||||
throw new Error(
|
||||
`prepared reply dispatch runtime was not published for ${params.activeSessionAgentId}`,
|
||||
);
|
||||
}
|
||||
// Plugin-owned additive grants stay internal to the authenticated in-process run.
|
||||
// Public agent params cannot supply them, and normal tool policy still filters them.
|
||||
const runtimePluginToolGrant =
|
||||
@@ -252,6 +261,10 @@ export function startAgentRunExecution(params: {
|
||||
dispatchAgentRunFromGateway(
|
||||
withAgentRunDispatchExecutionIdentity(
|
||||
{
|
||||
commandRuntimeContext: {
|
||||
config: replyDispatchRuntime.config,
|
||||
pluginGeneration: replyDispatchRuntime.pluginGeneration,
|
||||
},
|
||||
cronCreatorAuthority: prepared.cronCreatorAuthority,
|
||||
ingressOpts: {
|
||||
message,
|
||||
|
||||
@@ -153,6 +153,7 @@ export async function startGatewayCoreRuntime(input: {
|
||||
residentRegistry,
|
||||
shutdownRuntime,
|
||||
} = runtime;
|
||||
let currentPluginMetadataSnapshot = runtime.pluginMetadataSnapshot;
|
||||
if (desktopSessionRegistry) {
|
||||
kernel.addGatewayLifetimeSidecar({ stop: () => desktopSessionRegistry.stopAll() });
|
||||
}
|
||||
@@ -612,6 +613,7 @@ export async function startGatewayCoreRuntime(input: {
|
||||
env: params.env,
|
||||
workspaceDir: pluginWorkspaceDir,
|
||||
});
|
||||
currentPluginMetadataSnapshot = nextPluginMetadataSnapshot;
|
||||
replaceAttachedPluginRuntime(loaded);
|
||||
kernel.setPluginServices(null);
|
||||
if (previousPluginServices) {
|
||||
@@ -673,5 +675,6 @@ export async function startGatewayCoreRuntime(input: {
|
||||
loadGatewayModelCatalog,
|
||||
loadGatewayModelCatalogSnapshot,
|
||||
readPreparedGatewayModelCatalog,
|
||||
getPluginMetadataSnapshot: () => currentPluginMetadataSnapshot,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -151,6 +151,16 @@ vi.mock("../../commands/agent.js", () => ({
|
||||
agentCommandFromIngress: mocks.agentCommand,
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/prepared-model-runtime.js", () => ({
|
||||
// Direct handler tests bypass Gateway startup, so provide the lifecycle fact
|
||||
// that production publishes before admitting agent RPCs.
|
||||
loadPublishedGatewayReplyDispatchRuntime: async ({ agentId }: { agentId: string }) => ({
|
||||
agentId,
|
||||
config: mocks.loadConfigReturn,
|
||||
pluginGeneration: { pluginMetadataSnapshot: {} },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../acp/runtime/session-meta.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../acp/runtime/session-meta.js")>(
|
||||
"../../acp/runtime/session-meta.js",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CliDeps } from "../cli/deps.types.js";
|
||||
import type { ConfigFileSnapshot, OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { HeartbeatRunner } from "../infra/heartbeat-runner.js";
|
||||
import type { GatewayRestartEmitter } from "../infra/restart.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||
import type { ChannelHealthMonitor } from "./channel-health-monitor.js";
|
||||
import type { ChannelKind } from "./config-reload-plan.js";
|
||||
import type { GatewayReloadPlan } from "./config-reload.js";
|
||||
@@ -143,6 +144,7 @@ export type GatewayReloadHandlerParams = {
|
||||
broadcast: (event: string, payload: unknown, opts?: { dropIfSlow?: boolean }) => void;
|
||||
getState: () => GatewayHotReloadState;
|
||||
setState: (state: GatewayHotReloadState) => void;
|
||||
getPluginMetadataSnapshot?: () => PluginMetadataSnapshot | undefined;
|
||||
startChannel: GatewayChannelManager["startChannel"];
|
||||
stopChannel: GatewayChannelManager["stopChannel"];
|
||||
getChannelAutostartSuppression?: GatewayChannelManager["getAutostartSuppression"];
|
||||
|
||||
@@ -576,9 +576,11 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams)
|
||||
}
|
||||
|
||||
try {
|
||||
const pluginMetadataSnapshot = params.getPluginMetadataSnapshot?.();
|
||||
await refreshPreparedModelRuntimeSnapshots(nextConfig, {
|
||||
catalogMode: "static",
|
||||
allowGatewaySubagentBinding: true,
|
||||
...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}),
|
||||
});
|
||||
} catch (err) {
|
||||
scheduleRecoveryRestart("prepared model runtime reload", err);
|
||||
|
||||
@@ -135,6 +135,7 @@ export function startManagedGatewayConfigReloader(
|
||||
broadcast: params.broadcast,
|
||||
getState: params.getState,
|
||||
setState: params.setState,
|
||||
getPluginMetadataSnapshot: params.getPluginMetadataSnapshot,
|
||||
startChannel: params.startChannel,
|
||||
stopChannel: params.stopChannel,
|
||||
getChannelAutostartSuppression: params.getChannelAutostartSuppression,
|
||||
|
||||
@@ -551,7 +551,7 @@ export async function prepareGatewayServerBootstrap(input: {
|
||||
pluginWorkspaceDir,
|
||||
startupPluginIds,
|
||||
pluginManifestRecords,
|
||||
pluginMetadataSnapshot,
|
||||
pluginMetadataSnapshot: currentPluginMetadataSnapshot,
|
||||
pluginLookUpTable,
|
||||
baseMethods,
|
||||
ambientAutostartSuppressedChannelIds,
|
||||
|
||||
@@ -91,6 +91,7 @@ export async function finishGatewayStartup(params: {
|
||||
baseMethods,
|
||||
startupPluginIds,
|
||||
pluginManifestRecords,
|
||||
pluginMetadataSnapshot,
|
||||
pluginLookUpTable,
|
||||
ambientEnvTriggers,
|
||||
replaceAttachedPluginRuntime,
|
||||
@@ -126,6 +127,7 @@ export async function finishGatewayStartup(params: {
|
||||
gatewayRequestContext,
|
||||
gatewayInstanceRuntime,
|
||||
residentRegistry,
|
||||
getPluginMetadataSnapshot,
|
||||
} = runtime;
|
||||
const unregisterGatewayLifetimeSidecar = (sidecar: GatewayPostReadySidecarHandle) => {
|
||||
kernel.setGatewayLifetimeSidecars(
|
||||
@@ -255,6 +257,7 @@ export async function finishGatewayStartup(params: {
|
||||
gatewayPluginConfigAtStart,
|
||||
activationSourceConfig: startupActivationSourceConfig,
|
||||
pluginManifestRecords,
|
||||
...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}),
|
||||
ambientEnvTriggers,
|
||||
pluginRegistry: pluginRuntime.registry,
|
||||
defaultWorkspaceDir,
|
||||
@@ -393,6 +396,7 @@ export async function finishGatewayStartup(params: {
|
||||
cronStartState.handled = true;
|
||||
}
|
||||
},
|
||||
getPluginMetadataSnapshot,
|
||||
startChannel,
|
||||
stopChannel,
|
||||
getChannelAutostartSuppression: channelManager.getAutostartSuppression,
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
||||
import type { PluginHookGatewayCronService } from "../plugins/hook-types.js";
|
||||
import type { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { getPluginModuleLoaderStats } from "../plugins/plugin-module-loader-cache.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { PluginServicesHandle } from "../plugins/services.js";
|
||||
@@ -429,6 +430,7 @@ async function waitForAcpRuntimeBackendReady(params: {
|
||||
|
||||
async function prewarmConfiguredPrimaryModel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
workspaceDir?: string;
|
||||
log: { warn: (msg: string) => void };
|
||||
startupTrace?: GatewayStartupTrace;
|
||||
@@ -499,6 +501,7 @@ async function hydrateConfiguredExternalCliAuth(params: {
|
||||
|
||||
async function publishConfiguredModelRuntimeSnapshots(params: {
|
||||
cfg: OpenClawConfig;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
workspaceDir?: string;
|
||||
log: { warn: (msg: string) => void };
|
||||
startupTrace?: GatewayStartupTrace;
|
||||
@@ -509,6 +512,9 @@ async function publishConfiguredModelRuntimeSnapshots(params: {
|
||||
gatewayLifecycle: true,
|
||||
catalogMode: "static",
|
||||
allowGatewaySubagentBinding: true,
|
||||
...(params.pluginMetadataSnapshot
|
||||
? { pluginMetadataSnapshot: params.pluginMetadataSnapshot }
|
||||
: {}),
|
||||
...(params.workspaceDir ? { defaultWorkspaceDir: params.workspaceDir } : {}),
|
||||
...(params.startupTrace
|
||||
? {
|
||||
@@ -544,6 +550,7 @@ async function publishConfiguredModelRuntimeSnapshots(params: {
|
||||
async function publishStartupModelRuntime(
|
||||
params: {
|
||||
cfg: OpenClawConfig;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
workspaceDir?: string;
|
||||
log: { warn: (msg: string) => void };
|
||||
startupTrace?: GatewayStartupTrace;
|
||||
@@ -559,6 +566,7 @@ async function publishStartupModelRuntime(
|
||||
/** Start post-ready sidecars such as channels, hooks, plugin services, and cleanup tasks. */
|
||||
export async function startGatewaySidecars(params: {
|
||||
cfg: OpenClawConfig;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
pluginRegistry: ReturnType<typeof loadOpenClawPlugins>;
|
||||
defaultWorkspaceDir: string;
|
||||
deps: CliDeps;
|
||||
@@ -648,6 +656,9 @@ export async function startGatewaySidecars(params: {
|
||||
publishStartupModelRuntime(
|
||||
{
|
||||
cfg: params.cfg,
|
||||
...(params.pluginMetadataSnapshot
|
||||
? { pluginMetadataSnapshot: params.pluginMetadataSnapshot }
|
||||
: {}),
|
||||
workspaceDir: params.defaultWorkspaceDir,
|
||||
log: params.log,
|
||||
startupTrace: params.startupTrace,
|
||||
@@ -1097,6 +1108,7 @@ export async function startGatewayPostAttachRuntime(
|
||||
gatewayPluginConfigAtStart: OpenClawConfig;
|
||||
activationSourceConfig: OpenClawConfig;
|
||||
pluginManifestRecords: readonly PluginManifestRecord[];
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
ambientEnvTriggers?: AmbientEnvTriggerPolicy;
|
||||
pluginRegistry: ReturnType<typeof loadOpenClawPlugins>;
|
||||
defaultWorkspaceDir: string;
|
||||
@@ -1339,6 +1351,9 @@ export async function startGatewayPostAttachRuntime(
|
||||
return await measureStartup(params.startupTrace, "sidecars.total", () =>
|
||||
runtimeDeps.startGatewaySidecars({
|
||||
cfg: params.gatewayPluginConfigAtStart,
|
||||
...(params.pluginMetadataSnapshot
|
||||
? { pluginMetadataSnapshot: params.pluginMetadataSnapshot }
|
||||
: {}),
|
||||
pluginRegistry,
|
||||
defaultWorkspaceDir: params.defaultWorkspaceDir,
|
||||
deps: params.deps,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
connectWebchatClient,
|
||||
installGatewayTestHooks,
|
||||
onceMessage,
|
||||
prepareGatewayReplyRuntimeForTest,
|
||||
rpcReq,
|
||||
startConnectedServerWithClient,
|
||||
startServerWithClient,
|
||||
@@ -153,10 +154,11 @@ async function writeMainSessionEntry(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function sendAgentWsRequest(
|
||||
async function sendAgentWsRequest(
|
||||
socket: WebSocket,
|
||||
params: { reqId: string; message: string; idempotencyKey: string; sessionKey?: string },
|
||||
) {
|
||||
await prepareGatewayReplyRuntimeForTest();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "req",
|
||||
@@ -180,7 +182,7 @@ async function sendAgentWsRequestAndWaitFinal(
|
||||
(o) => o.type === "res" && o.id === params.reqId && o.payload?.status !== "accepted",
|
||||
params.timeoutMs,
|
||||
);
|
||||
sendAgentWsRequest(socket, params);
|
||||
await sendAgentWsRequest(socket, params);
|
||||
return await finalP;
|
||||
}
|
||||
|
||||
@@ -473,7 +475,7 @@ describe("gateway server agent", () => {
|
||||
ws,
|
||||
(o) => o.type === "res" && o.id === "ag1" && o.payload?.status !== "accepted",
|
||||
);
|
||||
sendAgentWsRequest(ws, {
|
||||
await sendAgentWsRequest(ws, {
|
||||
reqId: "ag1",
|
||||
message: "hi",
|
||||
idempotencyKey: "idem-ag",
|
||||
@@ -508,7 +510,7 @@ describe("gateway server agent", () => {
|
||||
message.type === "res" && message.id === runId && message.payload?.status !== "accepted",
|
||||
);
|
||||
|
||||
sendAgentWsRequest(ws, {
|
||||
await sendAgentWsRequest(ws, {
|
||||
reqId: runId,
|
||||
message: "persist this agent turn before ACK",
|
||||
sessionKey: "main",
|
||||
@@ -569,7 +571,7 @@ describe("gateway server agent", () => {
|
||||
message.type === "res" && message.id === runId && message.payload?.status !== "accepted",
|
||||
);
|
||||
|
||||
sendAgentWsRequest(ws, {
|
||||
await sendAgentWsRequest(ws, {
|
||||
reqId: runId,
|
||||
message: "keep this aborted agent turn queryable",
|
||||
sessionKey: "main",
|
||||
@@ -666,7 +668,7 @@ describe("gateway server agent", () => {
|
||||
});
|
||||
|
||||
const secondP = onceMessage(ws, (o) => o.type === "res" && o.id === "ag2");
|
||||
sendAgentWsRequest(ws, {
|
||||
await sendAgentWsRequest(ws, {
|
||||
reqId: "ag2",
|
||||
message: "hi again",
|
||||
idempotencyKey: "same-agent",
|
||||
|
||||
@@ -4,7 +4,12 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vit
|
||||
import type { RawData, WebSocket } from "ws";
|
||||
import { createDeferred } from "../../test/helpers/promise.js";
|
||||
import { startGatewayServerHarness, type GatewayServerHarness } from "./server.e2e-ws-harness.js";
|
||||
import { agentCommandMock, installGatewayTestHooks, onceMessage } from "./test-helpers.js";
|
||||
import {
|
||||
agentCommandMock,
|
||||
installGatewayTestHooks,
|
||||
onceMessage,
|
||||
prepareGatewayReplyRuntimeForTest,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks({ scope: "suite" });
|
||||
|
||||
@@ -32,8 +37,9 @@ beforeAll(async () => {
|
||||
harness = await startGatewayServerHarness();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(agentCommandMock).mockReset();
|
||||
await prepareGatewayReplyRuntimeForTest();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
agentCommandMock,
|
||||
getGatewayTestPort,
|
||||
installGatewayTestHooks,
|
||||
prepareGatewayReplyRuntimeForTest,
|
||||
startTestGatewayServer,
|
||||
setTestPluginRegistry,
|
||||
testState,
|
||||
@@ -154,10 +155,11 @@ beforeAll(async () => {
|
||||
server = await startTestGatewayServer(gatewayPort);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
testState.gatewayAuth = { mode: "token", token: gatewayToken };
|
||||
process.env.OPENCLAW_GATEWAY_PORT = String(gatewayPort);
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN = gatewayToken;
|
||||
await prepareGatewayReplyRuntimeForTest();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -664,6 +666,7 @@ describe("sessions_send agent targeting", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
await prepareGatewayReplyRuntimeForTest({ force: true });
|
||||
|
||||
const spy = agentCommandMock as unknown as Mock<(opts: unknown) => Promise<void>>;
|
||||
spy.mockImplementation(async (opts: unknown) =>
|
||||
@@ -803,6 +806,7 @@ describe("sessions_send direct-message requester routing", () => {
|
||||
[targetSessionKey]: { sessionId: "dm-scope-orion", updatedAt: Date.now() },
|
||||
},
|
||||
});
|
||||
await prepareGatewayReplyRuntimeForTest({ force: true });
|
||||
|
||||
const spy = agentCommandMock as unknown as Mock<(opts: unknown) => Promise<void>>;
|
||||
spy.mockReset();
|
||||
|
||||
@@ -103,6 +103,7 @@ let tempControlUiRoot: string | undefined;
|
||||
let suiteConfigRootSeq = 0;
|
||||
let lastSyncedSessionStorePath: string | undefined;
|
||||
let lastSyncedSessionConfigJson: string | undefined;
|
||||
let gatewayReplyRuntimePrepared = false;
|
||||
let activeSuiteGatewayServerCount = 0;
|
||||
let activeSuiteHookScopeCount = 0;
|
||||
// Gateway tests exercise RPC/server behavior, not production bind auto-detection by default.
|
||||
@@ -478,6 +479,7 @@ async function resetGatewayTestState(options: { uniqueConfigRoot: boolean }) {
|
||||
resetAgentEventsForTest();
|
||||
const mod = await getServerModule();
|
||||
await mod.resetPreparedModelCatalogForTest();
|
||||
gatewayReplyRuntimePrepared = false;
|
||||
agentDiscoveryMock.enabled = false;
|
||||
agentDiscoveryMock.discoverCalls = 0;
|
||||
agentDiscoveryMock.models = [];
|
||||
@@ -525,6 +527,28 @@ async function resetGatewayTestRuntimeOnly() {
|
||||
drainSystemEvents(sessionKey);
|
||||
}
|
||||
resetAgentEventsForTest({ preserveListeners: true });
|
||||
gatewayReplyRuntimePrepared = false;
|
||||
}
|
||||
|
||||
export async function prepareGatewayReplyRuntimeForTest(options?: {
|
||||
force?: boolean;
|
||||
}): Promise<void> {
|
||||
if (
|
||||
process.env.OPENCLAW_TEST_MINIMAL_GATEWAY !== "1" ||
|
||||
(!options?.force && gatewayReplyRuntimePrepared)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const [preparedRuntime, configRuntime] = await Promise.all([
|
||||
import("../agents/prepared-model-runtime.js"),
|
||||
import("../config/io.js"),
|
||||
]);
|
||||
await preparedRuntime.refreshPreparedModelRuntimeSnapshots(configRuntime.getRuntimeConfig(), {
|
||||
gatewayLifecycle: true,
|
||||
catalogMode: "static",
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
gatewayReplyRuntimePrepared = true;
|
||||
}
|
||||
|
||||
export function installGatewayTestHooks(options?: { scope?: "test" | "suite" }) {
|
||||
@@ -1229,6 +1253,9 @@ export async function rpcReq<T extends Record<string, unknown>>(
|
||||
// observes the updated test fixture state.
|
||||
resetConfigRuntimeState();
|
||||
clearSessionStoreCacheForTest();
|
||||
if (method === "agent" || method === "chat.send") {
|
||||
await prepareGatewayReplyRuntimeForTest();
|
||||
}
|
||||
const { randomUUID } = await import("node:crypto");
|
||||
const id = randomUUID();
|
||||
const responsePromise = onceMessage<{
|
||||
|
||||
@@ -22,6 +22,7 @@ export {
|
||||
getTrackedConnectChallengeNonce,
|
||||
installGatewayTestHooks,
|
||||
onceMessage,
|
||||
prepareGatewayReplyRuntimeForTest,
|
||||
readConnectChallengeNonce,
|
||||
rpcReq,
|
||||
startConnectedServerWithClient,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getCurrentPluginMetadataSnapshot,
|
||||
installTemporaryCurrentPluginMetadataSnapshot,
|
||||
isCurrentPluginMetadataSnapshotRuntimeGeneration,
|
||||
setCurrentPluginMetadataSnapshot,
|
||||
withPluginMetadataSnapshotScope,
|
||||
} from "./current-plugin-metadata-snapshot.js";
|
||||
@@ -166,23 +167,39 @@ describe("current plugin metadata snapshot", () => {
|
||||
).toBe(globalSnapshot);
|
||||
});
|
||||
|
||||
it("carries prepared metadata and registry as one runtime generation", async () => {
|
||||
it("carries prepared metadata and registry across nested agent workspaces", async () => {
|
||||
const config = { plugins: { allow: ["scoped"] } };
|
||||
const workspaceDir = "/workspace/scoped";
|
||||
const metadataSnapshot = createSnapshot({ config, workspaceDir });
|
||||
const pluginWorkspaceDir = "/workspace/plugins";
|
||||
const agentWorkspaceDir = "/workspace/agent-run";
|
||||
const metadataSnapshot = createSnapshot({ config, workspaceDir: pluginWorkspaceDir });
|
||||
const pluginRegistry = createEmptyPluginRegistry();
|
||||
setCurrentPluginMetadataSnapshot(undefined);
|
||||
|
||||
await withPluginRuntimeGenerationScope(
|
||||
{ config, metadataSnapshot, pluginRegistry, workspaceDir },
|
||||
{ config, metadataSnapshot, pluginRegistry },
|
||||
async () => {
|
||||
await Promise.resolve();
|
||||
expect(getCurrentPluginMetadataSnapshot({ config, workspaceDir })).toBe(metadataSnapshot);
|
||||
expect(getCurrentPluginMetadataSnapshot({ config, workspaceDir: agentWorkspaceDir })).toBe(
|
||||
metadataSnapshot,
|
||||
);
|
||||
expect(getCurrentPluginMetadataSnapshot({ config, workspaceDir: pluginWorkspaceDir })).toBe(
|
||||
metadataSnapshot,
|
||||
);
|
||||
expect(
|
||||
getCurrentPluginMetadataSnapshot({
|
||||
config: { plugins: { allow: ["derived-run-policy"] } },
|
||||
workspaceDir: agentWorkspaceDir,
|
||||
}),
|
||||
).toBe(metadataSnapshot);
|
||||
expect(isCurrentPluginMetadataSnapshotRuntimeGeneration(metadataSnapshot)).toBe(true);
|
||||
expect(getPluginRuntimeGatewayRequestScope()?.pluginRegistry).toBe(pluginRegistry);
|
||||
},
|
||||
);
|
||||
|
||||
expect(getCurrentPluginMetadataSnapshot({ config, workspaceDir })).toBeUndefined();
|
||||
expect(isCurrentPluginMetadataSnapshotRuntimeGeneration(metadataSnapshot)).toBe(false);
|
||||
expect(
|
||||
getCurrentPluginMetadataSnapshot({ config, workspaceDir: agentWorkspaceDir }),
|
||||
).toBeUndefined();
|
||||
expect(getPluginRuntimeGatewayRequestScope()).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -217,7 +234,6 @@ describe("current plugin metadata snapshot", () => {
|
||||
config: outerConfig,
|
||||
metadataSnapshot: outerSnapshot,
|
||||
pluginRegistry: outerRegistry,
|
||||
workspaceDir: "/workspace/outer",
|
||||
},
|
||||
async () => {
|
||||
await expect(
|
||||
@@ -225,7 +241,6 @@ describe("current plugin metadata snapshot", () => {
|
||||
{
|
||||
config: innerConfig,
|
||||
metadataSnapshot: innerSnapshot,
|
||||
workspaceDir: "/workspace/inner",
|
||||
},
|
||||
async () => {
|
||||
await Promise.resolve();
|
||||
@@ -414,14 +429,11 @@ describe("current plugin metadata snapshot", () => {
|
||||
const workspaceDir = "/workspace";
|
||||
const snapshot = createSnapshot({ config: sourceConfig, workspaceDir });
|
||||
|
||||
withPluginRuntimeGenerationScope(
|
||||
{ config: runtimeConfig, metadataSnapshot: snapshot, workspaceDir },
|
||||
() => {
|
||||
expect(getCurrentPluginMetadataSnapshot({ config: runtimeConfig, workspaceDir })).toBe(
|
||||
snapshot,
|
||||
);
|
||||
},
|
||||
);
|
||||
withPluginRuntimeGenerationScope({ config: runtimeConfig, metadataSnapshot: snapshot }, () => {
|
||||
expect(getCurrentPluginMetadataSnapshot({ config: runtimeConfig, workspaceDir })).toBe(
|
||||
snapshot,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a workspace-scoped snapshot when the caller does not provide workspace scope", () => {
|
||||
|
||||
@@ -63,6 +63,7 @@ type PluginMetadataSnapshotCandidate = {
|
||||
compatiblePolicyHashes?: readonly string[];
|
||||
compatibleConfigFingerprints?: readonly string[];
|
||||
hasConfigIdentity?: (config: OpenClawConfig) => boolean;
|
||||
immutableRuntimeGeneration?: boolean;
|
||||
};
|
||||
|
||||
type ScopedPluginMetadataSnapshot = PluginMetadataSnapshotCandidate & {
|
||||
@@ -300,6 +301,7 @@ export function withPluginMetadataSnapshotScope<T>(
|
||||
compatiblePolicyHashes,
|
||||
compatibleConfigFingerprints,
|
||||
hasConfigIdentity: (config) => configIdentities.has(config),
|
||||
immutableRuntimeGeneration: options.trustConfigIdentity === true,
|
||||
parent: scopedPluginMetadataSnapshot.getStore(),
|
||||
},
|
||||
run,
|
||||
@@ -333,6 +335,11 @@ function resolveCompatiblePluginMetadataSnapshot(
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
// Immutable runtime generations already selected their executable plugin graph. Nested config
|
||||
// and workspace projections are run data, not authority to reopen lifecycle-owned discovery.
|
||||
if (candidate.immutableRuntimeGeneration) {
|
||||
return snapshot;
|
||||
}
|
||||
const requestedWorkspaceDir =
|
||||
params.workspaceDir ??
|
||||
(params.allowWorkspaceScopedSnapshot === true || options.scopedOwnerContext === true
|
||||
@@ -398,6 +405,17 @@ function resolveCompatiblePluginMetadataSnapshot(
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export function isCurrentPluginMetadataSnapshotRuntimeGeneration(
|
||||
snapshot: PluginMetadataSnapshot,
|
||||
): boolean {
|
||||
for (let scoped = scopedPluginMetadataSnapshot.getStore(); scoped; scoped = scoped.parent) {
|
||||
if (scoped.snapshot === snapshot && scoped.immutableRuntimeGeneration === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getCurrentPluginMetadataSnapshot(
|
||||
params: CurrentPluginMetadataSnapshotParams = {},
|
||||
): PluginMetadataSnapshot | undefined {
|
||||
|
||||
@@ -4,7 +4,10 @@ import {
|
||||
getActiveDiagnosticsTimelineSpan,
|
||||
measureDiagnosticsTimelineSpanSync,
|
||||
} from "../infra/diagnostics-timeline.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
|
||||
import {
|
||||
getCurrentPluginMetadataSnapshot,
|
||||
isCurrentPluginMetadataSnapshotRuntimeGeneration,
|
||||
} from "./current-plugin-metadata-snapshot.js";
|
||||
import { resolveActivePluginInstallRoots } from "./install-root-context.js";
|
||||
import { hashJson } from "./installed-plugin-index-hash.js";
|
||||
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
|
||||
@@ -335,7 +338,7 @@ export function completePluginMetadataSnapshot(params: {
|
||||
}
|
||||
|
||||
/** Reuses process-stable plugin facts for a workspace proven to have no plugin root. */
|
||||
export function projectPluginMetadataSnapshotWorkspace(params: {
|
||||
function projectPluginMetadataSnapshotWorkspace(params: {
|
||||
snapshot: PluginMetadataSnapshot;
|
||||
config: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -418,7 +421,7 @@ export function resolvePluginMetadataSnapshot(
|
||||
}
|
||||
return loadPluginMetadataSnapshot(params);
|
||||
}
|
||||
if (!params.index) {
|
||||
if (!params.index || isCurrentPluginMetadataSnapshotRuntimeGeneration(current)) {
|
||||
return current;
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -21,7 +21,6 @@ export function withPluginRuntimeGenerationScope<T>(
|
||||
config: OpenClawConfig;
|
||||
metadataSnapshot: PluginMetadataSnapshot;
|
||||
pluginRegistry?: PluginRegistry;
|
||||
workspaceDir?: string;
|
||||
},
|
||||
run: () => T,
|
||||
): T {
|
||||
@@ -35,7 +34,9 @@ export function withPluginRuntimeGenerationScope<T>(
|
||||
{
|
||||
config: generation.config,
|
||||
trustConfigIdentity: true,
|
||||
...(generation.workspaceDir ? { workspaceDir: generation.workspaceDir } : {}),
|
||||
...(generation.metadataSnapshot.workspaceDir
|
||||
? { workspaceDir: generation.metadataSnapshot.workspaceDir }
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import {
|
||||
getCurrentPluginMetadataSnapshot,
|
||||
setCurrentPluginMetadataSnapshot,
|
||||
} from "../current-plugin-metadata-snapshot.js";
|
||||
import { resolveInstalledPluginIndexPolicyHash } from "../installed-plugin-index-policy.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "../plugin-metadata-lifecycle.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugin-metadata-snapshot.types.js";
|
||||
import { resolvePluginRuntimeLoadContext } from "./load-context.js";
|
||||
|
||||
const resolvePluginMetadataSnapshotMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../plugin-metadata-snapshot.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../plugin-metadata-snapshot.js")>()),
|
||||
resolvePluginMetadataSnapshot: resolvePluginMetadataSnapshotMock,
|
||||
}));
|
||||
|
||||
function createSnapshot(params: {
|
||||
config: OpenClawConfig;
|
||||
workspaceDir: string;
|
||||
}): PluginMetadataSnapshot {
|
||||
const policyHash = resolveInstalledPluginIndexPolicyHash(params.config);
|
||||
return {
|
||||
policyHash,
|
||||
workspaceDir: params.workspaceDir,
|
||||
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) => 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,
|
||||
},
|
||||
discovery: { candidates: [], diagnostics: [] },
|
||||
};
|
||||
}
|
||||
|
||||
describe("plugin runtime load context current snapshot ownership", () => {
|
||||
afterEach(() => {
|
||||
resolvePluginMetadataSnapshotMock.mockReset();
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
});
|
||||
|
||||
it("keeps operation-local metadata from replacing the Gateway lifecycle snapshot", () => {
|
||||
const lifecycleConfig = { plugins: { allow: ["lifecycle"] } };
|
||||
const operationConfig = { plugins: { allow: ["operation"] } };
|
||||
const lifecycleWorkspace = "/workspace/lifecycle";
|
||||
const operationWorkspace = "/workspace/operation";
|
||||
const lifecycleSnapshot = createSnapshot({
|
||||
config: lifecycleConfig,
|
||||
workspaceDir: lifecycleWorkspace,
|
||||
});
|
||||
const operationSnapshot = createSnapshot({
|
||||
config: operationConfig,
|
||||
workspaceDir: operationWorkspace,
|
||||
});
|
||||
setCurrentPluginMetadataSnapshot(lifecycleSnapshot, {
|
||||
config: lifecycleConfig,
|
||||
workspaceDir: lifecycleWorkspace,
|
||||
});
|
||||
resolvePluginMetadataSnapshotMock.mockReturnValue(operationSnapshot);
|
||||
|
||||
const context = resolvePluginRuntimeLoadContext({
|
||||
config: operationConfig,
|
||||
workspaceDir: operationWorkspace,
|
||||
});
|
||||
|
||||
expect(context.metadataSnapshot).toBe(operationSnapshot);
|
||||
expect(
|
||||
getCurrentPluginMetadataSnapshot({
|
||||
config: lifecycleConfig,
|
||||
workspaceDir: lifecycleWorkspace,
|
||||
}),
|
||||
).toBe(lifecycleSnapshot);
|
||||
expect(
|
||||
getCurrentPluginMetadataSnapshot({
|
||||
config: operationConfig,
|
||||
workspaceDir: operationWorkspace,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -32,8 +32,6 @@ const rebasePluginMetadataSnapshotManifestRegistryMock = vi.fn(
|
||||
);
|
||||
const resolveConfigWidePluginManifestRegistryMock = vi.fn(() => manifestRegistry);
|
||||
const isPluginMetadataSnapshotCompatibleMock = vi.fn(() => true);
|
||||
const getCurrentPluginMetadataSnapshotMock = vi.fn(() => undefined);
|
||||
const setCurrentPluginMetadataSnapshotMock = vi.fn();
|
||||
|
||||
let resolvePluginRuntimeLoadContext: typeof import("./load-context.js").resolvePluginRuntimeLoadContext;
|
||||
let buildPluginRuntimeLoadOptions: typeof import("./load-context.js").buildPluginRuntimeLoadOptions;
|
||||
@@ -70,11 +68,6 @@ vi.mock("../plugin-metadata-snapshot.js", () => ({
|
||||
resolvePluginMetadataSnapshot: loadPluginMetadataSnapshotMock,
|
||||
}));
|
||||
|
||||
vi.mock("../current-plugin-metadata-snapshot.js", () => ({
|
||||
getCurrentPluginMetadataSnapshot: getCurrentPluginMetadataSnapshotMock,
|
||||
setCurrentPluginMetadataSnapshot: setCurrentPluginMetadataSnapshotMock,
|
||||
}));
|
||||
|
||||
describe("resolvePluginRuntimeLoadContext", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
@@ -87,15 +80,11 @@ describe("resolvePluginRuntimeLoadContext", () => {
|
||||
applyPluginAutoEnableMock.mockReset();
|
||||
fingerprintPluginAutoEnableConfigMock.mockClear();
|
||||
fingerprintPluginAutoEnableEnvMock.mockClear();
|
||||
getCurrentPluginMetadataSnapshotMock.mockReset();
|
||||
getCurrentPluginMetadataSnapshotMock.mockReturnValue(undefined);
|
||||
isPluginMetadataSnapshotCompatibleMock.mockReset();
|
||||
isPluginMetadataSnapshotCompatibleMock.mockReturnValue(true);
|
||||
loadPluginMetadataSnapshotMock.mockClear();
|
||||
rebasePluginMetadataSnapshotManifestRegistryMock.mockClear();
|
||||
resolveConfigWidePluginManifestRegistryMock.mockClear();
|
||||
getCurrentPluginMetadataSnapshotMock.mockClear();
|
||||
setCurrentPluginMetadataSnapshotMock.mockClear();
|
||||
resolvePluginControlPlaneWorkspaceMock.mockClear();
|
||||
|
||||
loadConfigMock.mockReturnValue({ plugins: {} });
|
||||
@@ -156,12 +145,6 @@ describe("resolvePluginRuntimeLoadContext", () => {
|
||||
env,
|
||||
manifestRegistry,
|
||||
});
|
||||
expect(setCurrentPluginMetadataSnapshotMock).toHaveBeenCalledWith(metadataSnapshot, {
|
||||
config: rawConfig,
|
||||
compatibleConfigs: [resolvedConfig, rawConfig],
|
||||
env,
|
||||
workspaceDir: "/resolved-workspace",
|
||||
});
|
||||
expect(resolvePluginControlPlaneWorkspaceMock).toHaveBeenNthCalledWith(1, {
|
||||
config: rawConfig,
|
||||
env,
|
||||
@@ -193,24 +176,19 @@ describe("resolvePluginRuntimeLoadContext", () => {
|
||||
expect(loadPluginMetadataSnapshotMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stores derived metadata as the reusable runtime snapshot", () => {
|
||||
it("keeps derived metadata operation-local", () => {
|
||||
const derivedSnapshot = { ...metadataSnapshot } as typeof metadataSnapshot & {
|
||||
registrySource: "derived";
|
||||
};
|
||||
derivedSnapshot.registrySource = "derived";
|
||||
loadPluginMetadataSnapshotMock.mockReturnValueOnce(derivedSnapshot);
|
||||
|
||||
resolvePluginRuntimeLoadContext({
|
||||
const context = resolvePluginRuntimeLoadContext({
|
||||
config: { plugins: {} },
|
||||
env: { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(setCurrentPluginMetadataSnapshotMock).toHaveBeenCalledWith(derivedSnapshot, {
|
||||
config: { plugins: {} },
|
||||
compatibleConfigs: [{ plugins: {} }, { plugins: {} }],
|
||||
env: { HOME: "/tmp/openclaw-home" },
|
||||
workspaceDir: "/resolved-workspace",
|
||||
});
|
||||
expect(context.metadataSnapshot).toBe(derivedSnapshot);
|
||||
});
|
||||
|
||||
it("uses the source runtime snapshot for plugin activation source config", () => {
|
||||
@@ -327,7 +305,6 @@ describe("resolvePluginRuntimeLoadContext", () => {
|
||||
pluginIds,
|
||||
workspaceDir: "/resolved-workspace",
|
||||
});
|
||||
expect(setCurrentPluginMetadataSnapshotMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("builds plugin load options from the shared runtime context", () => {
|
||||
|
||||
@@ -11,7 +11,6 @@ import type { PluginInstallRecord } from "../../config/types.plugins.js";
|
||||
import { createSubsystemLogger } from "../../logging.js";
|
||||
import { resolvePluginActivationSourceConfig } from "../activation-source-config.js";
|
||||
import { resolvePluginControlPlaneWorkspace } from "../control-plane-workspace.js";
|
||||
import { setCurrentPluginMetadataSnapshot } from "../current-plugin-metadata-snapshot.js";
|
||||
import { extractPluginInstallRecordsFromInstalledPluginIndex } from "../installed-plugin-index-install-records.js";
|
||||
import type { PluginLoadOptions } from "../loader.js";
|
||||
import type { PluginManifestRegistry } from "../manifest-registry.js";
|
||||
@@ -252,16 +251,6 @@ export function resolvePluginRuntimeLoadContext(
|
||||
const installRecords = metadataSnapshot
|
||||
? extractPluginInstallRecordsFromInstalledPluginIndex(metadataSnapshot.index)
|
||||
: undefined;
|
||||
if (metadataSnapshot && metadataSnapshot.pluginIds === undefined) {
|
||||
// Scoped graphs are request-local; publishing one would hide other installed
|
||||
// providers from process-wide model normalization and later runtime loads.
|
||||
setCurrentPluginMetadataSnapshot(metadataSnapshot, {
|
||||
config: rawConfig,
|
||||
compatibleConfigs: [config, activationSourceConfig],
|
||||
env,
|
||||
workspaceDir,
|
||||
});
|
||||
}
|
||||
return {
|
||||
rawConfig,
|
||||
config,
|
||||
|
||||
@@ -1035,6 +1035,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
const context = createContext();
|
||||
const config = context.config;
|
||||
const registry = createToolRegistry([createOptionalDemoEntry()]);
|
||||
const preparedConfig = structuredClone(config);
|
||||
const metadataSnapshot = installToolManifestSnapshots({
|
||||
config,
|
||||
plugins: [
|
||||
@@ -1048,11 +1049,11 @@ describe("resolvePluginTools optional tools", () => {
|
||||
...createResolveToolsParams({ context, toolAllowlist: ["optional_tool"] }),
|
||||
preparedRuntime: {
|
||||
loadContext: {
|
||||
rawConfig: config,
|
||||
config,
|
||||
activationSourceConfig: config,
|
||||
rawConfig: preparedConfig,
|
||||
config: preparedConfig,
|
||||
activationSourceConfig: preparedConfig,
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir: "/tmp",
|
||||
workspaceDir: "/gateway/plugin-runtime",
|
||||
env: process.env,
|
||||
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
manifestRegistry: metadataSnapshot.manifestRegistry as never,
|
||||
|
||||
@@ -1177,11 +1177,9 @@ function resolvePluginToolLoadState(params: {
|
||||
const env = params.env ?? process.env;
|
||||
const baseConfig = applyTestPluginDefaults(params.context.config ?? {}, env);
|
||||
const preparedLoadContext = params.preparedRuntime?.loadContext;
|
||||
const usePreparedRuntime =
|
||||
preparedLoadContext !== undefined &&
|
||||
(baseConfig === preparedLoadContext.rawConfig || baseConfig === preparedLoadContext.config) &&
|
||||
env === preparedLoadContext.env &&
|
||||
params.context.workspaceDir === preparedLoadContext.workspaceDir;
|
||||
// The prepared runtime already owns one immutable Gateway plugin generation. Per-turn config
|
||||
// and workspace projections cannot invalidate that executable graph or reopen discovery.
|
||||
const usePreparedRuntime = preparedLoadContext !== undefined && env === preparedLoadContext.env;
|
||||
const context = usePreparedRuntime
|
||||
? preparedLoadContext
|
||||
: resolvePluginRuntimeLoadContext({
|
||||
|
||||
@@ -8,6 +8,7 @@ let listSkillCommandsForAgents: typeof import("./chat-commands.js").listSkillCom
|
||||
let listSkillCommandsForWorkspace: typeof import("./chat-commands.js").listSkillCommandsForWorkspace;
|
||||
let expandExplicitSkillReferences: typeof import("./chat-commands.js").expandExplicitSkillReferences;
|
||||
let resolveSkillCommandInvocation: typeof import("./chat-commands.js").resolveSkillCommandInvocation;
|
||||
let lastPluginMetadataSnapshot: unknown;
|
||||
|
||||
function resolveSkillReferenceInvocations(
|
||||
params: Parameters<typeof expandExplicitSkillReferences>[0],
|
||||
@@ -95,6 +96,7 @@ function buildWorkspaceSkillCommandSpecs(
|
||||
reservedNames?: Set<string>;
|
||||
skillFilter?: string[];
|
||||
agentId?: string;
|
||||
pluginMetadataSnapshot?: unknown;
|
||||
config?: {
|
||||
agents?: {
|
||||
defaults?: { skills?: string[] };
|
||||
@@ -103,6 +105,7 @@ function buildWorkspaceSkillCommandSpecs(
|
||||
};
|
||||
},
|
||||
) {
|
||||
lastPluginMetadataSnapshot = opts?.pluginMetadataSnapshot;
|
||||
const used = new Set<string>();
|
||||
for (const reserved of opts?.reservedNames ?? []) {
|
||||
used.add(reserved.toLowerCase());
|
||||
@@ -176,6 +179,7 @@ afterAll(() => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
lastPluginMetadataSnapshot = undefined;
|
||||
resolveNodeExecEligibilityMock.mockReturnValue({ canExec: false });
|
||||
});
|
||||
|
||||
@@ -645,4 +649,18 @@ describe("listSkillCommandsForWorkspace", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps explicit command discovery on the admitted plugin generation", async () => {
|
||||
const baseDir = tempDirs.make("openclaw-skills-workspace-generation-");
|
||||
const workspaceDir = await createWorkspace(baseDir, "main");
|
||||
const pluginMetadataSnapshot = { generation: "gateway" } as never;
|
||||
|
||||
listSkillCommandsForWorkspace({
|
||||
workspaceDir,
|
||||
cfg: {},
|
||||
pluginMetadataSnapshot,
|
||||
});
|
||||
|
||||
expect(lastPluginMetadataSnapshot).toBe(pluginMetadataSnapshot);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "../../agents/exec-defaults.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { loadWorkspaceSkills } from "../loading/workspace-skill-loader.js";
|
||||
import { getRemoteSkillEligibility } from "../runtime/remote.js";
|
||||
import type { SkillCommandSpec } from "../types.js";
|
||||
@@ -35,6 +36,7 @@ export function listSkillCommandsForWorkspace(params: {
|
||||
sessionKey?: string;
|
||||
execOverrides?: ExecPolicyOverrides;
|
||||
includeAllowlistHidden?: boolean;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): SkillCommandSpec[] {
|
||||
const nodeSkills = resolveNodeExecEligibility({
|
||||
cfg: params.cfg,
|
||||
@@ -48,7 +50,11 @@ export function listSkillCommandsForWorkspace(params: {
|
||||
remote: getRemoteSkillEligibility({ advertiseExecNode: nodeSkills.canExec }),
|
||||
};
|
||||
const entries = params.includeAllowlistHidden
|
||||
? loadWorkspaceSkills(params.workspaceDir, { config: params.cfg, eligibility })
|
||||
? loadWorkspaceSkills(params.workspaceDir, {
|
||||
config: params.cfg,
|
||||
eligibility,
|
||||
pluginMetadataSnapshot: params.pluginMetadataSnapshot,
|
||||
})
|
||||
: undefined;
|
||||
return buildWorkspaceSkillCommandSpecs(params.workspaceDir, {
|
||||
config: params.cfg,
|
||||
@@ -56,6 +62,7 @@ export function listSkillCommandsForWorkspace(params: {
|
||||
skillFilter: params.skillFilter,
|
||||
includeAllowlistHidden: params.includeAllowlistHidden,
|
||||
eligibility,
|
||||
pluginMetadataSnapshot: params.pluginMetadataSnapshot,
|
||||
...(entries ? { entries } : {}),
|
||||
reservedNames: listReservedChatSlashCommandNames(),
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { createDedupeCache } from "../../infra/dedupe.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { loadEnabledClaudeBundleCommands } from "../../plugins/bundle-commands.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { resolveSkillTelemetrySource } from "../loading/source.js";
|
||||
import { filterWorkspaceSkills, loadVisibleSkills } from "../loading/workspace-skill-loader.js";
|
||||
import type { SkillEligibilityContext, SkillCommandSpec, SkillEntry } from "../types.js";
|
||||
@@ -81,6 +82,7 @@ export function buildWorkspaceSkillCommandSpecs(
|
||||
skillFilter?: string[];
|
||||
includeAllowlistHidden?: boolean;
|
||||
eligibility?: SkillEligibilityContext;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
reservedNames?: Set<string>;
|
||||
},
|
||||
): SkillCommandSpec[] {
|
||||
@@ -99,6 +101,7 @@ export function buildWorkspaceSkillCommandSpecs(
|
||||
bundledSkillsDir: opts?.bundledSkillsDir,
|
||||
skillFilter: effectiveSkillFilter,
|
||||
eligibility: opts?.eligibility,
|
||||
pluginMetadataSnapshot: opts?.pluginMetadataSnapshot,
|
||||
});
|
||||
const userInvocable = filterUserInvocableSkillEntries(eligible);
|
||||
const used = new Set<string>();
|
||||
|
||||
@@ -47,6 +47,7 @@ vi.mock("../../plugins/plugin-metadata-snapshot.js", () => ({
|
||||
}));
|
||||
|
||||
let resolvePluginSkillDirs: typeof import("./plugin-skills.js").resolvePluginSkillDirs;
|
||||
let resolvePluginSkillDirsFromMetadata: typeof import("./plugin-skills.js").resolvePluginSkillDirsFromMetadata;
|
||||
|
||||
const tempDirs = createTrackedTempDirs();
|
||||
|
||||
@@ -188,7 +189,29 @@ afterEach(async () => {
|
||||
|
||||
describe("resolvePluginSkillDirs", () => {
|
||||
beforeAll(async () => {
|
||||
({ resolvePluginSkillDirs } = await import("./plugin-skills.js"));
|
||||
({ resolvePluginSkillDirs, resolvePluginSkillDirsFromMetadata } =
|
||||
await import("./plugin-skills.js"));
|
||||
});
|
||||
|
||||
it("uses supplied lifecycle metadata without a cold load", async () => {
|
||||
const { workspaceDir, acpxRoot, helperRoot } = await setupAcpxAndHelperRegistry();
|
||||
registerHealthyAcpBackend();
|
||||
const manifestRegistry = buildRegistry({ acpxRoot, helperRoot });
|
||||
|
||||
const dirs = resolvePluginSkillDirsFromMetadata({
|
||||
workspaceDir,
|
||||
config: {
|
||||
acp: { enabled: true },
|
||||
plugins: { entries: { acpx: { enabled: true }, helper: { enabled: true } } },
|
||||
} as OpenClawConfig,
|
||||
metadataSnapshot: {
|
||||
manifestRegistry,
|
||||
normalizePluginId: (pluginId: string) => pluginId,
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(dirs).toEqual([path.resolve(acpxRoot, "skills"), path.resolve(helperRoot, "skills")]);
|
||||
expect(hoisted.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
} from "../../plugins/config-policy.js";
|
||||
import { resolveMemorySlotDecision } from "../../plugins/config-state.js";
|
||||
import { registerPluginMetadataProcessMemoLifecycleClear } from "../../plugins/plugin-metadata-lifecycle.js";
|
||||
import { resolvePluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import { loadPluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { hasKind } from "../../plugins/slots.js";
|
||||
import { isPathInsideWithRealpath } from "../../security/scan-paths.js";
|
||||
import { CONFIG_DIR } from "../../utils.js";
|
||||
@@ -50,12 +51,27 @@ export function resolvePluginSkillDirs(params: {
|
||||
return [];
|
||||
}
|
||||
const config = params.config ?? {};
|
||||
const metadataSnapshot = resolvePluginMetadataSnapshot({
|
||||
const metadataSnapshot = loadPluginMetadataSnapshot({
|
||||
workspaceDir,
|
||||
config,
|
||||
env: process.env,
|
||||
allowWorkspaceScopedCurrent: true,
|
||||
});
|
||||
return resolvePluginSkillDirsFromMetadata({ ...params, metadataSnapshot });
|
||||
}
|
||||
|
||||
export function resolvePluginSkillDirsFromMetadata(params: {
|
||||
workspaceDir: string | undefined;
|
||||
config?: OpenClawConfig;
|
||||
pluginSkillsDir?: string;
|
||||
metadataSnapshot: PluginMetadataSnapshot;
|
||||
}): string[] {
|
||||
const workspaceDir = (params.workspaceDir ?? "").trim();
|
||||
if (!workspaceDir) {
|
||||
publishPluginSkills([], { pluginSkillsDir: params.pluginSkillsDir });
|
||||
return [];
|
||||
}
|
||||
const config = params.config ?? {};
|
||||
const metadataSnapshot = params.metadataSnapshot;
|
||||
const registry = metadataSnapshot.manifestRegistry;
|
||||
if (registry.plugins.length === 0) {
|
||||
publishPluginSkills([], {
|
||||
|
||||
@@ -7,13 +7,11 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { resetLogger, setLoggerOverride } from "../../logging/logger.js";
|
||||
import { loggingState } from "../../logging/state.js";
|
||||
import { setCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { resolveInstalledPluginIndexPolicyHash } from "../../plugins/installed-plugin-index-policy.js";
|
||||
import type {
|
||||
PluginManifestRecord,
|
||||
PluginManifestRegistry,
|
||||
} from "../../plugins/manifest-registry.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "../../plugins/plugin-metadata-lifecycle.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import { writeSkill, writeWorkspaceSkills } from "../test-support/e2e-test-helpers.js";
|
||||
import {
|
||||
@@ -145,21 +143,6 @@ function createWorkspacePluginMetadataSnapshot(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function setWorkspacePluginMetadataSnapshot(workspaceDir: string, config?: OpenClawConfig): void {
|
||||
const manifestRegistry = createWorkspacePluginRegistry(workspaceDir);
|
||||
setCurrentPluginMetadataSnapshot(
|
||||
createWorkspacePluginMetadataSnapshot({
|
||||
workspaceDir,
|
||||
manifestRegistry,
|
||||
...(config === undefined ? {} : { config }),
|
||||
}),
|
||||
{
|
||||
workspaceDir,
|
||||
...(config === undefined ? {} : { config }),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function expectMissingPath(pathToCheck: string) {
|
||||
let thrown: unknown;
|
||||
try {
|
||||
@@ -192,12 +175,17 @@ function loadTestWorkspaceSkills(
|
||||
workspaceDir: string,
|
||||
opts?: Parameters<typeof loadWorkspaceSkills>[1],
|
||||
) {
|
||||
setWorkspacePluginMetadataSnapshot(workspaceDir, opts?.config);
|
||||
const pluginMetadataSnapshot = createWorkspacePluginMetadataSnapshot({
|
||||
workspaceDir,
|
||||
manifestRegistry: createWorkspacePluginRegistry(workspaceDir),
|
||||
...(opts?.config === undefined ? {} : { config: opts.config }),
|
||||
});
|
||||
return loadWorkspaceSkills(workspaceDir, {
|
||||
managedSkillsDir: path.join(workspaceDir, ".managed"),
|
||||
bundledSkillsDir: "",
|
||||
pluginSkillsDir: path.join(workspaceDir, ".plugin-skills"),
|
||||
...opts,
|
||||
pluginMetadataSnapshot: opts?.pluginMetadataSnapshot ?? pluginMetadataSnapshot,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,7 +197,6 @@ beforeAll(async () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
setLoggerOverride(null);
|
||||
loggingState.rawConsole = null;
|
||||
resetLogger();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { isDefaultStateDir } from "../../config/paths.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { isPathInside } from "../../infra/path-guards.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
|
||||
import {
|
||||
isSessionSkillEnabled,
|
||||
@@ -34,7 +35,7 @@ import {
|
||||
readSkillFrontmatterSafe,
|
||||
type LocalSkillLoadDiagnostic,
|
||||
} from "./local-loader.js";
|
||||
import { resolvePluginSkillDirs } from "./plugin-skills.js";
|
||||
import { resolvePluginSkillDirs, resolvePluginSkillDirsFromMetadata } from "./plugin-skills.js";
|
||||
import type { Skill } from "./skill-contract.js";
|
||||
import { compactSkillPath, resolveSkillsUserHomeDir } from "./skill-paths.js";
|
||||
import {
|
||||
@@ -74,6 +75,7 @@ type WorkspaceSkillLoadOptions = {
|
||||
eligibility?: SkillEligibilityContext;
|
||||
workspaceOnly?: boolean;
|
||||
includeArchived?: boolean;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
};
|
||||
|
||||
export function normalizeWorkspaceSkillRoots(roots: WorkspaceSkillRoots): WorkspaceSkillRoots {
|
||||
@@ -334,6 +336,7 @@ function loadSkillEntries(
|
||||
workspaceSkillsDir?: string;
|
||||
workspaceOnly?: boolean;
|
||||
includeArchived?: boolean;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
},
|
||||
): SkillEntry[] {
|
||||
const limits = resolveSkillDiscoveryLimits(opts?.config);
|
||||
@@ -352,7 +355,14 @@ function loadSkillEntries(
|
||||
const extraDirs = normalizeTrimmedStringList(extraDirsRaw);
|
||||
const pluginSkillDirs = workspaceOnly
|
||||
? []
|
||||
: resolvePluginSkillDirs({ workspaceDir, config: opts?.config, pluginSkillsDir });
|
||||
: opts?.pluginMetadataSnapshot
|
||||
? resolvePluginSkillDirsFromMetadata({
|
||||
workspaceDir,
|
||||
config: opts.config,
|
||||
pluginSkillsDir,
|
||||
metadataSnapshot: opts.pluginMetadataSnapshot,
|
||||
})
|
||||
: resolvePluginSkillDirs({ workspaceDir, config: opts?.config, pluginSkillsDir });
|
||||
const mergedExtraDirs = [...extraDirs, ...pluginSkillDirs];
|
||||
|
||||
const bundledSkills = bundledSkillsDir
|
||||
@@ -481,6 +491,7 @@ export function resolveWorkspaceSkillPromptEntries(
|
||||
skillFilter?: string[];
|
||||
skillOverrides?: Record<string, boolean>;
|
||||
eligibility?: SkillEligibilityContext;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
},
|
||||
): { eligible: SkillEntry[]; skillFilter: string[] | undefined } {
|
||||
const skillFilter = resolveEffectiveWorkspaceSkillFilter(opts);
|
||||
@@ -573,6 +584,7 @@ export function loadVisibleSkills(
|
||||
skillOverrides?: Record<string, boolean>;
|
||||
agentId?: string;
|
||||
eligibility?: SkillEligibilityContext;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
},
|
||||
): SkillEntry[] {
|
||||
const entries = mergeRemoteNodeSkillEntries(loadSkillEntries(workspaceDir, opts), {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Workspace skill prompt helpers render bounded catalogs and reusable snapshots.
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { resolveEffectiveAgentSkillsLimits } from "../discovery/agent-filter.js";
|
||||
import { filterPromptVisibleSkillEntries } from "../discovery/skill-index.js";
|
||||
import type { SkillEligibilityContext, SkillEntry, SkillSnapshot } from "../types.js";
|
||||
@@ -24,6 +25,7 @@ type WorkspaceSkillBuildOptions = {
|
||||
skillOverrides?: Record<string, boolean>;
|
||||
eligibility?: SkillEligibilityContext;
|
||||
preserveEntryOrder?: boolean;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
};
|
||||
|
||||
function resolveWorkspaceSkillPromptState(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { pruneMapToMaxSize } from "../../infra/map-size.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { matchesSkillFilter } from "../discovery/filter.js";
|
||||
import {
|
||||
loadMergedWorkspaceSkills,
|
||||
@@ -33,6 +34,7 @@ type ReusableSkillSnapshotParams = {
|
||||
snapshotVersion?: number;
|
||||
watch?: boolean;
|
||||
hydrateExisting?: boolean;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
};
|
||||
|
||||
type ReusableSkillSnapshotResult = {
|
||||
@@ -108,6 +110,7 @@ export function resolveReusableWorkspaceSkillSnapshot(
|
||||
skillFilter: params.skillFilter,
|
||||
skillOverrides: params.skillOverrides,
|
||||
eligibility: params.eligibility,
|
||||
pluginMetadataSnapshot: params.pluginMetadataSnapshot,
|
||||
snapshotVersion,
|
||||
});
|
||||
return skillRoots ? { ...snapshot, skillRoots } : snapshot;
|
||||
|
||||
Reference in New Issue
Block a user