mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
refactor(agents): unify tool availability and plugin work ownership (#152073)
* refactor(agents): unify tool and plugin work ownership * test(agents): align migrated fixtures with host contracts * test(codex): avoid shadowing side-question options
This commit is contained in:
parent
730b8e75be
commit
45849c0dfd
42 changed files with 864 additions and 466 deletions
|
|
@ -65,6 +65,8 @@ OpenClaw sandboxes, ordinary node-host exec, and Codex `remote-exec` placements
|
|||
|
||||
Gateway-hosted agents check publication availability for ordinary messages and internal continuations, including when a subagent finishes after the requester yields. The check uses the current session workspace and GitHub identity. If publication is unavailable, `github_identity_status` remains available to explain identity setup or reconnection needs, subject to the session's tool policy. Standalone local runs and runs with tools disabled do not expose these managed publication tools.
|
||||
|
||||
The built-in, Codex, and Copilot tool surfaces use this same host-prepared availability. Harness options cannot replace the host's decision; tool profiles and Gateway authorization still apply.
|
||||
|
||||
Publication stages workspace changes with ordinary Git attribute conversion. It preserves unchanged committed file bytes, including existing CRLF line endings, rather than renormalizing unrelated tracked files.
|
||||
|
||||
Local session-owned worktrees can use the same **Publish PR** action in the Control UI. The Gateway derives the managed worktree, repository, branch, base, and head from current session ownership. It never accepts those authority facts from the browser or model. Publication retries use a durable request ID, an exact commit marker, remote branch observation, and pull-request lookup by head branch so a Gateway restart or lost response does not create duplicate commits, pushes, or pull requests.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ model discovery, auth preparation, or Responses parameters. An explicit
|
|||
observation, not a native ownership claim. Bound native sessions use the separate
|
||||
ownership contract below.
|
||||
|
||||
Use `params.hostCapabilities.createToolSurface(options)` to construct OpenClaw
|
||||
tools. The host captures publication availability for the admitted attempt and
|
||||
applies it when building the surface; harnesses do not need to forward that fact,
|
||||
and plugin-supplied options cannot replace it. Tool profiles still filter the
|
||||
catalog, and each executable remains bound to the host's live authority.
|
||||
|
||||
### Native tool-policy enforcement
|
||||
|
||||
Set `conversationToolPolicySupport: "exact"` only when `runAttempt` enforces every
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { CodexAppServerClient } from "./client.js";
|
||||
import { threadStartResult as createThreadStartResult } from "./codex-app-server.test-fixtures.js";
|
||||
import { type CodexPluginConfig, resolveCodexAppServerRuntimeOptions } from "./config.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import { setCodexTestToolFactory } from "./host-capability.test-support.js";
|
||||
import { setManagedCodexPluginRoot } from "./managed-binary.js";
|
||||
import { codexNativeSubagentMonitorRuntime } from "./native-subagent-monitor.js";
|
||||
import { defaultCodexPluginMetadataCache } from "./plugin-metadata-cache.js";
|
||||
|
|
@ -177,7 +177,7 @@ describe("Codex runtime startup resource lifetime", () => {
|
|||
params.sandbox = createSandboxContext({});
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () => [createRuntimeDynamicTool("message")];
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("message")]);
|
||||
const resourcesSpy = vi.spyOn(runAttemptResources, "prepareCodexAttemptResources");
|
||||
const releaseSandbox = vi.spyOn(sandboxExecServer, "releaseCodexSandboxExecServerEnvironment");
|
||||
const allocated: Array<
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
type OpenClawCodingToolsFactory =
|
||||
(typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"];
|
||||
|
||||
/** Mutable dependency seam shared by dynamic-tool construction and its behavioral tests. */
|
||||
export const dynamicToolBuildState: {
|
||||
openClawCodingToolsFactory?: OpenClawCodingToolsFactory;
|
||||
} = {};
|
||||
|
|
@ -1,20 +1,15 @@
|
|||
import "./dynamic-tool-build.test-support.js";
|
||||
import fs from "node:fs/promises";
|
||||
import "./dynamic-tool-build.test-support.js";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { setCodexTestToolFactory } from "./host-capability.test-support.js";
|
||||
|
||||
const {
|
||||
buildDynamicToolsForTest,
|
||||
createCodexRuntimePlanFixture,
|
||||
createParams,
|
||||
hoisted,
|
||||
resetOpenClawCodingToolsFactoryForTests,
|
||||
setOpenClawCodingToolsFactoryForTests,
|
||||
} = await import("./dynamic-tool-build.test-support.js");
|
||||
const { buildDynamicToolsForTest, createCodexRuntimePlanFixture, createParams, hoisted } =
|
||||
await import("./dynamic-tool-build.test-support.js");
|
||||
type OpenClawCodingToolsOptionsForTest = NonNullable<
|
||||
Parameters<Parameters<typeof setOpenClawCodingToolsFactoryForTests>[0]>[0]
|
||||
Parameters<Parameters<typeof setCodexTestToolFactory>[1]>[0]
|
||||
>;
|
||||
|
||||
describe("Codex app-server dynamic tool question prompts", () => {
|
||||
|
|
@ -29,7 +24,6 @@ describe("Codex app-server dynamic tool question prompts", () => {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetOpenClawCodingToolsFactoryForTests();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
|
|
@ -60,7 +54,7 @@ describe("Codex app-server dynamic tool question prompts", () => {
|
|||
const onToolResult = vi.fn();
|
||||
params.onToolResult = hasCallback ? onToolResult : undefined;
|
||||
let capturedQuestionPrompt: OpenClawCodingToolsOptionsForTest["questionPrompt"];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
capturedQuestionPrompt = options?.questionPrompt;
|
||||
return [];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { EmbeddedRunAttemptParamsV2 as EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { vi } from "vitest";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import { buildDynamicTools } from "./dynamic-tool-build.js";
|
||||
import { createCodexTestHostCapabilities } from "./host-capability.test-support.js";
|
||||
import { createCodexTestModel } from "./test-support.js";
|
||||
|
|
@ -44,16 +43,6 @@ vi.mock("openclaw/plugin-sdk/node-selection-runtime", async (importOriginal) =>
|
|||
return { ...actual, loadNodeExecAvailability: hoisted.loadNodeExecAvailability };
|
||||
});
|
||||
|
||||
export function setOpenClawCodingToolsFactoryForTests(
|
||||
factory: NonNullable<typeof dynamicToolBuildState.openClawCodingToolsFactory>,
|
||||
): void {
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = factory;
|
||||
}
|
||||
|
||||
export function resetOpenClawCodingToolsFactoryForTests(): void {
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = undefined;
|
||||
}
|
||||
|
||||
export function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAttemptParams {
|
||||
return {
|
||||
hostCapabilities: createCodexTestHostCapabilities(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import "./dynamic-tool-build.test-support.js";
|
||||
// Codex tests cover dynamic tool build plugin behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import "./dynamic-tool-build.test-support.js";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
|
|
@ -46,6 +46,10 @@ import {
|
|||
resolveCodexDynamicToolsLoadingForRuntime,
|
||||
} from "./dynamic-tool-profile.js";
|
||||
import { createCodexDynamicToolBridge } from "./dynamic-tools.js";
|
||||
import {
|
||||
createCodexTestHostCapabilities,
|
||||
setCodexTestToolFactory,
|
||||
} from "./host-capability.test-support.js";
|
||||
import * as nativeExecutionPolicy from "./native-execution-policy.js";
|
||||
import {
|
||||
CODEX_OPENCLAW_DIRECT_DYNAMIC_TOOL_NAMESPACE,
|
||||
|
|
@ -54,14 +58,8 @@ import {
|
|||
import { resolveCodexDynamicToolDirectNames } from "./run-attempt-tools.js";
|
||||
import { createCodexTestModel } from "./test-support.js";
|
||||
|
||||
const {
|
||||
buildDynamicToolsForTest,
|
||||
createCodexRuntimePlanFixture,
|
||||
createParams,
|
||||
hoisted,
|
||||
resetOpenClawCodingToolsFactoryForTests,
|
||||
setOpenClawCodingToolsFactoryForTests,
|
||||
} = await import("./dynamic-tool-build.test-support.js");
|
||||
const { buildDynamicToolsForTest, createCodexRuntimePlanFixture, createParams, hoisted } =
|
||||
await import("./dynamic-tool-build.test-support.js");
|
||||
|
||||
let tempDir: string;
|
||||
const hostCapabilityClosers: Array<() => void> = [];
|
||||
|
|
@ -111,7 +109,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
let capturedOnYield:
|
||||
| ((message: string, acknowledgment?: string) => Promise<void> | void)
|
||||
| undefined;
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
capturedOnYield = (options as { onYield?: typeof capturedOnYield }).onYield;
|
||||
return [];
|
||||
});
|
||||
|
|
@ -138,12 +136,9 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const bindToolSurface = vi.fn(params.hostCapabilities.bindToolSurface);
|
||||
params.hostCapabilities = Object.freeze({
|
||||
...params.hostCapabilities,
|
||||
bindToolSurface,
|
||||
});
|
||||
params.hostCapabilities = createCodexTestHostCapabilities({ bindToolSurface });
|
||||
const factory = vi.fn(() => [createRuntimeDynamicTool("read")]);
|
||||
setOpenClawCodingToolsFactoryForTests(factory);
|
||||
setCodexTestToolFactory(params, factory);
|
||||
const resolveCronCreatorToolAuthority = vi.fn(async () => ({
|
||||
tools: ["read"],
|
||||
provenance: { version: 1 as const, source: "final-executable-surface" as const },
|
||||
|
|
@ -173,7 +168,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
...createOpenClawCodingTools(options).filter((tool) => tool.name === "message"),
|
||||
createRuntimeDynamicTool("paired_host_plugin"),
|
||||
]);
|
||||
setOpenClawCodingToolsFactoryForTests(factory);
|
||||
setCodexTestToolFactory(params, factory);
|
||||
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
sandbox: {
|
||||
|
|
@ -211,7 +206,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factory = vi.fn(() => [createRuntimeDynamicTool("exec")]);
|
||||
setOpenClawCodingToolsFactoryForTests(factory);
|
||||
setCodexTestToolFactory(params, factory);
|
||||
|
||||
await expect(
|
||||
buildDynamicToolsForTest(params, workspaceDir, {
|
||||
|
|
@ -341,11 +336,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
);
|
||||
try {
|
||||
await bindProductionCodexHostCapabilities(params);
|
||||
setOpenClawCodingToolsFactoryForTests((options) =>
|
||||
createOpenClawCodingTools(options).filter((tool) =>
|
||||
["read", "write", "edit", "apply_patch", "exec", "process"].includes(tool.name),
|
||||
),
|
||||
);
|
||||
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
sandboxSessionKey: params.sandboxSessionKey ?? params.sessionKey,
|
||||
nativeToolSurfaceEnabled: false,
|
||||
|
|
@ -374,7 +365,10 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
await expect(fs.readFile(targetPath, "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(result.success).toBe(false);
|
||||
expect(beforeToolCall).not.toHaveBeenCalled();
|
||||
expect(tools.map((tool) => tool.name).toSorted()).toEqual(
|
||||
const codingTools = tools.filter(({ name }) =>
|
||||
["read", "write", "edit", "apply_patch", "exec", "process"].includes(name),
|
||||
);
|
||||
expect(codingTools.map((tool) => tool.name).toSorted()).toEqual(
|
||||
source === "intersection" ? ["read"] : ["apply_patch", "read"],
|
||||
);
|
||||
const nativeTools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
|
|
@ -428,7 +422,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
for (const close of hostCapabilityClosers.splice(0)) {
|
||||
close();
|
||||
}
|
||||
resetOpenClawCodingToolsFactoryForTests();
|
||||
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
|
|
@ -537,7 +531,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.disableTools = false;
|
||||
params.model = createCodexTestModel("codex", ["text", "image"]);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("view_image"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
|
|
@ -559,7 +553,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.execOverrides = { host: "gateway", mode: "full" };
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [];
|
||||
});
|
||||
|
|
@ -634,7 +628,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
tools: { exec: { safeBins: ["echo"], safeBinProfiles: { echo: { maxPositional: 1 } } } },
|
||||
};
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
setOpenClawCodingToolsFactoryForTests((options) =>
|
||||
setCodexTestToolFactory(params, (options) =>
|
||||
createOpenClawCodingTools(options).filter((tool) => ["exec", "process"].includes(tool.name)),
|
||||
);
|
||||
|
||||
|
|
@ -673,7 +667,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
},
|
||||
} as never;
|
||||
let receivedOptions: Record<string, unknown> | undefined;
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
receivedOptions = options as Record<string, unknown>;
|
||||
return [
|
||||
createRuntimeDynamicTool("web_search"),
|
||||
|
|
@ -720,7 +714,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.toolsAllow = toolsAllow;
|
||||
params.config = { tools: { web: { search } } } as never;
|
||||
let receivedOptions: Record<string, unknown> | undefined;
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
receivedOptions = options as Record<string, unknown>;
|
||||
return [createRuntimeDynamicTool("web_search"), createRuntimeDynamicTool("web_fetch")];
|
||||
});
|
||||
|
|
@ -800,7 +794,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.chatType = "direct";
|
||||
params.messageActionTurnCapability = "turn-capability-1";
|
||||
let receivedOptions: unknown;
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
receivedOptions = options;
|
||||
return [createRuntimeDynamicTool("message")];
|
||||
});
|
||||
|
|
@ -827,7 +821,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.taskSuggestionDeliveryMode = "gateway";
|
||||
let receivedOptions: unknown;
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
receivedOptions = options;
|
||||
return [createRuntimeDynamicTool("message")];
|
||||
});
|
||||
|
|
@ -858,7 +852,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
catalogMode: "direct-only" as const,
|
||||
};
|
||||
const factory = vi.fn(() => [createRuntimeDynamicTool("read"), output]);
|
||||
setOpenClawCodingToolsFactoryForTests(factory);
|
||||
setCodexTestToolFactory(params, factory);
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
sandbox: null,
|
||||
nativeToolSurfaceEnabled: shouldEnableCodexAppServerNativeToolSurface(params),
|
||||
|
|
@ -915,7 +909,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.config = { tools: { exec: { mode: "ask" } } };
|
||||
setOpenClawCodingToolsFactoryForTests((options) =>
|
||||
setCodexTestToolFactory(params, (options) =>
|
||||
createOpenClawCodingTools(options).filter((tool) =>
|
||||
["openclaw", "message", "session_status"].includes(tool.name),
|
||||
),
|
||||
|
|
@ -969,7 +963,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.config = policy === "core policy" ? { tools: { deny: ["openclaw"] } } : {};
|
||||
params.toolsAllow = policy === "turn allowlist" ? ["message"] : ["openclaw", "message"];
|
||||
setOpenClawCodingToolsFactoryForTests((options) =>
|
||||
setCodexTestToolFactory(params, (options) =>
|
||||
createOpenClawCodingTools(options).filter((tool) =>
|
||||
["openclaw", "message"].includes(tool.name),
|
||||
),
|
||||
|
|
@ -991,7 +985,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.toolsAllow = ["openclaw"];
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
setCodexTestToolFactory(params, () => [
|
||||
{ ...createRuntimeDynamicTool("openclaw"), catalogMode: "direct-only" },
|
||||
]);
|
||||
|
||||
|
|
@ -1016,7 +1010,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.toolsAllow = toolsAllow;
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
setCodexTestToolFactory(params, () => [
|
||||
{ ...createRuntimeDynamicTool("openclaw"), catalogMode: "direct-only" },
|
||||
]);
|
||||
|
||||
|
|
@ -1035,7 +1029,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const computerContextEpoch = { value: 0 };
|
||||
let receivedEpoch: { value: number } | undefined;
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
receivedEpoch = (options as { computerContextEpoch?: { value: number } })
|
||||
.computerContextEpoch;
|
||||
return [createRuntimeDynamicTool("message")];
|
||||
|
|
@ -1051,7 +1045,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("message")]);
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("message")]);
|
||||
let webSearchAllowed = true;
|
||||
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
|
|
@ -1070,7 +1064,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.toolsAllow = ["message"];
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("web_search"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
|
|
@ -1098,7 +1092,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.toolsAllow = ["message"];
|
||||
setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("message")]);
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("message")]);
|
||||
let persistentWebSearchAllowed = true;
|
||||
let webSearchAllowed = true;
|
||||
|
||||
|
|
@ -1129,7 +1123,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
},
|
||||
},
|
||||
} as never;
|
||||
setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("message")]);
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("message")]);
|
||||
let persistentWebSearchAllowed = false;
|
||||
let webSearchAllowed = true;
|
||||
|
||||
|
|
@ -1175,7 +1169,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
ownerAccountId: "default",
|
||||
};
|
||||
let receivedOptions: unknown;
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
receivedOptions = options;
|
||||
return [createRuntimeDynamicTool("message")];
|
||||
});
|
||||
|
|
@ -1214,7 +1208,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
},
|
||||
},
|
||||
} as never;
|
||||
setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("message")]);
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("message")]);
|
||||
let persistentWebSearchAllowed = true;
|
||||
|
||||
await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
|
|
@ -1238,7 +1232,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
},
|
||||
},
|
||||
} as never;
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("web_search"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
|
|
@ -1253,7 +1247,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("web_search"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
|
|
@ -1353,10 +1347,11 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
return Reflect.get(target, property, receiver);
|
||||
},
|
||||
});
|
||||
setOpenClawCodingToolsFactoryForTests(() => sourceTools);
|
||||
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => sourceTools);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
||||
|
|
@ -1370,10 +1365,11 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
...createRuntimeDynamicTool("dofbot_move_angles"),
|
||||
parameters: { type: "array", items: { type: "number" } },
|
||||
};
|
||||
setOpenClawCodingToolsFactoryForTests(() => [brokenTool, messageTool]);
|
||||
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [brokenTool, messageTool]);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
||||
|
|
@ -1397,7 +1393,11 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
|
||||
it("limits Codex memory flush runs to managed read and write tools", async () => {
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [
|
||||
createRuntimeDynamicTool("read"),
|
||||
|
|
@ -1409,9 +1409,6 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
createRuntimeDynamicTool("web_search"),
|
||||
];
|
||||
});
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.trigger = "memory";
|
||||
|
|
@ -1444,14 +1441,14 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
});
|
||||
|
||||
it("keeps persistent search disabled during a memory flush when config disables it", async () => {
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("read"),
|
||||
createRuntimeDynamicTool("write"),
|
||||
createRuntimeDynamicTool("web_search"),
|
||||
]);
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.trigger = "memory";
|
||||
|
|
@ -1491,7 +1488,11 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
createOpenClawCodingTools({ workspaceDir: tempDir }).find((tool) => tool.name === "exec"),
|
||||
"assembled exec tool",
|
||||
);
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("read"),
|
||||
createRuntimeDynamicTool("write"),
|
||||
createRuntimeDynamicTool("edit"),
|
||||
|
|
@ -1500,9 +1501,6 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
createRuntimeDynamicTool("process"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
||||
|
|
@ -1532,14 +1530,14 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
});
|
||||
|
||||
it("exposes Docker sandbox shell tools when OpenClaw sandboxing disables native Code Mode", async () => {
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
createRuntimeDynamicTool("process"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const sandbox = { enabled: true, backendId: "docker" } as never;
|
||||
|
|
@ -1604,13 +1602,13 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
pluginConfig: {},
|
||||
},
|
||||
])("does not expose the Gateway shell path under $label", async (testCase) => {
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(path.join(tempDir, "gateway-policy-session.jsonl"), workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
createRuntimeDynamicTool("process"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(path.join(tempDir, "gateway-policy-session.jsonl"), workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
Object.assign(params, testCase.params);
|
||||
|
|
@ -1651,12 +1649,13 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
cacheKey: String(available),
|
||||
isAvailable: () => available,
|
||||
});
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(path.join(tempDir, "eligibility.jsonl"), workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(path.join(tempDir, "eligibility.jsonl"), workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
for (const host of ["auto", "node"] as const) {
|
||||
|
|
@ -1671,9 +1670,9 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
});
|
||||
|
||||
it("shares discovery across attempt catalogs but refreshes the next attempt", async () => {
|
||||
setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("exec")]);
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(path.join(tempDir, "catalog-discovery.jsonl"), workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("exec")]);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
hoisted.loadNodeExecAvailability.mockClear();
|
||||
|
|
@ -1695,9 +1694,9 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
});
|
||||
|
||||
it("propagates cancellation during node discovery without publishing tools", async () => {
|
||||
setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("exec")]);
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(path.join(tempDir, "cancel-discovery.jsonl"), workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("exec")]);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const runAbortController = new AbortController();
|
||||
|
|
@ -1742,10 +1741,11 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
],
|
||||
details: { status: "running" },
|
||||
});
|
||||
setOpenClawCodingToolsFactoryForTests(() => [execTool, createRuntimeDynamicTool("message")]);
|
||||
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [execTool, createRuntimeDynamicTool("message")]);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.execOverrides = {
|
||||
|
|
@ -1808,6 +1808,10 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
|
||||
const runtimePolicySessionFile = path.join(tempDir, "runtime-policy-session.jsonl");
|
||||
const runtimePolicyParams = createParams(runtimePolicySessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(runtimePolicyParams, () => [
|
||||
execTool,
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
runtimePolicyParams.disableTools = false;
|
||||
runtimePolicyParams.runtimePlan = createCodexRuntimePlanFixture();
|
||||
runtimePolicyParams.sessionKey = "agent:main:session-1";
|
||||
|
|
@ -1945,9 +1949,10 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
const execTool = createRuntimeDynamicTool("exec");
|
||||
const processTool = createRuntimeDynamicTool("process");
|
||||
const messageTool = createRuntimeDynamicTool("message");
|
||||
setOpenClawCodingToolsFactoryForTests(() => [execTool, processTool, messageTool]);
|
||||
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(path.join(tempDir, "restricted-session.jsonl"), workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [execTool, processTool, messageTool]);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.toolsAllow = ["exec", "process", "message"];
|
||||
|
|
@ -2017,7 +2022,9 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
])(
|
||||
"preserves shared runtime selectors in Codex dynamic tools: $allow",
|
||||
async ({ allow, expected }) => {
|
||||
setOpenClawCodingToolsFactoryForTests(() =>
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(path.join(tempDir, "selector-session.jsonl"), workspaceDir);
|
||||
setCodexTestToolFactory(params, () =>
|
||||
[
|
||||
"automations",
|
||||
"read",
|
||||
|
|
@ -2031,8 +2038,6 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
"message",
|
||||
].map(createRuntimeDynamicTool),
|
||||
);
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(path.join(tempDir, "selector-session.jsonl"), workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.execOverrides = { host: "gateway" };
|
||||
|
|
@ -2055,14 +2060,14 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
])(
|
||||
"keeps Docker shell projections pinned for runtime selectors $allow restricted by $restrictWith",
|
||||
async ({ allow, restrictWith, expected }) => {
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
createRuntimeDynamicTool("process"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.toolsAllow = allow;
|
||||
|
|
@ -2112,14 +2117,14 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
);
|
||||
|
||||
it("exposes node shell but not sandbox shell tools when sandbox routing is disabled", async () => {
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
createRuntimeDynamicTool("process"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
||||
|
|
@ -2137,13 +2142,13 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
});
|
||||
|
||||
it("does not expose sandbox_exec without a matching process follow-up tool", async () => {
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
||||
|
|
@ -2156,14 +2161,14 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
});
|
||||
|
||||
it("honors Codex dynamic tool excludes for sandbox shell exposure", async () => {
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
createRuntimeDynamicTool("process"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
||||
|
|
@ -2197,7 +2202,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.messageActionTurnCapability = "turn-capability-1";
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [];
|
||||
});
|
||||
|
|
@ -2222,7 +2227,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.preparedModelRuntime = { metadataSnapshot: { plugins: [] } } as never;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return options?.senderIsOwner && options.preparedModelRuntime
|
||||
? [createRuntimeDynamicTool("intent")]
|
||||
|
|
@ -2249,7 +2254,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.currentMessagingTarget = "user:U123";
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [];
|
||||
});
|
||||
|
|
@ -2272,7 +2277,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.approvalReviewerDeviceId = "device-ios-reviewer";
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [];
|
||||
});
|
||||
|
|
@ -2295,7 +2300,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.allocateToolOutcomeOrdinal = allocateToolOutcomeOrdinal;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [];
|
||||
});
|
||||
|
|
@ -2328,7 +2333,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
}
|
||||
});
|
||||
params.isTurnTainted = () => turnTainted;
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
const filesystemTools = createOpenClawCodingTools(options).filter((tool) =>
|
||||
["write", "edit"].includes(tool.name),
|
||||
);
|
||||
|
|
@ -2427,7 +2432,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
agentId: "main",
|
||||
sessionId: params.sessionId,
|
||||
});
|
||||
setOpenClawCodingToolsFactoryForTests(() => [wrappedTool]);
|
||||
setCodexTestToolFactory(params, () => [wrappedTool]);
|
||||
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, { sandbox: null as never });
|
||||
|
||||
|
|
@ -2454,7 +2459,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
...createRuntimeDynamicTool("invalid_registered_tool"),
|
||||
parameters: { type: "array", items: { type: "string" } },
|
||||
};
|
||||
setOpenClawCodingToolsFactoryForTests((options) => [
|
||||
setCodexTestToolFactory(params, (options) => [
|
||||
messageTool,
|
||||
...(options?.enableHeartbeatTool === true ? [heartbeatTool, invalidTool] : []),
|
||||
]);
|
||||
|
|
@ -2509,7 +2514,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.config = runtimeConfig;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [];
|
||||
});
|
||||
|
|
@ -2543,7 +2548,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.sessionRoot = workspaceDir;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [];
|
||||
});
|
||||
|
|
@ -2571,7 +2576,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.model = { ...params.model, provider: "openai", id: "gpt-5.6-sol" };
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [];
|
||||
});
|
||||
|
|
@ -2627,7 +2632,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.toolAuthProfileStore = toolAuthProfileStore;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [];
|
||||
});
|
||||
|
|
@ -2663,7 +2668,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
},
|
||||
};
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [];
|
||||
});
|
||||
|
|
@ -2684,7 +2689,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
const factoryOptions: unknown[] = [];
|
||||
setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
factoryOptions.push(options);
|
||||
return [createRuntimeDynamicTool("sessions_spawn")];
|
||||
});
|
||||
|
|
@ -2970,7 +2975,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
const replyOperation = { acceptedSteeredInboundAudio: false };
|
||||
params.replyOperation = replyOperation as EmbeddedRunAttemptParams["replyOperation"];
|
||||
params.sourceReplyDeliveryMode = "message_tool_only";
|
||||
setOpenClawCodingToolsFactoryForTests((options) =>
|
||||
setCodexTestToolFactory(params, (options) =>
|
||||
createOpenClawCodingTools(options).filter((tool) => tool.name === "message"),
|
||||
);
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
|
|
@ -3043,7 +3048,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
params.requireExplicitMessageTarget = required;
|
||||
const factory = vi.fn((_options: Parameters<typeof createOpenClawCodingTools>[0]) => []);
|
||||
const onMessageToolTargetResolved = vi.fn();
|
||||
setOpenClawCodingToolsFactoryForTests(factory);
|
||||
setCodexTestToolFactory(params, factory);
|
||||
await buildDynamicToolsForTest(params, workspaceDir, { onMessageToolTargetResolved });
|
||||
expect(factory.mock.calls[0]?.[0]?.requireExplicitMessageTarget).toBe(expected);
|
||||
expect(onMessageToolTargetResolved).toHaveBeenCalledExactlyOnceWith(expected);
|
||||
|
|
@ -3055,7 +3060,7 @@ describe("Codex app-server dynamic tool build", () => {
|
|||
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
setOpenClawCodingToolsFactoryForTests((options) =>
|
||||
setCodexTestToolFactory(params, (options) =>
|
||||
createOpenClawCodingTools(options).filter((tool) => tool.name === "message"),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import {
|
|||
readCodexPluginConfig,
|
||||
type CodexPluginConfig,
|
||||
} from "./config.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import {
|
||||
filterCodexDynamicTools,
|
||||
filterCodexDynamicToolsForDisabledNativeSurface,
|
||||
|
|
@ -243,7 +242,6 @@ export async function buildDynamicTools(
|
|||
const toolBuildStages = createCodexDynamicToolBuildStageTracker();
|
||||
const modelHasVision = params.model.input?.includes("image") ?? false;
|
||||
const agentDir = params.agentDir ?? resolveAgentDir(params.config ?? {}, input.sessionAgentId);
|
||||
const injectedOpenClawCodingToolsFactory = dynamicToolBuildState.openClawCodingToolsFactory;
|
||||
const nativeExecutionPolicy = resolveCodexNativeExecutionPolicyForRun(params, {
|
||||
agentId: input.policyAgentId,
|
||||
runtimeSessionKey: input.sandboxSessionKey,
|
||||
|
|
@ -313,7 +311,6 @@ export async function buildDynamicTools(
|
|||
...(params.skillLibraryAuthoring
|
||||
? { skillWorkshop: { libraryAuthoring: params.skillLibraryAuthoring } }
|
||||
: {}),
|
||||
githubPublicationAvailable: params.githubPublicationAvailable,
|
||||
authProfileStore: params.toolAuthProfileStore ?? params.authProfileStore,
|
||||
abortSignal: input.runAbortController.signal,
|
||||
emitBeforeToolCallDiagnostics: false,
|
||||
|
|
@ -369,12 +366,6 @@ export async function buildDynamicTools(
|
|||
input.onMessageToolTargetResolved?.(options.requireExplicitMessageTarget === true);
|
||||
const buildOpenClawCodingTools = () => {
|
||||
const bindingOptions = { cwd: input.effectiveCwd ?? input.effectiveWorkspace };
|
||||
if (injectedOpenClawCodingToolsFactory) {
|
||||
return params.hostCapabilities.bindToolSurface(
|
||||
injectedOpenClawCodingToolsFactory(options),
|
||||
bindingOptions,
|
||||
);
|
||||
}
|
||||
const createToolSurface = params.hostCapabilities.createToolSurface;
|
||||
if (!createToolSurface) {
|
||||
throw new Error("Codex tool construction requires a current host capability");
|
||||
|
|
|
|||
|
|
@ -1,14 +1,48 @@
|
|||
import { createOpenClawCodingTools } from "openclaw/plugin-sdk/agent-harness";
|
||||
import type { EmbeddedRunAttemptParamsV2 as EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
|
||||
type ToolsFactory = typeof createOpenClawCodingTools;
|
||||
type HostCapabilities = EmbeddedRunAttemptParams["hostCapabilities"];
|
||||
const toolFactories = new WeakMap<object, ToolsFactory | undefined>();
|
||||
|
||||
export function setCodexTestToolFactory(
|
||||
params: Pick<EmbeddedRunAttemptParams, "hostCapabilities">,
|
||||
factory: ToolsFactory,
|
||||
): void {
|
||||
if (!toolFactories.has(params.hostCapabilities)) {
|
||||
throw new Error(
|
||||
"Synthetic tools require a lightweight test host; keep real host identity intact.",
|
||||
);
|
||||
}
|
||||
toolFactories.set(params.hostCapabilities, factory);
|
||||
toolFactories.set(params, factory);
|
||||
}
|
||||
|
||||
export function getCodexTestToolFactory(
|
||||
params: Pick<EmbeddedRunAttemptParams, "hostCapabilities">,
|
||||
): ToolsFactory | undefined {
|
||||
return toolFactories.get(params) ?? toolFactories.get(params.hostCapabilities);
|
||||
}
|
||||
|
||||
/** Minimal host authority for tests that do not exercise host policy or approvals. */
|
||||
export function createCodexTestHostCapabilities(): EmbeddedRunAttemptParams["hostCapabilities"] {
|
||||
return Object.freeze({
|
||||
export function createCodexTestHostCapabilities(
|
||||
overrides: Partial<Omit<HostCapabilities, "createToolSurface">> = {},
|
||||
): HostCapabilities {
|
||||
const host: HostCapabilities = Object.freeze({
|
||||
kind: "agent-harness-host-capability",
|
||||
version: 1,
|
||||
assertActive: () => {},
|
||||
bindToolSurface: (tools) => tools,
|
||||
createToolSurface: (options, bindingOptions) =>
|
||||
host.bindToolSurface(
|
||||
(toolFactories.get(host) ?? createOpenClawCodingTools)(options),
|
||||
bindingOptions,
|
||||
),
|
||||
runBeforeToolCall: async (request) => ({ blocked: false, params: request.params }),
|
||||
requestApproval: async () => undefined,
|
||||
waitForApproval: async () => undefined,
|
||||
...overrides,
|
||||
});
|
||||
toolFactories.set(host, undefined);
|
||||
return host;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createOpenClawCodingTools } from "openclaw/plugin-sdk/agent-harness";
|
||||
import {
|
||||
abortAndDrainAgentHarnessRun,
|
||||
nativeHookRelayTesting,
|
||||
|
|
@ -12,6 +11,7 @@ import {
|
|||
type EmbeddedRunAttemptParamsV2 as EmbeddedRunAttemptParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { clearRuntimeAuthProfileStoreSnapshots } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { setHostToolFactoryForTest } from "openclaw/plugin-sdk/agent-runtime-test-contracts";
|
||||
import { resetDiagnosticEventsForTest } from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
import type { ExecApprovalsFile } from "openclaw/plugin-sdk/exec-approvals-runtime";
|
||||
import { clearInternalHooks, resetGlobalHookRunner } from "openclaw/plugin-sdk/hook-runtime";
|
||||
|
|
@ -39,8 +39,11 @@ import {
|
|||
turnStartResult,
|
||||
} from "./codex-app-server.test-fixtures.js";
|
||||
import * as codexRequirements from "./config-requirements.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import { createCodexDynamicToolBridge } from "./dynamic-tools.js";
|
||||
import {
|
||||
createCodexTestHostCapabilities,
|
||||
getCodexTestToolFactory,
|
||||
} from "./host-capability.test-support.js";
|
||||
import { setManagedCodexPluginRoot } from "./managed-binary.js";
|
||||
import { nativeHookRelayUnregisterQueue } from "./native-hook-relay-state.js";
|
||||
import { defaultCodexPluginMetadataCache } from "./plugin-metadata-cache.js";
|
||||
|
|
@ -75,12 +78,7 @@ const execApprovalsRuntimeMocks = vi.hoisted(() => ({
|
|||
function createHarnessHostCapabilities(
|
||||
params: EmbeddedRunAttemptParams,
|
||||
): EmbeddedRunAttemptParams["hostCapabilities"] {
|
||||
return Object.freeze({
|
||||
kind: "agent-harness-host-capability",
|
||||
version: 1,
|
||||
assertActive: () => {},
|
||||
bindToolSurface: (tools) => tools,
|
||||
createToolSurface: (options) => createOpenClawCodingTools(options),
|
||||
return createCodexTestHostCapabilities({
|
||||
runBeforeToolCall: async ({ nativeOperation: _nativeOperation, approvalMode, ...request }) =>
|
||||
await runBeforeToolCallHook({
|
||||
...request,
|
||||
|
|
@ -102,8 +100,6 @@ function createHarnessHostCapabilities(
|
|||
turnSourceThreadId: params.currentThreadTs,
|
||||
}),
|
||||
}),
|
||||
requestApproval: async () => undefined,
|
||||
waitForApproval: async () => undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -350,6 +346,10 @@ export function createNativeRunParams(
|
|||
export async function bindProductionHarnessHostCapabilitiesForTest(
|
||||
params: EmbeddedRunAttemptParams,
|
||||
): Promise<() => void> {
|
||||
const factory = getCodexTestToolFactory(params);
|
||||
if (factory) {
|
||||
await setHostToolFactoryForTest(params, factory);
|
||||
}
|
||||
const { hostCapabilities: _hostCapabilities, ...attempt } = params;
|
||||
const host = await createAgentHarnessHostCapabilitiesForTest({ attempt, pluginId: "codex" });
|
||||
params.hostCapabilities = host.capabilities;
|
||||
|
|
@ -729,7 +729,6 @@ export function setupRunAttemptTestHooks(): void {
|
|||
resetCodexAppServerClientFactoryForTest();
|
||||
setManagedCodexPluginRoot(undefined);
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = undefined;
|
||||
codexWorkspaceDirCache.clear();
|
||||
await nativeHookRelayUnregisterQueue.clear();
|
||||
await nativeHookRelayTesting.clearNativeHookRelaysForTests();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
|||
import { upsertSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { formatSqliteSessionFileMarker } from "openclaw/plugin-sdk/sqlite-runtime-testing";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import { setCodexTestToolFactory } from "./host-capability.test-support.js";
|
||||
import {
|
||||
createCodexRuntimePlanFixture,
|
||||
createParams,
|
||||
|
|
@ -58,9 +58,7 @@ describe("runCodexAppServerAttempt agent-end context", () => {
|
|||
params.messageChannel = "discord";
|
||||
params.memberRoleIds = ["maintainer-role"];
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () => [
|
||||
createRuntimeDynamicTool("skill_workshop"),
|
||||
];
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("skill_workshop")]);
|
||||
|
||||
// Protocol events drive these cases; host load must not spend the execution budget.
|
||||
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"] });
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { SessionManager } from "openclaw/plugin-sdk/agent-sessions";
|
|||
import { appendSessionTranscriptMessageByIdentity } from "openclaw/plugin-sdk/session-transcript-runtime";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { projectContextEngineAssemblyForCodex } from "./context-engine-projection.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import { setCodexTestToolFactory } from "./host-capability.test-support.js";
|
||||
import {
|
||||
assistantMessage,
|
||||
bindProductionHarnessHostCapabilitiesForTest,
|
||||
|
|
@ -180,9 +180,9 @@ it("does not replay covered history on the same thread after local message-tool
|
|||
content: [{ type: "text" as const, text: "Sent." }],
|
||||
details: { messageId: "telegram-123" },
|
||||
}));
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () => [messageTool];
|
||||
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [messageTool]);
|
||||
await attachSqliteSessionTarget(
|
||||
params,
|
||||
path.join(tempDir, "local-source-reply-sessions.json"),
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ import { initializeGlobalHookRunner } from "openclaw/plugin-sdk/hook-runtime";
|
|||
import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { readAttemptTerminal } from "./attempt-terminal.test-helper.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import {
|
||||
emitDynamicToolStartedDiagnostic,
|
||||
emitDynamicToolTerminalDiagnostic,
|
||||
} from "./dynamic-tool-diagnostics.js";
|
||||
import { hasPendingDynamicToolTerminalDiagnostic } from "./dynamic-tool-execution.js";
|
||||
import { setCodexTestToolFactory } from "./host-capability.test-support.js";
|
||||
import type { CodexDynamicToolCallParams } from "./protocol.js";
|
||||
import {
|
||||
bindProductionHarnessHostCapabilitiesForTest,
|
||||
|
|
@ -63,14 +63,13 @@ setupRunAttemptTestHooks();
|
|||
describe("runCodexAppServerAttempt dynamic tools", () => {
|
||||
it("acknowledges a terminal sandbox process poll only after Codex accepts its exact result", async () => {
|
||||
const process = createProcessPollDeliveryContract("codex-result-delivery");
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () => [
|
||||
{ ...process.tool, name: "sandbox_process" },
|
||||
];
|
||||
|
||||
const harness = createStartedThreadHarness();
|
||||
const params = createParams(
|
||||
path.join(tempDir, "session.jsonl"),
|
||||
path.join(tempDir, "workspace"),
|
||||
);
|
||||
setCodexTestToolFactory(params, () => [{ ...process.tool, name: "sandbox_process" }]);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
const closeHostCapabilities = await bindProductionHarnessHostCapabilitiesForTest(params);
|
||||
|
|
@ -153,12 +152,13 @@ describe("runCodexAppServerAttempt dynamic tools", () => {
|
|||
details: { status: "no_answer" },
|
||||
};
|
||||
});
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () => [tool];
|
||||
|
||||
const harness = createStartedThreadHarness();
|
||||
const params = createParams(
|
||||
path.join(tempDir, "session.jsonl"),
|
||||
path.join(tempDir, "workspace"),
|
||||
);
|
||||
setCodexTestToolFactory(params, () => [tool]);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.timeoutMs = waitMs + 120_000;
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
|
|
@ -229,7 +229,7 @@ describe("runCodexAppServerAttempt dynamic tools", () => {
|
|||
};
|
||||
});
|
||||
tool.execute = execute;
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () => [tool];
|
||||
|
||||
const harness = createStartedThreadHarness();
|
||||
let closeHostCapabilities: (() => void) | undefined;
|
||||
const unsubscribeDiagnostics = onInternalDiagnosticEvent((event) => {
|
||||
|
|
@ -242,6 +242,7 @@ describe("runCodexAppServerAttempt dynamic tools", () => {
|
|||
path.join(tempDir, "session.jsonl"),
|
||||
path.join(tempDir, "workspace"),
|
||||
);
|
||||
setCodexTestToolFactory(params, () => [tool]);
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
closeHostCapabilities = await bindProductionHarnessHostCapabilitiesForTest(params);
|
||||
const runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
|
@ -882,7 +883,7 @@ describe("runCodexAppServerAttempt dynamic tools", () => {
|
|||
params.sandboxSessionKey = "agent:main:policy";
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () => [createRuntimeDynamicTool("echo")];
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("echo")]);
|
||||
const harness = createStartedThreadHarness();
|
||||
const closeHostCapabilities = await bindProductionHarnessHostCapabilitiesForTest(params);
|
||||
const run = runCodexAppServerAttempt(params);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtim
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CodexAppServerClient } from "./client.js";
|
||||
import { resolveCodexSupervisionAppServerRuntimeOptions } from "./config.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import { setCodexTestToolFactory } from "./host-capability.test-support.js";
|
||||
import { buildCodexAppServerConnectionFingerprint } from "./plugin-app-cache-key.js";
|
||||
import { isJsonObject } from "./protocol.js";
|
||||
import {
|
||||
|
|
@ -192,11 +192,13 @@ describe("Codex native configuration", () => {
|
|||
});
|
||||
const start = vi.spyOn(CodexAppServerClient, "start").mockResolvedValue(harness.client);
|
||||
const clientFactory = vi.fn(sharedClientModule.getLeasedSharedCodexAppServerClient);
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () =>
|
||||
nativeSearchEnabled ? [createRuntimeDynamicTool("web_search")] : [];
|
||||
|
||||
// This test owns review-policy projection, not requester-scoped MCP discovery.
|
||||
agentHarnessRuntimeMocks.forceModelToolsUnsupported = !nativeSearchEnabled;
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () =>
|
||||
nativeSearchEnabled ? [createRuntimeDynamicTool("web_search")] : [],
|
||||
);
|
||||
params.registerPluginRuntimeRefreshConsumer = vi.fn();
|
||||
params.agentDir = agentDir;
|
||||
params.provider = "anthropic";
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||
import { readAttemptTerminal } from "./attempt-terminal.test-helper.js";
|
||||
import { resolveCodexAppServerHomeDir } from "./auth-start-options.js";
|
||||
import { CodexAppServerClient } from "./client.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import { CodexAppServerEventProjector } from "./event-projector.js";
|
||||
import { buildEmptyToolTelemetry } from "./event-projector.test-harness.js";
|
||||
import { setCodexTestToolFactory } from "./host-capability.test-support.js";
|
||||
import { isJsonObject } from "./protocol.js";
|
||||
import {
|
||||
bindProductionHarnessHostCapabilitiesForTest,
|
||||
|
|
@ -118,11 +118,12 @@ describe("managed Codex plugin refresh", () => {
|
|||
details: {},
|
||||
};
|
||||
});
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () => [slow, reload];
|
||||
|
||||
const params = createParams(
|
||||
path.join(tempDir, "session.jsonl"),
|
||||
path.join(tempDir, "workspace"),
|
||||
);
|
||||
setCodexTestToolFactory(params, () => [slow, reload]);
|
||||
const originalTask = "Reload the plugin and verify the changed behavior.";
|
||||
const receipt = "already-committed-effect-42";
|
||||
const prior = new CodexAppServerEventProjector(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { loadUserTurnTranscriptRecorderFactoryForTest } from "openclaw/plugin-sd
|
|||
import { upsertSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { projectContextEngineAssemblyForCodex } from "./context-engine-projection.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import { setCodexTestToolFactory } from "./host-capability.test-support.js";
|
||||
import type { CodexServerNotification } from "./protocol.js";
|
||||
import {
|
||||
bindProductionHarnessHostCapabilitiesForTest,
|
||||
|
|
@ -131,7 +131,7 @@ describe("runCodexAppServerAttempt question refresh", () => {
|
|||
pendingRefresh = true;
|
||||
return { content: [{ type: "text" as const, text: "generation changed" }], details: {} };
|
||||
});
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () => [reload];
|
||||
setCodexTestToolFactory(params, () => [reload]);
|
||||
params.pluginRuntimeRefreshPending = () => pendingRefresh;
|
||||
if (!params.sessionKey) {
|
||||
throw new Error("Expected the fixture's managed session key");
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ import {
|
|||
resolveCodexAppServerRuntimeOptions,
|
||||
resolveCodexSupervisionAppServerRuntimeOptions,
|
||||
} from "./config.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import {
|
||||
buildDynamicTools,
|
||||
shouldEnableCodexAppServerNativeToolSurface,
|
||||
|
|
@ -63,6 +62,7 @@ import { filterCodexDynamicTools } from "./dynamic-tool-profile.js";
|
|||
import { createCodexDynamicToolBridge } from "./dynamic-tools.js";
|
||||
import * as elicitationBridge from "./elicitation-bridge.js";
|
||||
import { CodexAppServerEventProjector } from "./event-projector.js";
|
||||
import { setCodexTestToolFactory } from "./host-capability.test-support.js";
|
||||
import { buildCodexRuntimeModelParams } from "./model-runtime.js";
|
||||
import {
|
||||
buildCodexAppServerConnectionFingerprint,
|
||||
|
|
@ -175,11 +175,6 @@ const testing = {
|
|||
buildDynamicTools,
|
||||
filterCodexDynamicTools,
|
||||
resolveCodexDynamicToolDirectNames,
|
||||
setOpenClawCodingToolsFactoryForTests(
|
||||
factory: NonNullable<typeof dynamicToolBuildState.openClawCodingToolsFactory>,
|
||||
): void {
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = factory;
|
||||
},
|
||||
shouldEnableCodexAppServerNativeToolSurface,
|
||||
withCodexStartupTimeout,
|
||||
};
|
||||
|
|
@ -1026,7 +1021,9 @@ describe("runCodexAppServerAttempt", () => {
|
|||
const cleanup = vi.fn(async (_reason: string) => undefined);
|
||||
const cleanupOwners: boolean[] = [];
|
||||
const preparationError = new Error(`failed during ${outcome}`);
|
||||
testing.setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
const { sessionFile, workspaceDir } = createRunPaths();
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
cleanupOwners.push(options?.registerRunCleanup !== undefined);
|
||||
options?.registerRunCleanup?.(cleanup);
|
||||
if (
|
||||
|
|
@ -1037,8 +1034,6 @@ describe("runCodexAppServerAttempt", () => {
|
|||
}
|
||||
return [createRuntimeDynamicTool("message")];
|
||||
});
|
||||
const { sessionFile, workspaceDir } = createRunPaths();
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
|
|
@ -1074,13 +1069,13 @@ describe("runCodexAppServerAttempt", () => {
|
|||
const cleanup = vi.fn(async () => {
|
||||
throw cleanupError;
|
||||
});
|
||||
testing.setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
const { sessionFile, workspaceDir } = createRunPaths();
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
options?.registerRunCleanup?.(cleanup);
|
||||
throw preparationError;
|
||||
});
|
||||
const warning = vi.spyOn(embeddedAgentLog, "warn");
|
||||
const { sessionFile, workspaceDir } = createRunPaths();
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.oneShotCliRun = true;
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
|
@ -1156,13 +1151,13 @@ describe("runCodexAppServerAttempt", () => {
|
|||
expect(authProfileStore.profiles[authProfileId]).toHaveProperty("keyRef");
|
||||
});
|
||||
it("starts active OpenClaw sandbox threads with Codex native execution disabled", async () => {
|
||||
testing.setOpenClawCodingToolsFactoryForTests(() => [
|
||||
const { sessionFile, workspaceDir } = createRunPaths();
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
createRuntimeDynamicTool("process"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
const { sessionFile, workspaceDir } = createRunPaths();
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
|
@ -1259,14 +1254,14 @@ describe("runCodexAppServerAttempt", () => {
|
|||
request,
|
||||
};
|
||||
try {
|
||||
testing.setOpenClawCodingToolsFactoryForTests(() => [
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
createRuntimeDynamicTool("process"),
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
|
@ -2391,12 +2386,6 @@ describe("runCodexAppServerAttempt", () => {
|
|||
});
|
||||
|
||||
it("keeps the heartbeat schema deferred and stable across normal and heartbeat turns", async () => {
|
||||
testing.setOpenClawCodingToolsFactoryForTests((options) => [
|
||||
createRuntimeDynamicTool("message"),
|
||||
...(options?.enableHeartbeatTool === true
|
||||
? [createRuntimeDynamicTool("heartbeat_respond")]
|
||||
: []),
|
||||
]);
|
||||
const { sessionFile, workspaceDir } = createRunPaths();
|
||||
const createHeartbeatRunParams = (trigger?: EmbeddedRunAttemptParams["trigger"]) => {
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
|
|
@ -2561,13 +2550,6 @@ describe("runCodexAppServerAttempt", () => {
|
|||
});
|
||||
|
||||
it("keeps the persistent dynamic schema stable across heartbeat-only turns", async () => {
|
||||
testing.setOpenClawCodingToolsFactoryForTests((options) => [
|
||||
createRuntimeDynamicTool("message"),
|
||||
createRuntimeDynamicTool("web_search"),
|
||||
...(options?.enableHeartbeatTool === true
|
||||
? [createRuntimeDynamicTool("heartbeat_respond")]
|
||||
: []),
|
||||
]);
|
||||
const { sessionFile, workspaceDir } = createRunPaths();
|
||||
const createHeartbeatRunParams = (trigger?: EmbeddedRunAttemptParams["trigger"]) => {
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
|
|
@ -2613,11 +2595,11 @@ describe("runCodexAppServerAttempt", () => {
|
|||
expect(specNames(nextNormalBridge.specs)).toEqual(specNames(normalBridge.specs));
|
||||
});
|
||||
it("disables Codex native tool surfaces when runtime toolsAllow is empty", async () => {
|
||||
testing.setOpenClawCodingToolsFactoryForTests(() => [
|
||||
const params = createRunParams();
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("message"),
|
||||
createRuntimeDynamicTool("web_search"),
|
||||
]);
|
||||
const params = createRunParams();
|
||||
params.disableTools = false;
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
|
@ -2771,7 +2753,9 @@ describe("runCodexAppServerAttempt", () => {
|
|||
details: {},
|
||||
};
|
||||
});
|
||||
testing.setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
|
||||
const params = createRunParams();
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
const tools = createOpenClawCodingTools(options).filter((tool) =>
|
||||
["read", "write", "edit", "apply_patch", "exec", "process", "progress_card"].includes(
|
||||
tool.name,
|
||||
|
|
@ -2783,7 +2767,6 @@ describe("runCodexAppServerAttempt", () => {
|
|||
}
|
||||
return tools;
|
||||
});
|
||||
const params = createRunParams();
|
||||
params.disableTools = false;
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
|
@ -3127,8 +3110,8 @@ describe("runCodexAppServerAttempt", () => {
|
|||
});
|
||||
|
||||
it("fails closed for Codex app defaults when restricted native tools have no plugin config", async () => {
|
||||
testing.setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("message")]);
|
||||
const params = createRunParams();
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("message")]);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.toolsAllow = [];
|
||||
|
|
@ -4523,11 +4506,12 @@ describe("runCodexAppServerAttempt", () => {
|
|||
await fs.writeFile(path.join(workspaceDir, "USER.md"), userProfile);
|
||||
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary);
|
||||
registerMemoryPromptForTest();
|
||||
testing.setOpenClawCodingToolsFactoryForTests(() => [
|
||||
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("memory_search"),
|
||||
createRuntimeDynamicTool("memory_get"),
|
||||
]);
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
|
@ -4620,11 +4604,12 @@ describe("runCodexAppServerAttempt", () => {
|
|||
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
|
||||
await fs.writeFile(path.join(workspaceDir, "memory/2026-06-09.md"), datedMemory);
|
||||
registerMemoryPromptForTest();
|
||||
testing.setOpenClawCodingToolsFactoryForTests(() => [
|
||||
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("memory_search"),
|
||||
createRuntimeDynamicTool("memory_get"),
|
||||
]);
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
|
@ -4648,11 +4633,12 @@ describe("runCodexAppServerAttempt", () => {
|
|||
const memorySummary = "User avoids Chase cards while over 5/24.";
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary);
|
||||
testing.setOpenClawCodingToolsFactoryForTests(() => [
|
||||
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("memory_search"),
|
||||
createRuntimeDynamicTool("memory_get"),
|
||||
]);
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
|
@ -4890,8 +4876,9 @@ describe("runCodexAppServerAttempt", () => {
|
|||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary);
|
||||
registerMemoryPromptForTest();
|
||||
testing.setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("memory_get")]);
|
||||
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("memory_get")]);
|
||||
params.disableTools = false;
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
|
@ -4984,7 +4971,7 @@ describe("runCodexAppServerAttempt", () => {
|
|||
},
|
||||
},
|
||||
} as EmbeddedRunAttemptParams["config"];
|
||||
testing.setOpenClawCodingToolsFactoryForTests(() => [
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("memory_search"),
|
||||
createRuntimeDynamicTool("memory_get"),
|
||||
]);
|
||||
|
|
@ -5032,11 +5019,12 @@ describe("runCodexAppServerAttempt", () => {
|
|||
},
|
||||
];
|
||||
});
|
||||
testing.setOpenClawCodingToolsFactoryForTests(() => [
|
||||
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("memory_search"),
|
||||
createRuntimeDynamicTool("memory_get"),
|
||||
]);
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
|
@ -5071,11 +5059,12 @@ describe("runCodexAppServerAttempt", () => {
|
|||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary);
|
||||
registerMemoryPromptForTest();
|
||||
testing.setOpenClawCodingToolsFactoryForTests(() => [
|
||||
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, () => [
|
||||
createRuntimeDynamicTool("memory_search"),
|
||||
createRuntimeDynamicTool("memory_get"),
|
||||
]);
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
setAgentWorkspaceForTest(params, path.join(tempDir, "memory-workspace"));
|
||||
|
|
@ -5295,16 +5284,17 @@ describe("runCodexAppServerAttempt", () => {
|
|||
content: [{ type: "text" as const, text: "file contents" }],
|
||||
details: {},
|
||||
}));
|
||||
testing.setOpenClawCodingToolsFactoryForTests((options) => {
|
||||
|
||||
const { sessionFile, workspaceDir } = createRunPaths();
|
||||
const harness = createStartedThreadHarness();
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestToolFactory(params, (options) => {
|
||||
const tools = createOpenClawCodingTools(options).filter((tool) => tool.name === "read");
|
||||
for (const tool of tools) {
|
||||
tool.execute = executeRead;
|
||||
}
|
||||
return tools;
|
||||
});
|
||||
const { sessionFile, workspaceDir } = createRunPaths();
|
||||
const harness = createStartedThreadHarness();
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
setCodexTestModelSupportsTools(params, true);
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.toolsAllow = ["read"];
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { readAttemptTerminal } from "./attempt-terminal.test-helper.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import { setCodexTestToolFactory } from "./host-capability.test-support.js";
|
||||
import {
|
||||
createCodexRuntimePlanFixture,
|
||||
createParams,
|
||||
|
|
@ -42,10 +42,8 @@ describe("Codex workspace instruction snapshots", () => {
|
|||
const updatedGuidance = "Later workspace changes wait for a new session.";
|
||||
await fs.mkdir(agentWorkspaceDir, { recursive: true });
|
||||
await fs.writeFile(path.join(agentWorkspaceDir, "AGENTS.md"), initialGuidance);
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () => [
|
||||
createRuntimeDynamicTool("memory_get"),
|
||||
];
|
||||
const params = createParams(sessionFile, executionDir);
|
||||
setCodexTestToolFactory(params, () => [createRuntimeDynamicTool("memory_get")]);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.bootstrapWorkspaceDir = agentWorkspaceDir;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import * as authBridge from "./auth-bridge.js";
|
|||
import { runBoundedCodexAppServerTurn } from "./bounded-turn.js";
|
||||
import { CodexAppServerClient } from "./client.js";
|
||||
import { resolveCodexSupervisionAppServerRuntimeOptions } from "./config.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import { setCodexTestToolFactory } from "./host-capability.test-support.js";
|
||||
import { createCodexNativeTestState } from "./native-app-server.test-support.js";
|
||||
import { buildCodexAppServerConnectionFingerprint } from "./plugin-app-cache-key.js";
|
||||
import { assertCodexThreadStartResponse } from "./protocol-validators.js";
|
||||
|
|
@ -342,7 +342,7 @@ async function createRunParams(fixture: NativeFixture) {
|
|||
params.permissionMode = "full";
|
||||
params.timeoutMs = 20_000;
|
||||
params.config = { tools: { web: { search: { enabled: false } } } };
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = () => [];
|
||||
setCodexTestToolFactory(params, () => []);
|
||||
registerCodexTestSessionIdentity(params.sessionFile, params.sessionId, params.sessionKey);
|
||||
return params;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { createAdmittedHostCapabilityTestFixture } from "openclaw/plugin-sdk/plu
|
|||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import * as clientCleanup from "./attempt-client-cleanup.js";
|
||||
import { codexTestTurnIds } from "./codex-app-server.test-fixtures.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import { CodexEphemeralTurn } from "./ephemeral-turn.js";
|
||||
import { CodexNativeToolLifecycleProjector } from "./event-projector-native-tool-lifecycle.js";
|
||||
import { createCodexTestBindingStore } from "./session-binding.test-helpers.js";
|
||||
import {
|
||||
createClientHarness,
|
||||
createCodexTestModel,
|
||||
|
|
@ -14,10 +14,10 @@ import {
|
|||
} from "./test-support.js";
|
||||
|
||||
const {
|
||||
readCodexAppServerBindingMock,
|
||||
getSharedCodexAppServerClientMock,
|
||||
retireSharedCodexAppServerClientIfCurrentMock,
|
||||
runCodexAppServerSideQuestion,
|
||||
runCodexAppServerSideQuestionImpl,
|
||||
createFakeClient,
|
||||
threadResult,
|
||||
turnStartResult,
|
||||
|
|
@ -32,7 +32,6 @@ describe("runCodexAppServerSideQuestion", () => {
|
|||
useSideQuestionTestSetup();
|
||||
|
||||
it("executes inherited Gateway shell tools through the side run's host authority", async () => {
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = undefined;
|
||||
const workspaceDir = tempDirs.make("codex-side-gateway-shell-");
|
||||
const config = { tools: { exec: { host: "gateway" as const, mode: "full" as const } } };
|
||||
const runId = "side-gateway-shell";
|
||||
|
|
@ -51,8 +50,7 @@ describe("runCodexAppServerSideQuestion", () => {
|
|||
const client = createFakeClient({ completeTurn: false, onTurnStart: turnStarted.resolve });
|
||||
getSharedCodexAppServerClientMock.mockResolvedValue(client);
|
||||
const parent = { threadId: "parent-thread", cwd: workspaceDir, model: "gpt-5.5" };
|
||||
readCodexAppServerBindingMock.mockReturnValue(parent);
|
||||
const run = runCodexAppServerSideQuestion(
|
||||
const run = runCodexAppServerSideQuestionImpl(
|
||||
sideParams({
|
||||
cfg: config,
|
||||
runtimeModel: createCodexTestModel("openai"),
|
||||
|
|
@ -70,6 +68,7 @@ describe("runCodexAppServerSideQuestion", () => {
|
|||
hostCapabilities: host.hostCapabilities,
|
||||
opts: { runId },
|
||||
}),
|
||||
{ bindingStore: { ...createCodexTestBindingStore(), read: () => parent } },
|
||||
);
|
||||
try {
|
||||
await Promise.race([
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { nativeHookRelayTesting } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { setHostToolFactoryForTest } from "openclaw/plugin-sdk/agent-runtime-test-contracts";
|
||||
import { resetDiagnosticEventsForTest } from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
import { resetGlobalHookRunner } from "openclaw/plugin-sdk/hook-runtime";
|
||||
import { afterEach, beforeEach, expect, vi } from "vitest";
|
||||
|
|
@ -6,7 +7,11 @@ import {
|
|||
codexTestTurnIds,
|
||||
createFakeCodexAppServerClient,
|
||||
} from "./codex-app-server.test-fixtures.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import {
|
||||
createCodexTestHostCapabilities,
|
||||
getCodexTestToolFactory,
|
||||
setCodexTestToolFactory,
|
||||
} from "./host-capability.test-support.js";
|
||||
import { isJsonObject, type CodexServerNotification, type JsonObject } from "./protocol.js";
|
||||
import {
|
||||
createCodexTestBindingStore,
|
||||
|
|
@ -77,10 +82,16 @@ const bindingStore: CodexAppServerBindingStore = {
|
|||
read: (...args) => readCodexAppServerBindingMock(...args),
|
||||
};
|
||||
|
||||
function runCodexAppServerSideQuestion(
|
||||
async function runCodexAppServerSideQuestion(
|
||||
params: Parameters<typeof runCodexAppServerSideQuestionImpl>[0],
|
||||
options: Omit<Parameters<typeof runCodexAppServerSideQuestionImpl>[1], "bindingStore"> = {},
|
||||
) {
|
||||
const runId = params.opts?.runId;
|
||||
if (runId && !getCodexTestToolFactory(params)) {
|
||||
await setHostToolFactoryForTest({ runId }, (toolOptions) =>
|
||||
createOpenClawCodingToolsMock(toolOptions),
|
||||
);
|
||||
}
|
||||
return runCodexAppServerSideQuestionImpl(params, { ...options, bindingStore });
|
||||
}
|
||||
|
||||
|
|
@ -269,6 +280,11 @@ const TEST_HOST_CAPABILITIES: SideQuestionParams["hostCapabilities"] = Object.fr
|
|||
});
|
||||
|
||||
function sideParams(overrides: Partial<SideQuestionParams> = {}): SideQuestionParams {
|
||||
let hostCapabilities = overrides.hostCapabilities ?? TEST_HOST_CAPABILITIES;
|
||||
if (!hostCapabilities.createToolSurface) {
|
||||
hostCapabilities = createCodexTestHostCapabilities(hostCapabilities);
|
||||
setCodexTestToolFactory({ hostCapabilities }, createOpenClawCodingToolsMock);
|
||||
}
|
||||
const authProfileId = Object.hasOwn(overrides, "authProfileId")
|
||||
? overrides.authProfileId
|
||||
: "openai:work";
|
||||
|
|
@ -330,7 +346,7 @@ function sideParams(overrides: Partial<SideQuestionParams> = {}): SideQuestionPa
|
|||
modelRegistry: {} as never,
|
||||
},
|
||||
...overrides,
|
||||
hostCapabilities: overrides.hostCapabilities ?? TEST_HOST_CAPABILITIES,
|
||||
hostCapabilities,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -342,8 +358,6 @@ export function useSideQuestionTestSetup() {
|
|||
getSharedCodexAppServerClientMock.mockReset();
|
||||
retireSharedCodexAppServerClientIfCurrentMock.mockReset();
|
||||
createOpenClawCodingToolsMock.mockReset();
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = (...args) =>
|
||||
createOpenClawCodingToolsMock(...args);
|
||||
toolExecuteMock.mockReset();
|
||||
handleCodexAppServerApprovalRequestMock.mockReset();
|
||||
resolveCodexProviderWebSearchSupportForClientMock.mockReset();
|
||||
|
|
@ -393,7 +407,6 @@ export function useSideQuestionTestSetup() {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
dynamicToolBuildState.openClawCodingToolsFactory = undefined;
|
||||
await nativeHookRelayTesting.clearNativeHookRelaysForTests();
|
||||
resetDiagnosticEventsForTest();
|
||||
resetGlobalHookRunner();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import path from "node:path";
|
||||
import { AuthStorage, ModelRegistry } from "openclaw/plugin-sdk/agent-sessions";
|
||||
import { createAgentHarnessHostCapabilitiesForTest } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { withTempDir } from "openclaw/plugin-sdk/test-env";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createCopilotToolBridge } from "./tool-bridge.js";
|
||||
|
||||
describe("Copilot GitHub publication tools", () => {
|
||||
it.each([
|
||||
{ available: undefined, profile: "coding", expected: [] },
|
||||
{ available: false, profile: "coding", expected: ["github_identity_status"] },
|
||||
{ available: true, profile: "coding", expected: ["github_identity_status", "github_publish"] },
|
||||
{ available: true, profile: "messaging", expected: [] },
|
||||
] as const)(
|
||||
"exposes host-prepared GitHub tools: $available / $profile",
|
||||
async ({ available, profile, expected }) => {
|
||||
await withTempDir("openclaw-copilot-github-tools-", async (workspaceDir) => {
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
const attempt: Parameters<typeof createAgentHarnessHostCapabilitiesForTest>[0]["attempt"] =
|
||||
{
|
||||
agentId: "main",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||
runId: "copilot-github-tools",
|
||||
workspaceDir,
|
||||
prompt: "Inspect the repository",
|
||||
timeoutMs: 5_000,
|
||||
provider: "github-copilot",
|
||||
modelId: "gpt-4o",
|
||||
model: {
|
||||
id: "gpt-4o",
|
||||
name: "GPT-4o",
|
||||
api: "openai-completions",
|
||||
provider: "github-copilot",
|
||||
baseUrl: "https://example.com",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 4_096,
|
||||
},
|
||||
thinkLevel: "off",
|
||||
authStorage,
|
||||
modelRegistry: ModelRegistry.inMemory(authStorage),
|
||||
authProfileStore: { version: 1, profiles: {} },
|
||||
config: { tools: { profile } },
|
||||
githubPublicationAvailable: available,
|
||||
};
|
||||
const host = await createAgentHarnessHostCapabilitiesForTest({
|
||||
attempt,
|
||||
pluginId: "copilot",
|
||||
});
|
||||
let bridge: Awaited<ReturnType<typeof createCopilotToolBridge>> | undefined;
|
||||
try {
|
||||
bridge = await createCopilotToolBridge({
|
||||
agentId: "main",
|
||||
sessionId: "session-1",
|
||||
modelId: "gpt-4o",
|
||||
modelProvider: "github-copilot",
|
||||
spawnWorkspaceDir: undefined,
|
||||
workspaceDir,
|
||||
attemptParams: { ...attempt, hostCapabilities: host.capabilities },
|
||||
});
|
||||
const tools = bridge.promptToolPolicy.apply().callableToolNames;
|
||||
expect(tools.filter((name) => name.startsWith("github_"))).toEqual(expected);
|
||||
} finally {
|
||||
bridge?.cleanup?.();
|
||||
host.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
// Copilot plugin module implements tool bridge behavior.
|
||||
import {
|
||||
convertMcpCallToolResult,
|
||||
type Tool as SdkTool,
|
||||
|
|
@ -53,20 +52,6 @@ interface CopilotSessionHolder {
|
|||
current: { abort?: () => unknown } | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Structural subset of `EmbeddedRunAttemptParamsV2` carried into the tool
|
||||
* bridge for PI-parity tool context (see
|
||||
* `src/agents/pi-embedded-runner/run/attempt.ts:1029-1117` — the
|
||||
* authoritative `createOpenClawCodingTools({...})` call shape).
|
||||
*
|
||||
* Declared from `EmbeddedRunAttemptParamsV2` (imported from the
|
||||
* `openclaw/plugin-sdk/agent-harness-runtime` boundary, *not* from
|
||||
* `attempt.ts` in this extension) to avoid an `attempt.ts` ↔
|
||||
* `tool-bridge.ts` import cycle while keeping the field shapes
|
||||
* authoritative. Production callers pass the live attempt params; test
|
||||
* fixtures can use the flat fields below for minimal-config wiring, but every
|
||||
* constructed tool surface still requires the host-bound capability.
|
||||
*/
|
||||
type CopilotToolAttemptParams = Partial<Omit<EmbeddedRunAttemptParamsV2, "hostCapabilities">> &
|
||||
Pick<EmbeddedRunAttemptParamsV2, "hostCapabilities">;
|
||||
|
||||
|
|
@ -110,15 +95,6 @@ interface CopilotToolBridgeInput {
|
|||
*/
|
||||
spawnWorkspaceDir: string | undefined;
|
||||
abortSignal?: AbortSignal;
|
||||
/**
|
||||
* Full PI-parity attempt parameters. When set, the bridge forwards
|
||||
* identity, channel, owner/policy, auth-profile, message-routing,
|
||||
* model, and run-trace fields to `createOpenClawCodingTools` so the
|
||||
* wrapped-tool enforcement layer
|
||||
* (`src/agents/pi-tools.before-tool-call.ts`) receives the same
|
||||
* context the in-tree PI runner provides. See
|
||||
* `src/agents/pi-embedded-runner/run/attempt.ts:1029-1117`.
|
||||
*/
|
||||
attemptParams: CopilotToolAttemptParams;
|
||||
/**
|
||||
* Mutable session holder used to wire `onYield` to the live
|
||||
|
|
@ -132,10 +108,7 @@ interface CopilotToolBridgeInput {
|
|||
* the in-flight SDK session; this callback lets the caller track
|
||||
* the yield so the final attempt result can carry
|
||||
* `yieldDetected: true` (the parent runner uses it to mark
|
||||
* liveness as paused and stop_reason as `end_turn`). Mirrors
|
||||
* the PI/codex contract — see
|
||||
* `src/agents/pi-embedded-runner/run/attempt.ts:1107-1113` and
|
||||
* `extensions/codex/src/app-server/run-attempt.ts:539-541`.
|
||||
* liveness as paused and stop_reason as `end_turn`).
|
||||
*/
|
||||
onYieldDetected?: (message?: string, acknowledgment?: string) => void;
|
||||
onToolCompleted?: (completion: CopilotToolCompletion) => void | Promise<void>;
|
||||
|
|
@ -361,17 +334,11 @@ function buildOpenClawCodingToolsOptions(
|
|||
): OpenClawCodingToolsOptions {
|
||||
const a = input.attemptParams;
|
||||
|
||||
// Mirror PI's `sandboxSessionKey` derivation (attempt.ts:873-874) so
|
||||
// wrapped tools see the same policy key PI uses. When the attempt
|
||||
// exposes neither sandboxSessionKey nor sessionKey, fall back to the
|
||||
// flat input.sessionKey/sessionId.
|
||||
// Sandbox policy may belong to a different key than the live session.
|
||||
const sandboxSessionKey =
|
||||
a.sandboxSessionKey?.trim() || a.sessionKey?.trim() || input.sessionKey || input.sessionId;
|
||||
|
||||
// When sandboxSessionKey differs from the real run session key (e.g.
|
||||
// Telegram direct peer key vs `agent:main:main`), pass the live key
|
||||
// so `session_status: "current"` resolves to the active run session,
|
||||
// not the stale sandbox key. Mirrors PI attempt.ts:1057-1060.
|
||||
// Keep current-session reads on the live run rather than its sandbox policy key.
|
||||
const liveSessionKey = a.sessionKey ?? input.sessionKey;
|
||||
const runSessionKey =
|
||||
liveSessionKey && liveSessionKey !== sandboxSessionKey ? liveSessionKey : undefined;
|
||||
|
|
@ -451,18 +418,11 @@ function buildOpenClawCodingToolsOptions(
|
|||
forceHeartbeatTool: a.forceHeartbeatTool,
|
||||
authProfileStore: a.toolAuthProfileStore ?? a.authProfileStore,
|
||||
computerContextEpoch: input.computerContextEpoch,
|
||||
// recordToolPrepStage intentionally omitted: copilot does not
|
||||
// surface attempt-stage telemetry yet. Codex omits this too.
|
||||
onToolOutcome: a.onToolOutcome,
|
||||
isTurnTainted: a.isTurnTainted,
|
||||
onYield: (message, acknowledgment) => {
|
||||
// Notify the caller first so the final attempt result can carry
|
||||
// yieldDetected even if the abort below races a concurrent
|
||||
// settle path. Errors thrown by the caller's handler must not
|
||||
// skip the abort, so wrap defensively. Mirrors PI (`attempt.ts`
|
||||
// sets `yieldDetected = true; yieldMessage = message;` before
|
||||
// calling abort) and codex (`onYieldDetected()` runs before the
|
||||
// run-abort controller fires).
|
||||
// Record the yield before abort can settle the attempt; a callback failure
|
||||
// must not prevent interruption of the native session.
|
||||
try {
|
||||
input.onYieldDetected?.(message, acknowledgment);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
import { prepareGitHubPublicationAvailability } from "../../../gateway/github-publication-availability.js";
|
||||
import { getGatewayContextResolver } from "../../../plugins/runtime/gateway-request-scope.js";
|
||||
import { agentHarnessExposesOpenClawTools } from "../../harness/tool-surface.js";
|
||||
import {
|
||||
createAdmittedGatewayToolCallerIdentity,
|
||||
withGatewayToolCallerIdentity,
|
||||
} from "../../tools/gateway-caller-context.js";
|
||||
import type { EmbeddedRunAttemptParams } from "./types.js";
|
||||
|
||||
/** Prepare Gateway tools once at the shared dispatch boundary, including internal continuations. */
|
||||
export async function withPreparedEmbeddedGatewayTools<T>(
|
||||
attempt: Pick<
|
||||
EmbeddedRunAttemptParams,
|
||||
| "admittedRunContext"
|
||||
| "cronCreatorAuthorityCapability"
|
||||
| "messageChannel"
|
||||
| "messageProvider"
|
||||
| "currentMessagingTarget"
|
||||
| "currentChannelId"
|
||||
| "agentAccountId"
|
||||
| "currentThreadTs"
|
||||
| "sessionId"
|
||||
| "disableTools"
|
||||
| "sessionPersistence"
|
||||
| "githubPublicationAvailable"
|
||||
> & { agentId: string; sessionKey: string; agentHarnessId: string },
|
||||
isAttemptCurrent: () => boolean,
|
||||
run: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
const callerIdentity = createAdmittedGatewayToolCallerIdentity({
|
||||
admittedRunContext: attempt.admittedRunContext,
|
||||
cronAuthorityCheck: attempt.cronCreatorAuthorityCapability?.isCurrent,
|
||||
agentId: attempt.agentId,
|
||||
sessionKey: attempt.sessionKey,
|
||||
turnSourceChannel: attempt.messageChannel ?? attempt.messageProvider,
|
||||
turnSourceLocal:
|
||||
!attempt.messageChannel &&
|
||||
!attempt.messageProvider &&
|
||||
attempt.cronCreatorAuthorityCapability?.callerOrigin.kind === "local"
|
||||
? true
|
||||
: undefined,
|
||||
turnSourceTo: attempt.currentMessagingTarget ?? attempt.currentChannelId,
|
||||
turnSourceAccountId: attempt.agentAccountId,
|
||||
turnSourceThreadId: attempt.currentThreadTs,
|
||||
});
|
||||
return withGatewayToolCallerIdentity(callerIdentity, async () => {
|
||||
const resolveGatewayContext = getGatewayContextResolver(attempt.admittedRunContext);
|
||||
const gateway = resolveGatewayContext?.();
|
||||
if (
|
||||
!attempt.disableTools &&
|
||||
attempt.sessionPersistence !== "detached" &&
|
||||
agentHarnessExposesOpenClawTools(attempt.agentHarnessId) &&
|
||||
gateway &&
|
||||
!gateway.localEmbedded
|
||||
) {
|
||||
// Yield, compaction, and retries recheck the current session and exact live host;
|
||||
// an earlier attempt's availability must not determine its successor's tool catalog.
|
||||
const isCurrent = () => isAttemptCurrent() && resolveGatewayContext?.() === gateway;
|
||||
attempt.githubPublicationAvailable = await prepareGitHubPublicationAvailability({
|
||||
sessionId: attempt.sessionId,
|
||||
sessionKey: attempt.sessionKey,
|
||||
agentId: attempt.agentId,
|
||||
assertCurrent: isCurrent,
|
||||
});
|
||||
if (!isCurrent()) {
|
||||
throw new Error("GitHub tool preparation outlived its admitted Gateway run");
|
||||
}
|
||||
}
|
||||
return run();
|
||||
});
|
||||
}
|
||||
|
|
@ -1,16 +1,9 @@
|
|||
import type { ThinkLevel } from "../../../auto-reply/thinking.js";
|
||||
import type { GroupToolPolicyConfig } from "../../../config/types.tools.js";
|
||||
import { prepareGitHubPublicationAvailability } from "../../../gateway/github-publication-availability.js";
|
||||
import {
|
||||
freezeDiagnosticTraceContext,
|
||||
type DiagnosticTraceContext,
|
||||
} from "../../../infra/diagnostic-trace-context.js";
|
||||
import { getGatewayContextResolver } from "../../../plugins/runtime/gateway-request-scope.js";
|
||||
import { agentHarnessExposesOpenClawTools } from "../../harness/tool-surface.js";
|
||||
import {
|
||||
createAdmittedGatewayToolCallerIdentity,
|
||||
withGatewayToolCallerIdentity,
|
||||
} from "../../tools/gateway-caller-context.js";
|
||||
import { mergeForcedEmbeddedAttemptToolsAllow } from "./attempt-tool-construction-plan.js";
|
||||
import type { EmbeddedRunTrigger, RunEmbeddedAgentParams } from "./params.js";
|
||||
import type { EmbeddedRunAttemptParams } from "./types.js";
|
||||
|
|
@ -130,66 +123,3 @@ export function buildEmbeddedAttemptToolRunContext(
|
|||
...(params.trace ? { trace: freezeDiagnosticTraceContext(params.trace) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Prepare Gateway tools once at the shared dispatch boundary, including internal continuations. */
|
||||
export async function withPreparedEmbeddedGatewayTools<T>(
|
||||
attempt: Pick<
|
||||
EmbeddedRunAttemptParams,
|
||||
| "admittedRunContext"
|
||||
| "cronCreatorAuthorityCapability"
|
||||
| "messageChannel"
|
||||
| "messageProvider"
|
||||
| "currentMessagingTarget"
|
||||
| "currentChannelId"
|
||||
| "agentAccountId"
|
||||
| "currentThreadTs"
|
||||
| "sessionId"
|
||||
| "disableTools"
|
||||
| "sessionPersistence"
|
||||
| "githubPublicationAvailable"
|
||||
> & { agentId: string; sessionKey: string; agentHarnessId: string },
|
||||
isAttemptCurrent: () => boolean,
|
||||
run: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
const callerIdentity = createAdmittedGatewayToolCallerIdentity({
|
||||
admittedRunContext: attempt.admittedRunContext,
|
||||
cronAuthorityCheck: attempt.cronCreatorAuthorityCapability?.isCurrent,
|
||||
agentId: attempt.agentId,
|
||||
sessionKey: attempt.sessionKey,
|
||||
turnSourceChannel: attempt.messageChannel ?? attempt.messageProvider,
|
||||
turnSourceLocal:
|
||||
!attempt.messageChannel &&
|
||||
!attempt.messageProvider &&
|
||||
attempt.cronCreatorAuthorityCapability?.callerOrigin.kind === "local"
|
||||
? true
|
||||
: undefined,
|
||||
turnSourceTo: attempt.currentMessagingTarget ?? attempt.currentChannelId,
|
||||
turnSourceAccountId: attempt.agentAccountId,
|
||||
turnSourceThreadId: attempt.currentThreadTs,
|
||||
});
|
||||
return withGatewayToolCallerIdentity(callerIdentity, async () => {
|
||||
const resolveGatewayContext = getGatewayContextResolver(attempt.admittedRunContext);
|
||||
const gateway = resolveGatewayContext?.();
|
||||
if (
|
||||
!attempt.disableTools &&
|
||||
attempt.sessionPersistence !== "detached" &&
|
||||
agentHarnessExposesOpenClawTools(attempt.agentHarnessId) &&
|
||||
gateway &&
|
||||
!gateway.localEmbedded
|
||||
) {
|
||||
// Yield, compaction, and retries recheck the current session and exact live host;
|
||||
// an earlier attempt's availability must not determine its successor's tool catalog.
|
||||
const isCurrent = () => isAttemptCurrent() && resolveGatewayContext?.() === gateway;
|
||||
attempt.githubPublicationAvailable = await prepareGitHubPublicationAvailability({
|
||||
sessionId: attempt.sessionId,
|
||||
sessionKey: attempt.sessionKey,
|
||||
agentId: attempt.agentId,
|
||||
assertCurrent: isCurrent,
|
||||
});
|
||||
if (!isCurrent()) {
|
||||
throw new Error("GitHub tool preparation outlived its admitted Gateway run");
|
||||
}
|
||||
}
|
||||
return run();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ import { remapSkillReferencePaths } from "../sandbox-skills.js";
|
|||
import { prepareEmbeddedSkills } from "../skill-runtime.js";
|
||||
import { mapThinkingLevelForProvider } from "../utils.js";
|
||||
import { prepareExecApprovalContinuationForAttempt } from "./attempt-exec-approval-continuation.js";
|
||||
import { withPreparedEmbeddedGatewayTools } from "./attempt-gateway-tools.js";
|
||||
import { applyResolvedToolPromptFinalizer } from "./attempt-prompt-support.js";
|
||||
import { EMBEDDED_RUN_ATTEMPT_DISPATCH_STAGE } from "./attempt-stage-timing.js";
|
||||
import { withPreparedEmbeddedGatewayTools } from "./attempt-tool-run-context.js";
|
||||
import { resolveAttemptDispatchApiKey } from "./auth-store.js";
|
||||
import { runEmbeddedAttemptWithBackend } from "./backend.js";
|
||||
import type { PreparedEmbeddedRunInput } from "./execution-context.js";
|
||||
|
|
|
|||
|
|
@ -156,6 +156,32 @@ afterEach(() => {
|
|||
});
|
||||
|
||||
describe("agent harness host capability", () => {
|
||||
it.each([
|
||||
{ available: undefined, expected: [] },
|
||||
{ available: false, expected: ["github_identity_status"] },
|
||||
{ available: true, expected: ["github_identity_status", "github_publish"] },
|
||||
])(
|
||||
"captures GitHub availability independently of plugin inputs: $available",
|
||||
async ({ available, expected }) => {
|
||||
const { attempt } = await admittedAttempt("github-tools", {
|
||||
githubPublicationAvailable: available,
|
||||
});
|
||||
const host = createAgentHarnessHostCapabilities({ attempt, pluginId: "copilot" });
|
||||
attempt.githubPublicationAvailable = available !== true;
|
||||
try {
|
||||
const tools = host.capabilities.createToolSurface?.({
|
||||
githubPublicationAvailable: available !== true,
|
||||
config: { tools: { profile: "coding" } },
|
||||
});
|
||||
expect(
|
||||
tools?.filter((tool) => tool.name.startsWith("github_")).map((tool) => tool.name),
|
||||
).toEqual(expected);
|
||||
} finally {
|
||||
host.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["restart", "unrelated scope", "user abort", "timeout"] as const)(
|
||||
"preserves the original cancellation when a startup capability closes: %s",
|
||||
async (reason) => {
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ export function createAgentHarnessHostCapabilities(params: {
|
|||
runWithScope: <T>(run: () => Promise<T>) => Promise<T>;
|
||||
} {
|
||||
const attempt = params.attempt;
|
||||
const githubPublicationAvailable = attempt.githubPublicationAvailable;
|
||||
const workSignal = getAsyncWorkSignal();
|
||||
const attemptSignal = attempt.abortSignal;
|
||||
const installationTarget = getInstallationTarget();
|
||||
|
|
@ -513,6 +514,8 @@ export function createAgentHarnessHostCapabilities(params: {
|
|||
createOpenClawCodingToolsInternal(
|
||||
{
|
||||
...options,
|
||||
// Availability belongs to this prepared host, not mutable plugin inputs.
|
||||
githubPublicationAvailable,
|
||||
skillsSnapshot: options?.skillsSnapshot ?? skillsSnapshot,
|
||||
skillUsagePaths: options?.skillUsagePaths ?? skillUsagePaths,
|
||||
operationalRunInstance,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { registerPreparedModelRuntimeClose } from "./prepared-model-runtime.life
|
|||
import {
|
||||
discardPreparedPluginGeneration,
|
||||
registerPreparedPluginLifetime,
|
||||
retainPreparedPluginRegistry,
|
||||
} from "./prepared-model-runtime.plugin-lifetime.js";
|
||||
import { PreparedModelRuntimeBuildResources } from "./prepared-model-runtime.resources.js";
|
||||
import { prepareAgentCatalogSource } from "./prepared-model-runtime.scoped-catalog.js";
|
||||
|
|
@ -60,7 +61,7 @@ export type PreparedModelRuntimeBuildCandidate = Readonly<{
|
|||
isGenerationCurrent?: () => boolean;
|
||||
isBuildCurrent?: () => boolean;
|
||||
onBeforeAuthCapture?: () => void;
|
||||
ownsRegistryResources?: boolean;
|
||||
inspectRegistry?: boolean;
|
||||
}>;
|
||||
|
||||
export type PreparedModelRuntimeBuildResult = Readonly<{
|
||||
|
|
@ -84,12 +85,12 @@ function groupBuildCandidates<K>(
|
|||
|
||||
async function buildSnapshotBatch(
|
||||
requestedCandidates: readonly PreparedModelRuntimeBuildCandidate[],
|
||||
registryResources: PreparedModelRuntimeBuildResources,
|
||||
catalogMode: PreparedModelRuntimeCatalogMode,
|
||||
pluginMetadataSnapshot?: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"],
|
||||
onBuildStats?: (stats: PreparedModelRuntimeBuildStats) => void,
|
||||
includeCredentialProviders = catalogMode === "live",
|
||||
onStage?: (stage: string) => void,
|
||||
registryResources?: PreparedModelRuntimeBuildResources,
|
||||
onPrepared?: (input: PreparedModelRuntimeInput, result: PreparedModelRuntimeBuildResult) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<PreparedModelRuntimeBuildResult[]> {
|
||||
|
|
@ -160,8 +161,8 @@ async function buildSnapshotBatch(
|
|||
[
|
||||
...groupBuildCandidates(generationCandidates, (candidate) => {
|
||||
const workspace = preparedModelRuntimeWorkspaceFactsKey(candidate.input);
|
||||
if (candidate.ownsRegistryResources) {
|
||||
return `owned\0${workspace}`;
|
||||
if (candidate.inspectRegistry) {
|
||||
return `inspection\0${workspace}`;
|
||||
}
|
||||
const kind = candidate.prepareInboundPluginRegistry ? "configured" : "dynamic";
|
||||
return pluginGeneration ? workspace : `${kind}\0${workspace}`;
|
||||
|
|
@ -229,8 +230,9 @@ async function buildSnapshotBatch(
|
|||
onBeforeAuthCapture: (input) => candidateByInput.get(input)!.onBeforeAuthCapture?.(),
|
||||
onStage,
|
||||
signal,
|
||||
...(groupCandidates.some((candidate) => candidate.ownsRegistryResources)
|
||||
? { registryResources }
|
||||
registryResources,
|
||||
...(groupCandidates.some((candidate) => candidate.inspectRegistry)
|
||||
? { loadRuntimeRegistry: registryResources.load.bind(registryResources) }
|
||||
: {}),
|
||||
},
|
||||
prepareInboundPluginRegistry ? loadInboundPluginRegistry : undefined,
|
||||
|
|
@ -462,7 +464,9 @@ export function startSerializedSnapshotBuildBatch(
|
|||
const startBuild = (async () => {
|
||||
// Register before waiting: shutdown also owns resources from unfinished builds.
|
||||
registerPreparedPluginLifetime();
|
||||
await using registryResources = new PreparedModelRuntimeBuildResources();
|
||||
await using registryResources = new PreparedModelRuntimeBuildResources(
|
||||
retainPreparedPluginRegistry,
|
||||
);
|
||||
if (previousBuildCompletions.length > 0) {
|
||||
await Promise.all(previousBuildCompletions);
|
||||
// Queued publications register while the prior build settles. Recheck them here so a
|
||||
|
|
@ -472,6 +476,7 @@ export function startSerializedSnapshotBuildBatch(
|
|||
signal.throwIfAborted();
|
||||
return await buildSnapshotBatch(
|
||||
candidates,
|
||||
registryResources,
|
||||
catalogMode,
|
||||
pluginMetadataSnapshot,
|
||||
onBuildStats,
|
||||
|
|
@ -480,7 +485,6 @@ export function startSerializedSnapshotBuildBatch(
|
|||
stage = nextStage;
|
||||
progress?.onStage(nextStage);
|
||||
},
|
||||
registryResources,
|
||||
progress
|
||||
? (input, result) => {
|
||||
progress.onPrepared(input, result);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import { resolvePreparedProviderStaticConfigs } from "../plugins/provider-discov
|
|||
import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js";
|
||||
import { getPluginRegistryInspectionResources } from "../plugins/registry-inspection-resources.js";
|
||||
import { capturePluginLifecycleAuthority } from "../plugins/registry-lifecycle.js";
|
||||
import type { PluginRegistry } from "../plugins/registry-types.js";
|
||||
import { withPluginRuntimeGenerationScope } from "../plugins/runtime/generation-scope.js";
|
||||
import { resolveRuntimeSyntheticAuthProviderRefs } from "../plugins/synthetic-auth.runtime.js";
|
||||
import { prepareAmbientAgentCredentialsForDiscovery } from "./agent-auth-discovery.js";
|
||||
|
|
@ -71,7 +70,7 @@ import {
|
|||
discardPreparedPluginGeneration,
|
||||
retainPreparedPluginRegistry,
|
||||
} from "./prepared-model-runtime.plugin-lifetime.js";
|
||||
import type { PreparedModelRuntimeBuildResources } from "./prepared-model-runtime.resources.js";
|
||||
import { PreparedModelRuntimeBuildResources } from "./prepared-model-runtime.resources.js";
|
||||
import {
|
||||
listPreparedSyntheticAuthProviderRefs,
|
||||
prepareSyntheticAuth,
|
||||
|
|
@ -83,7 +82,6 @@ import type {
|
|||
PreparedModelRuntimeInput,
|
||||
PreparedModelRuntimePluginGeneration,
|
||||
} from "./prepared-model-runtime.types.js";
|
||||
import { releaseRuntimePluginWork, retainRuntimePluginWork } from "./runtime-plugin-work.js";
|
||||
import { AuthStorage } from "./sessions/auth-storage.js";
|
||||
|
||||
type PreparedConfiguredRegistryGroup = {
|
||||
|
|
@ -107,6 +105,7 @@ export async function prepareWorkspaceBuildGroup(
|
|||
assertCurrent?: (input: PreparedModelRuntimeInput) => void;
|
||||
onBeforeAuthCapture?: (input: PreparedModelRuntimeInput) => void;
|
||||
registryResources?: PreparedModelRuntimeBuildResources;
|
||||
loadRuntimeRegistry?: PreparedModelRuntimeBuildResources["load"];
|
||||
purpose?: RuntimePluginLoadPurpose;
|
||||
} = {},
|
||||
loadInboundPluginRegistry?: PreparedInboundRegistryLoader,
|
||||
|
|
@ -160,32 +159,22 @@ export async function prepareWorkspaceBuildGroup(
|
|||
const preferBuiltPluginArtifacts =
|
||||
reusablePluginGeneration?.preferBuiltPluginArtifacts ??
|
||||
options.preferBuiltPluginArtifacts === true;
|
||||
options.registryResources?.retainGeneration(reusablePluginGeneration);
|
||||
const retainedRegistries = new Set<PluginRegistry>();
|
||||
await using registryBorrows = new AsyncDisposableStack();
|
||||
await using localResources = new AsyncDisposableStack();
|
||||
const registryResources =
|
||||
options.registryResources ??
|
||||
localResources.use(new PreparedModelRuntimeBuildResources(retainPreparedPluginRegistry));
|
||||
registryResources.retainGeneration(reusablePluginGeneration);
|
||||
// Borrowing spans the caller's construction; registry acquisition is selected independently.
|
||||
const preparingRegistries = prepareWorkspacePluginRegistries(
|
||||
input,
|
||||
pluginMetadataSnapshot,
|
||||
(registry) => {
|
||||
// Initial run admission can inspect a new selection before its caller holds
|
||||
// a generation lease. Borrow the selected source before that async load.
|
||||
if (!retainedRegistries.has(registry)) {
|
||||
retainedRegistries.add(registry);
|
||||
const release = retainPreparedPluginRegistry(registry);
|
||||
if (release) {
|
||||
let releaseWork = () => {};
|
||||
// Record physical cleanup before replacement admission can refuse this build's work.
|
||||
registryBorrows.defer(() => releaseRuntimePluginWork(release, releaseWork));
|
||||
releaseWork = retainRuntimePluginWork([registry]);
|
||||
}
|
||||
}
|
||||
},
|
||||
(registry) => registryResources.retainRegistry(registry),
|
||||
loadInboundPluginRegistry,
|
||||
preferBuiltPluginArtifacts,
|
||||
reusablePluginGeneration,
|
||||
options.getConfiguredHarnessRuntimes,
|
||||
options.basePluginIds,
|
||||
options.registryResources,
|
||||
options.loadRuntimeRegistry,
|
||||
options.purpose,
|
||||
);
|
||||
const { inboundPluginRegistry, runtimePluginRegistry, primaryRegistry } =
|
||||
|
|
|
|||
|
|
@ -156,7 +156,9 @@ export function prepareWorkspacePluginRegistries(
|
|||
reusableGeneration?: PreparedModelRuntimePluginGeneration,
|
||||
getConfiguredHarnessRuntimes?: () => readonly string[],
|
||||
basePluginIds?: readonly string[],
|
||||
registryResources?: PreparedModelRuntimeBuildResources,
|
||||
loadRuntimeRegistry:
|
||||
| PreparedModelRuntimeBuildResources["load"]
|
||||
| typeof loadAgentRuntimePluginRegistryHandle = loadAgentRuntimePluginRegistryHandle,
|
||||
purpose?: RuntimePluginLoadPurpose,
|
||||
): PreparedWorkspacePluginRegistries | Promise<PreparedWorkspacePluginRegistries> {
|
||||
// Passive reads stay runtime-free; catalog workers and executable probes carry explicit scope.
|
||||
|
|
@ -190,9 +192,6 @@ export function prepareWorkspacePluginRegistries(
|
|||
}
|
||||
primaryRegistry ??= reusableGeneration?.mediaCapabilityProviderSource?.registry ?? baseRegistry;
|
||||
let loadedPrimaryRegistry: PluginRegistry | undefined;
|
||||
const loadRuntimeRegistry = registryResources
|
||||
? registryResources.load.bind(registryResources)
|
||||
: loadAgentRuntimePluginRegistryHandle;
|
||||
const runtimePluginRegistry =
|
||||
purpose === "model-catalog" || input.runtimePluginSelections || !baseRegistry
|
||||
? loadRuntimeRegistry(
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ export async function publishPreparedModelRuntimeOwnerBatch(params: {
|
|||
inventoryOwner: owner,
|
||||
pluginGeneration: owner.pendingPluginGeneration,
|
||||
prepareInboundPluginRegistry: owner.provenance === "configured",
|
||||
ownsRegistryResources:
|
||||
inspectRegistry:
|
||||
owner.provenance === "run" || (owner.provenance === "ephemeral" && input.readOnly === true),
|
||||
isGenerationCurrent,
|
||||
isBuildCurrent: params.isBuildCurrent ?? isCurrent,
|
||||
|
|
@ -681,7 +681,7 @@ export async function publishModelRuntimeSnapshot(
|
|||
}
|
||||
},
|
||||
prepareInboundPluginRegistry: provenance === "configured",
|
||||
ownsRegistryResources:
|
||||
inspectRegistry:
|
||||
provenance === "run" || (provenance === "ephemeral" && input.readOnly === true),
|
||||
pluginGeneration: reusablePluginGeneration,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -166,7 +166,11 @@ export function ownPreparedPluginGeneration(
|
|||
getPluginMetadataSnapshotCache(generation.pluginMetadataSnapshot),
|
||||
);
|
||||
const releases: Array<() => void | Promise<void>> = [];
|
||||
const retainedRegistries: PluginRegistry[] = [];
|
||||
const selectedRegistries = new Set(
|
||||
[generation.pluginRegistry, generation.inboundPluginRegistry].filter(
|
||||
(registry) => registry !== undefined,
|
||||
),
|
||||
);
|
||||
const acquisitionFailures: unknown[] = [];
|
||||
const lifetime = createLifetime(
|
||||
async () => {
|
||||
|
|
@ -182,14 +186,13 @@ export function ownPreparedPluginGeneration(
|
|||
);
|
||||
}
|
||||
},
|
||||
() => retainRuntimePluginWork(retainedRegistries),
|
||||
() => retainRuntimePluginWork(selectedRegistries),
|
||||
);
|
||||
try {
|
||||
for (const registry of new Set([generation.pluginRegistry, generation.inboundPluginRegistry])) {
|
||||
const release = registry && retainPreparedPluginRegistry(registry);
|
||||
if (registry && release) {
|
||||
for (const registry of selectedRegistries) {
|
||||
const release = retainPreparedPluginRegistry(registry);
|
||||
if (release) {
|
||||
releases.push(release);
|
||||
retainedRegistries.push(registry);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -7,21 +7,33 @@ import {
|
|||
} from "./prepared-model-runtime.test-harness.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createDeferred } from "../../test/helpers/promise.js";
|
||||
import {
|
||||
createPluginMetadataSnapshot,
|
||||
makeRegistry,
|
||||
} from "../config/plugin-auto-enable.test-helpers.js";
|
||||
import { PluginInstance } from "../plugins/plugin-instance.js";
|
||||
import { bindPluginRuntimeArtifactSelection } from "../plugins/plugin-runtime-artifact-binding.js";
|
||||
import { resolvePluginRuntimeArtifactSelection } from "../plugins/plugin-runtime-artifact-selection.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
||||
import { isPluginRegistryRetired } from "../plugins/registry-lifecycle.js";
|
||||
import { clearActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { setPluginRuntimeLoadContext } from "../plugins/runtime/load-context.js";
|
||||
import { createPluginRecord } from "../plugins/status.test-helpers.js";
|
||||
import {
|
||||
createOpenClawTestState,
|
||||
type OpenClawTestState,
|
||||
} from "../test-utils/openclaw-test-state.js";
|
||||
import { loadPreparedInboundPluginRegistry } from "./prepared-model-runtime.inbound-registry.js";
|
||||
import {
|
||||
acquireAgentRunPreparedModelRuntime,
|
||||
acquirePublishedPreparedModelRuntime,
|
||||
markPreparedModelRuntimeSnapshotsStale,
|
||||
prepareModelRuntimeSnapshot,
|
||||
publishPreparedModelRuntimeSnapshot,
|
||||
refreshPreparedModelRuntimeSnapshots,
|
||||
} from "./prepared-model-runtime.js";
|
||||
import { retainPreparedPluginRegistry } from "./prepared-model-runtime.plugin-lifetime.js";
|
||||
import { PreparedModelRuntimeBuildResources } from "./prepared-model-runtime.resources.js";
|
||||
import * as runtimePlugins from "./runtime-plugins.js";
|
||||
|
||||
const mocks = getPreparedModelRuntimeMocks();
|
||||
|
|
@ -36,31 +48,101 @@ afterEach(async ({ task }) => {
|
|||
await cleanupPreparedModelRuntimeHarness(state, task.result?.state === "fail");
|
||||
});
|
||||
|
||||
async function acquireConfiguredRegistryBorrower() {
|
||||
async function acquireConfiguredRegistryBorrower(source: "owned" | "gateway" = "owned") {
|
||||
mocks.configuredAgentIds = ["default"];
|
||||
const config = {};
|
||||
const workspaceDir = "/tmp/unused-workspace";
|
||||
const metadata = createPluginMetadataSnapshot({
|
||||
config,
|
||||
workspaceDir,
|
||||
manifestRegistry: makeRegistry([
|
||||
{ id: "prepared-registry-borrow", origin: "config", channels: [] },
|
||||
]),
|
||||
});
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const record = createPluginRecord({ id: "prepared-registry-borrow" });
|
||||
const manifest = metadata.plugins[0]!;
|
||||
const record = createPluginRecord({
|
||||
id: manifest.id,
|
||||
rootDir: manifest.rootDir,
|
||||
source: manifest.source,
|
||||
origin: manifest.origin,
|
||||
});
|
||||
registry.plugins.push(record);
|
||||
const instance = new PluginInstance(record.id, { record, registry });
|
||||
mocks.loadAgentRuntimePluginRegistryHandle.mockReturnValue(registry);
|
||||
const config = {};
|
||||
const input = {
|
||||
agentId: "default",
|
||||
config,
|
||||
agentDir: state.agentDir("default"),
|
||||
inheritedAuthDir: state.agentDir("default"),
|
||||
workspaceDir: "/tmp/unused-workspace",
|
||||
workspaceDir,
|
||||
...(source === "gateway" ? { allowGatewaySubagentBinding: true } : {}),
|
||||
};
|
||||
if (source === "gateway") {
|
||||
bindPluginRuntimeArtifactSelection(record, {
|
||||
runtimeEntry: resolvePluginRuntimeArtifactSelection({
|
||||
...manifest,
|
||||
entryKind: "runtime",
|
||||
preferBuiltPluginArtifacts: false,
|
||||
}),
|
||||
});
|
||||
setPluginRuntimeLoadContext(registry, {
|
||||
rawConfig: config,
|
||||
config,
|
||||
activationSourceConfig: config,
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir,
|
||||
env: process.env,
|
||||
metadataSnapshot: metadata,
|
||||
manifestRegistry: metadata.manifestRegistry,
|
||||
logger: { info() {}, warn() {}, error() {}, debug() {} },
|
||||
});
|
||||
setActivePluginRegistry(registry, undefined, "gateway-bindable", workspaceDir);
|
||||
expect(loadPreparedInboundPluginRegistry(input, metadata)).toBe(registry);
|
||||
expect(mocks.loadAgentRuntimePluginRegistryHandle).not.toHaveBeenCalled();
|
||||
}
|
||||
await refreshPreparedModelRuntimeSnapshots(config, {
|
||||
gatewayLifecycle: true,
|
||||
catalogMode: "static",
|
||||
...(source === "gateway"
|
||||
? { allowGatewaySubagentBinding: true, pluginMetadataSnapshot: metadata }
|
||||
: {}),
|
||||
});
|
||||
const borrower = await acquirePublishedPreparedModelRuntime(input);
|
||||
await borrower.snapshot.loadFullModelCatalog?.();
|
||||
return { registry, config, input, borrower, instance };
|
||||
return { registry, config, input, borrower, instance, metadata };
|
||||
}
|
||||
|
||||
describe("prepared registry construction borrows", () => {
|
||||
it("reacquires a refused source and rejects admission after construction closes", async () => {
|
||||
const { registry, borrower, instance } = await acquireConfiguredRegistryBorrower();
|
||||
const construction = new PreparedModelRuntimeBuildResources(retainPreparedPluginRegistry);
|
||||
await borrower[Symbol.asyncDispose]();
|
||||
const releaseReplacement = instance.reserveReplacement();
|
||||
try {
|
||||
expect(() => construction.retainRegistry(registry)).toThrow("replacement is in progress");
|
||||
releaseReplacement();
|
||||
construction.retainRegistry(registry);
|
||||
expect(() => instance.reserveReplacement()()).toThrow("active retained work");
|
||||
await construction[Symbol.asyncDispose]();
|
||||
instance.reserveReplacement()();
|
||||
expect(() => construction.retainRegistry(registry)).toThrow(
|
||||
"construction resources have been released",
|
||||
);
|
||||
const loads = mocks.loadAgentRuntimePluginRegistryHandle.mock.calls.length;
|
||||
await expect(construction.load({ config: {} }, () => {})).rejects.toThrow(
|
||||
"construction resources have been released",
|
||||
);
|
||||
expect(mocks.loadAgentRuntimePluginRegistryHandle).toHaveBeenCalledTimes(loads);
|
||||
markPreparedModelRuntimeSnapshotsStale("construction owner closed");
|
||||
expect(isPluginRegistryRetired(registry)).toBe(true);
|
||||
} finally {
|
||||
releaseReplacement();
|
||||
await construction[Symbol.asyncDispose]();
|
||||
await borrower[Symbol.asyncDispose]();
|
||||
}
|
||||
});
|
||||
|
||||
it("retains selected inbound resources until a cancelled initial run inspection settles", async () => {
|
||||
const { registry, input, borrower, instance } = await acquireConfiguredRegistryBorrower();
|
||||
const inspecting = createDeferred();
|
||||
|
|
@ -162,4 +244,80 @@ describe("prepared registry construction borrows", () => {
|
|||
}
|
||||
expect(isPluginRegistryRetired(registry)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(
|
||||
(["run", "configured", "explicit"] as const).flatMap((owner) =>
|
||||
(["owned", "gateway"] as const).map((source) => ({ owner, source })),
|
||||
),
|
||||
)(
|
||||
"keeps $owner/$source busy through post-facts projection, then permits idle replacement",
|
||||
async ({ owner, source }) => {
|
||||
const { registry, config, input, borrower, instance, metadata } =
|
||||
await acquireConfiguredRegistryBorrower(source);
|
||||
const projecting = createDeferred();
|
||||
const finishProjection = createDeferred();
|
||||
mocks.buildPreparedModelCatalogSnapshot.mockImplementationOnce(async () => {
|
||||
projecting.resolve();
|
||||
await finishProjection.promise;
|
||||
return { entries: [], routeVariants: [] };
|
||||
});
|
||||
let lease: Awaited<ReturnType<typeof acquireAgentRunPreparedModelRuntime>> | undefined;
|
||||
const pending =
|
||||
owner === "run"
|
||||
? acquireAgentRunPreparedModelRuntime(
|
||||
{
|
||||
...input,
|
||||
runtimePluginSelections: [
|
||||
{ provider: "custom", modelId: "selected", runtime: "openclaw" },
|
||||
],
|
||||
},
|
||||
{ catalogMode: "live", pluginGeneration: borrower.pluginGeneration },
|
||||
).then((acquired) => {
|
||||
lease = acquired;
|
||||
return acquired.snapshot;
|
||||
})
|
||||
: owner === "configured"
|
||||
? refreshPreparedModelRuntimeSnapshots(config, {
|
||||
gatewayLifecycle: true,
|
||||
catalogMode: "live",
|
||||
...(source === "gateway"
|
||||
? { allowGatewaySubagentBinding: true, pluginMetadataSnapshot: metadata }
|
||||
: {}),
|
||||
}).then(() => prepareModelRuntimeSnapshot(input))
|
||||
: publishPreparedModelRuntimeSnapshot(input, {
|
||||
force: true,
|
||||
provenance: "explicit",
|
||||
catalogMode: "live",
|
||||
});
|
||||
const settled = Promise.allSettled([pending]);
|
||||
try {
|
||||
await Promise.race([
|
||||
projecting.promise,
|
||||
pending.then(() => {
|
||||
throw new Error("Preparation skipped live catalog projection");
|
||||
}),
|
||||
]);
|
||||
await borrower[Symbol.asyncDispose]();
|
||||
expect(isPluginRegistryRetired(registry)).toBe(false);
|
||||
expect(() => instance.reserveReplacement()()).toThrow("active retained work");
|
||||
finishProjection.resolve();
|
||||
const snapshot = await pending;
|
||||
expect(snapshot.pluginRegistry).toBe(registry);
|
||||
if (lease) {
|
||||
expect(() => instance.reserveReplacement()()).toThrow("active retained work");
|
||||
await lease[Symbol.asyncDispose]();
|
||||
}
|
||||
expect(isPluginRegistryRetired(registry)).toBe(false);
|
||||
instance.reserveReplacement()();
|
||||
} finally {
|
||||
finishProjection.resolve();
|
||||
await settled;
|
||||
await lease?.[Symbol.asyncDispose]();
|
||||
await borrower[Symbol.asyncDispose]();
|
||||
if (source === "gateway") {
|
||||
await clearActivePluginRegistry(registry);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,23 +52,16 @@ class PreparedRegistryResources {
|
|||
}
|
||||
}
|
||||
|
||||
retain(work = false): PreparedModelRuntimeResourceClaim {
|
||||
retain(): PreparedModelRuntimeResourceClaim {
|
||||
this.assertOpen();
|
||||
const releaseWork = work ? retainRuntimePluginWork([this.acquired.registry]) : () => {};
|
||||
let claim: PreparedModelRuntimeResourceClaim;
|
||||
try {
|
||||
claim = this.acquired.resources.retain();
|
||||
} catch (error) {
|
||||
releaseWork();
|
||||
throw error;
|
||||
}
|
||||
const claim = this.acquired.resources.retain();
|
||||
this.claims++;
|
||||
let release: Promise<void> | undefined;
|
||||
return {
|
||||
release: () => {
|
||||
if (!release) {
|
||||
const completion = createDeferredCore();
|
||||
release = releaseRuntimePluginWork(() => completion.promise, releaseWork);
|
||||
release = completion.promise;
|
||||
const pending = this.trackRelease(claim.release);
|
||||
this.claims--;
|
||||
// The final generation lease joins original-view and donor cleanup as well.
|
||||
|
|
@ -123,19 +116,49 @@ class PreparedRegistryResources {
|
|||
}
|
||||
}
|
||||
|
||||
/** Construction holds every exact registry until publication has taken its own claim. */
|
||||
/** Batch scopes retain selected sources through later catalog work; idle publication owns custody only. */
|
||||
export class PreparedModelRuntimeBuildResources {
|
||||
private readonly claims = new Map<PreparedRegistryResources, PreparedModelRuntimeResourceClaim>();
|
||||
private readonly registries = new Set<PluginRegistry>();
|
||||
private readonly releases = new AsyncDisposableStack();
|
||||
|
||||
private retain(resources: PreparedRegistryResources | undefined): void {
|
||||
if (resources && !this.claims.has(resources)) {
|
||||
this.claims.set(resources, resources.retain(true));
|
||||
constructor(
|
||||
private readonly retainPhysicalRegistry: (
|
||||
registry: PluginRegistry,
|
||||
) => (() => void | Promise<void>) | undefined,
|
||||
) {}
|
||||
|
||||
private assertOpen(): void {
|
||||
if (this.releases.disposed) {
|
||||
throw new Error("Prepared registry construction resources have been released");
|
||||
}
|
||||
}
|
||||
|
||||
retainRegistry(registry: PluginRegistry): void {
|
||||
this.assertOpen();
|
||||
if (this.registries.has(registry)) {
|
||||
return;
|
||||
}
|
||||
const release = this.retainPhysicalRegistry(registry);
|
||||
let releaseWork = () => {};
|
||||
let completion: Promise<void> | undefined;
|
||||
const releaseClaim = () => (completion ??= releaseRuntimePluginWork(release, releaseWork));
|
||||
// External registry owners keep physical custody, but construction still owns finite work.
|
||||
this.releases.defer(releaseClaim);
|
||||
try {
|
||||
releaseWork = retainRuntimePluginWork([registry]);
|
||||
} catch (error) {
|
||||
// Acquisition cleanup may wait for this claim; start it now and let the stack join it.
|
||||
void releaseClaim().catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
this.registries.add(registry);
|
||||
}
|
||||
|
||||
retainGeneration(generation: PreparedModelRuntimePluginGeneration | undefined): void {
|
||||
for (const registry of [generation?.pluginRegistry, generation?.inboundPluginRegistry]) {
|
||||
this.retain(registry && state.registries.get(registry));
|
||||
if (registry) {
|
||||
this.retainRegistry(registry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,13 +166,14 @@ export class PreparedModelRuntimeBuildResources {
|
|||
params: Parameters<typeof acquireAgentRuntimePluginRegistry>[0],
|
||||
onPrimaryRegistry: (registry: PluginRegistry) => void,
|
||||
): Promise<PluginRegistry> {
|
||||
this.assertOpen();
|
||||
const assertLifetime = capturePreparedModelRuntimeLifetime();
|
||||
const acquired = await acquireAgentRuntimePluginRegistry(params);
|
||||
if ("resources" in acquired) {
|
||||
const resources = new PreparedRegistryResources(acquired);
|
||||
try {
|
||||
assertLifetime();
|
||||
this.retain(resources);
|
||||
this.retainRegistry(acquired.registry);
|
||||
// The build claim now owns finite work before producer custody crosses another await.
|
||||
acquired.releaseWork();
|
||||
} catch (error) {
|
||||
|
|
@ -157,7 +181,7 @@ export class PreparedModelRuntimeBuildResources {
|
|||
throw error;
|
||||
}
|
||||
} else {
|
||||
this.retain(state.registries.get(acquired.registry));
|
||||
this.retainRegistry(acquired.registry);
|
||||
}
|
||||
onPrimaryRegistry(
|
||||
state.registries.get(acquired.registry)?.primaryRegistry ?? acquired.primaryRegistry,
|
||||
|
|
@ -165,16 +189,9 @@ export class PreparedModelRuntimeBuildResources {
|
|||
return acquired.registry;
|
||||
}
|
||||
|
||||
async [Symbol.asyncDispose](): Promise<void> {
|
||||
const claims = [...this.claims.values()];
|
||||
this.claims.clear();
|
||||
const results = await Promise.allSettled(claims.map((claim) => claim.release()));
|
||||
const failures = results.flatMap((result) =>
|
||||
result.status === "rejected" ? [result.reason] : [],
|
||||
);
|
||||
if (failures.length > 0) {
|
||||
throw new AggregateError(failures, "Prepared registry construction cleanup failed");
|
||||
}
|
||||
[Symbol.asyncDispose](): Promise<void> {
|
||||
this.registries.clear();
|
||||
return this.releases.disposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ export function retainRuntimePluginWork(registries: Iterable<PluginRegistry>): (
|
|||
|
||||
/** Keep replacement blocked through physical cleanup, including retained cleanup failures. */
|
||||
export async function releaseRuntimePluginWork(
|
||||
release: () => void | Promise<void>,
|
||||
release: (() => void | Promise<void>) | undefined,
|
||||
releaseWork: () => void,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await release();
|
||||
await release?.();
|
||||
} catch (error) {
|
||||
if (!hasRetainedPluginRuntimeCloseError(error)) {
|
||||
releaseWork();
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ import { prepareConfiguredRuntimeFacts } from "../prepared-model-runtime.configu
|
|||
import { prepareWorkspaceBuildGroup } from "../prepared-model-runtime.facts.js";
|
||||
import { createPreparedModelRuntimeSnapshot } from "../prepared-model-runtime.full-catalog.js";
|
||||
import { closePreparedModelRuntimeSnapshots } from "../prepared-model-runtime.lifecycle.js";
|
||||
import { retainPreparedPluginGeneration } from "../prepared-model-runtime.plugin-lifetime.js";
|
||||
import {
|
||||
retainPreparedPluginGeneration,
|
||||
retainPreparedPluginRegistry,
|
||||
} from "../prepared-model-runtime.plugin-lifetime.js";
|
||||
import {
|
||||
closeEphemeralPreparedModelRuntimeResources,
|
||||
PreparedModelRuntimeBuildResources,
|
||||
|
|
@ -181,7 +184,7 @@ module.exports = { id: '${id}', register(api) {
|
|||
}
|
||||
setActivePluginRegistry(donor.registry);
|
||||
const donorCurrent = capturePluginLifecycleAuthority(donor.registry);
|
||||
const construction = new PreparedModelRuntimeBuildResources();
|
||||
const construction = new PreparedModelRuntimeBuildResources(retainPreparedPluginRegistry);
|
||||
let releasePublication: ReturnType<typeof retainPreparedPluginGeneration> | undefined;
|
||||
let releasingOwners: Promise<PromiseSettledResult<void>[]> | undefined;
|
||||
let closeDonor: Promise<void> | undefined;
|
||||
|
|
@ -198,7 +201,11 @@ module.exports = { id: '${id}', register(api) {
|
|||
},
|
||||
],
|
||||
"static",
|
||||
{ includeCredentialProviders: false, registryResources: construction },
|
||||
{
|
||||
includeCredentialProviders: false,
|
||||
registryResources: construction,
|
||||
loadRuntimeRegistry: construction.load.bind(construction),
|
||||
},
|
||||
);
|
||||
if (mode === "rollback") {
|
||||
const source = getPluginRegistryInspectionResources(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import {
|
|||
runWithCronCreatorAuthorityCapability,
|
||||
type CronCreatorAuthorityCapability,
|
||||
} from "../../agents/cron-creator-authority-context.js";
|
||||
import { withPreparedEmbeddedGatewayTools } from "../../agents/embedded-agent-runner/run/attempt-tool-run-context.js";
|
||||
import { withPreparedEmbeddedGatewayTools } from "../../agents/embedded-agent-runner/run/attempt-gateway-tools.js";
|
||||
import * as hostFileWrite from "../../agents/host-file-write.js";
|
||||
import { makeSettledChild } from "../../agents/subagents/announce/subagent-announce.requester-settle-wake.test-support.js";
|
||||
import {
|
||||
|
|
|
|||
78
src/gateway/test-helpers.listener.test.ts
Normal file
78
src/gateway/test-helpers.listener.test.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import type { Server } from "node:http";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createDeferred } from "../../test/helpers/promise.js";
|
||||
import { reserveGatewayTestListener } from "./test-helpers.listener.js";
|
||||
|
||||
vi.mock("./server-runtime-state.js", () => ({
|
||||
createGatewayHttpTransport: async (params: { port: number; testListener?: Server }) =>
|
||||
params.testListener,
|
||||
}));
|
||||
|
||||
function createTestTransport(transport: typeof import("./server-runtime-state.js"), port: number) {
|
||||
return transport.createGatewayHttpTransport({
|
||||
port,
|
||||
} as Parameters<typeof transport.createGatewayHttpTransport>[0]);
|
||||
}
|
||||
|
||||
describe("reserved Gateway test listeners", () => {
|
||||
it("adopts a single reservation through the transport dispatcher", async () => {
|
||||
const transport = await import("./server-runtime-state.js");
|
||||
const reservation = await reserveGatewayTestListener();
|
||||
try {
|
||||
await expect(
|
||||
reservation.start(() => createTestTransport(transport, reservation.port)),
|
||||
).resolves.toBe(reservation.listener);
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
reservation.listener.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["first", "second"] as const)(
|
||||
"adopts overlapping reservations when %s startup settles first",
|
||||
async (firstToSettle) => {
|
||||
const transport = await import("./server-runtime-state.js");
|
||||
const first = await reserveGatewayTestListener();
|
||||
const second = await reserveGatewayTestListener();
|
||||
const reservations = [first, second];
|
||||
const entered = reservations.map(() => createDeferred());
|
||||
const release = reservations.map(() => createDeferred());
|
||||
const runs = reservations.map((reservation, index) =>
|
||||
reservation.start(async () => {
|
||||
entered[index]!.resolve();
|
||||
await release[index]!.promise;
|
||||
return createTestTransport(transport, reservation.port);
|
||||
}),
|
||||
);
|
||||
// Observe both rejections immediately, including the pre-fix recursive spy failure.
|
||||
const settled = Promise.allSettled(runs);
|
||||
try {
|
||||
await Promise.race([
|
||||
Promise.all(entered.map(({ promise }) => promise)),
|
||||
...runs.map(async (run) => {
|
||||
await run;
|
||||
throw new Error("Startup settled before both reservation callbacks entered");
|
||||
}),
|
||||
]);
|
||||
const order = firstToSettle === "first" ? [0, 1] : [1, 0];
|
||||
for (const index of order) {
|
||||
release[index]!.resolve();
|
||||
await expect(runs[index]).resolves.toBe(reservations[index]!.listener);
|
||||
}
|
||||
} finally {
|
||||
release.forEach((gate) => gate.resolve());
|
||||
await settled;
|
||||
// The synthetic transport returns the listener but does not own its close.
|
||||
await Promise.all(
|
||||
reservations.map(
|
||||
({ listener }) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
listener.close((error) => (error ? reject(error) : resolve()));
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -2,6 +2,35 @@ import assert from "node:assert/strict";
|
|||
import { createServer } from "node:http";
|
||||
import type { Socket } from "node:net";
|
||||
|
||||
type ListenerReservation = {
|
||||
listener: ReturnType<typeof createServer>;
|
||||
adopt: () => void;
|
||||
};
|
||||
|
||||
async function createListenerDispatcher() {
|
||||
const { vi } = await import("vitest");
|
||||
const transport = await import("./server-runtime-state.js");
|
||||
const createTransport = transport.createGatewayHttpTransport;
|
||||
const listeners = new Map<number, ListenerReservation>();
|
||||
const spy = vi
|
||||
.spyOn(transport, "createGatewayHttpTransport")
|
||||
.mockImplementation(async (params) => {
|
||||
const reservation = listeners.get(params.port);
|
||||
const runtime = await createTransport(
|
||||
reservation ? { ...params, testListener: reservation.listener } : params,
|
||||
);
|
||||
reservation?.adopt();
|
||||
return runtime;
|
||||
});
|
||||
return { listeners, restore: () => spy.mockRestore() };
|
||||
}
|
||||
|
||||
// Overlapping startups share the original constructor and its import. Recapturing
|
||||
// an installed spy would replace that same mock and make it call itself.
|
||||
let activeDispatcher:
|
||||
| { ready: ReturnType<typeof createListenerDispatcher>; pending: number }
|
||||
| undefined;
|
||||
|
||||
/** Retain the exact loopback listener until the real Gateway transport adopts it. */
|
||||
export async function reserveGatewayTestListener(port = 0) {
|
||||
const listener = createServer();
|
||||
|
|
@ -30,25 +59,33 @@ export async function reserveGatewayTestListener(port = 0) {
|
|||
listener,
|
||||
closeUnadopted,
|
||||
async start<T>(run: () => Promise<T>): Promise<T> {
|
||||
const { vi } = await import("vitest");
|
||||
const transport = await import("./server-runtime-state.js");
|
||||
const createTransport = transport.createGatewayHttpTransport;
|
||||
const spy = vi
|
||||
.spyOn(transport, "createGatewayHttpTransport")
|
||||
.mockImplementation(async (params) => {
|
||||
if (params.port !== address.port) {
|
||||
return createTransport(params);
|
||||
}
|
||||
const runtime = await createTransport({ ...params, testListener: listener });
|
||||
adopted = true;
|
||||
return runtime;
|
||||
});
|
||||
const owner = (activeDispatcher ??= { ready: createListenerDispatcher(), pending: 0 });
|
||||
owner.pending++;
|
||||
let dispatcher: Awaited<typeof owner.ready> | undefined;
|
||||
try {
|
||||
const result = await run();
|
||||
listener.off("connection", rejectEarlyConnection);
|
||||
return result;
|
||||
dispatcher = await owner.ready;
|
||||
assert(
|
||||
!dispatcher.listeners.has(address.port),
|
||||
"Reserved Gateway listener is already starting",
|
||||
);
|
||||
dispatcher.listeners.set(address.port, {
|
||||
listener,
|
||||
adopt: () => {
|
||||
adopted = true;
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await run();
|
||||
listener.off("connection", rejectEarlyConnection);
|
||||
return result;
|
||||
} finally {
|
||||
dispatcher.listeners.delete(address.port);
|
||||
}
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
if (--owner.pending === 0) {
|
||||
activeDispatcher = undefined;
|
||||
dispatcher?.restore();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Focused public test contracts for native agent-runtime adapters.
|
||||
|
||||
export { setHostToolFactoryForTest } from "./test-helpers/agents/host-tool-factory.js";
|
||||
export {
|
||||
AUTH_PROFILE_RUNTIME_CONTRACT,
|
||||
createAuthAliasManifestRegistry,
|
||||
|
|
|
|||
26
src/plugin-sdk/test-helpers/agents/host-tool-factory.ts
Normal file
26
src/plugin-sdk/test-helpers/agents/host-tool-factory.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { onTestFinished, vi } from "vitest";
|
||||
import type { createOpenClawCodingToolsInternal } from "../../../agents/agent-tools.js";
|
||||
|
||||
type ToolsFactory = typeof createOpenClawCodingToolsInternal;
|
||||
let createTools: ToolsFactory | undefined;
|
||||
const factories = new Map<string, ToolsFactory>();
|
||||
|
||||
/** Substitutes construction while preserving the real host's private authority and bindings. */
|
||||
export async function setHostToolFactoryForTest(
|
||||
params: { runId: string },
|
||||
factory: ToolsFactory,
|
||||
): Promise<void> {
|
||||
const agentTools = await import("../../../agents/agent-tools.js");
|
||||
const actual = (createTools ??= agentTools.createOpenClawCodingToolsInternal);
|
||||
factories.set(params.runId, factory);
|
||||
const spy = vi
|
||||
.spyOn(agentTools, "createOpenClawCodingToolsInternal")
|
||||
.mockImplementation((...args) => {
|
||||
const runFactory = args[0]?.runId ? factories.get(args[0].runId) : undefined;
|
||||
return (runFactory ?? actual)(...args);
|
||||
});
|
||||
onTestFinished(() => {
|
||||
factories.clear();
|
||||
spy.mockRestore();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue